diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..fcb1a06ad --- /dev/null +++ b/NOTICE @@ -0,0 +1,20 @@ +Charliecloud is copyright © Triad National Security, LLC and others. + +This software was produced in part under U.S. Government contract +89233218CNA000001 for Los Alamos National Laboratory (LANL), which is operated +by Triad National Security, LLC for the U.S. Department of Energy/National +Nuclear Security Administration. + +The Government is granted for itself and others acting on its behalf a +nonexclusive, paid-up, irrevocable worldwide license in this material to +reproduce, prepare derivative works, distribute copies to the public, perform +publicly and display publicly, and to permit others to do so. + +Neither the government nor Triad National Security, LLC makes any warranty, +express or implied, or assumes any liability for use of this software. + +If software is modified to produce derivative works, such derivative works +should be clearly marked, so as not to confuse it with the version available +from LANL. + +LA-CC 14-096 diff --git a/README.rst b/README.rst index 2fdb1b692..09f8d1b04 100644 --- a/README.rst +++ b/README.rst @@ -136,32 +136,15 @@ Other publications: Supercomputing*. DOI: `10.1145/3624062.3624585 `_. -Copyright and license ---------------------- - -Charliecloud is copyright © 2014–2023 Triad National Security, LLC and others. - -This software was produced under U.S. Government contract 89233218CNA000001 -for Los Alamos National Laboratory (LANL), which is operated by Triad National -Security, LLC for the U.S. Department of Energy/National Nuclear Security -Administration. - -This is open source software (LA-CC 14-096); you can redistribute it and/or -modify it under the terms of the Apache License, Version 2.0. A copy is -included in file LICENSE. You may not use this software except in compliance -with the license. - -The Government is granted for itself and others acting on its behalf a -nonexclusive, paid-up, irrevocable worldwide license in this material to -reproduce, prepare derivative works, distribute copies to the public, perform -publicly and display publicly, and to permit others to do so. +License and intellectual property +--------------------------------- -Neither the government nor Triad National Security, LLC makes any warranty, -express or implied, or assumes any liability for use of this software. +Charliecloud is open source software; you can redistribute it and/or modify it +under the terms of the Apache License, Version 2.0. A copy is included in file +LICENSE. You may not use this software except in compliance with the license. -If software is modified to produce derivative works, such derivative works -should be clearly marked, so as not to confuse it with the version available -from LANL. +Copyrights and patents are retained by contributors. No copyright assignment +is required to contribute to Charliecloud. .. LocalWords: USENIX's CNA Meisam figshare diff --git a/VERSION b/VERSION index d3a551e7c..90db34b47 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.38~pre +0.39~pre diff --git a/bin/ch-checkns.c b/bin/ch-checkns.c index 10f26969a..3425d0528 100644 --- a/bin/ch-checkns.c +++ b/bin/ch-checkns.c @@ -71,7 +71,7 @@ void fatal_(const char *file, int line, int errno_, const char *str) char *url = "https://github.com/hpc/charliecloud/blob/master/bin/ch-checkns.c"; printf("error: %s: %d: %s\n", file, line, str); printf("errno: %d\nsee: %s\n", errno_, url); - exit(1); + exit(EXIT_MISC_ERR); } int main(int argc, char *argv[]) diff --git a/bin/ch-completion.bash b/bin/ch-completion.bash index 79ba59725..6a56cb10b 100644 --- a/bin/ch-completion.bash +++ b/bin/ch-completion.bash @@ -259,14 +259,16 @@ _ch_convert_complete () { _image_build_opts="-b --bind --build-arg -f --file --force --force-cmd -n --dry-run --parse-only -t --tag" +_image_modify_opts="-c -S --shell" + _image_common_opts="-a --arch --always-download --auth --break --cache --cache-large --dependencies -h --help --no-cache --no-lock --no-xattrs --profile --rebuild --password-many -q --quiet -s --storage --tls-no-verify -v --verbose --version --xattrs" -_image_subcommands="build build-cache delete gestalt - import list pull push reset undelete" +_image_subcommands="build build-cache delete gestalt import + list modify pull push reset undelete" # archs taken from ARCH_MAP in charliecloud.py _archs="amd64 arm/v5 arm/v6 arm/v7 arm64/v8 386 mips64le ppc64le s390x" @@ -374,20 +376,26 @@ _ch_image_complete () { build-cache) COMPREPLY=( $(compgen -W "--reset --gc --tree --dot" -- "$cur") ) ;; - delete|list) - if [[ "$sub_cmd" == "list" ]]; then + delete|list|modify) + case "$sub_cmd" in + list) if [[ "$prev" == "--undeletable" || "$prev" == "--undeleteable" || "$prev" == "-u" ]]; then COMPREPLY=( $(compgen -W "$(_ch_undelete_list "$strg_dir")" -- "$cur") ) return 0 fi - extras+="$extras -l --long -u --undeletable" + extras="$extras -l --long -u --undeletable" # If “cur” starts with “--undelete,” add “--undeleteable” (the less # correct version of “--undeletable”) to the list of possible # completions. if [[ ${cur::10} == "--undelete" ]]; then extras="$extras --undeleteable" fi - fi + ;; + modify) + # FIXME: Implement + extras="$extras $_image_modify_opts" + ;; + esac COMPREPLY=( $(compgen -W "$(_ch_list_images "$strg_dir") $extras" -- "$cur") ) __ltrim_colon_completions "$cur" ;; diff --git a/bin/ch-image.py.in b/bin/ch-image.py.in index 4871c90b9..e3538ec59 100644 --- a/bin/ch-image.py.in +++ b/bin/ch-image.py.in @@ -13,7 +13,9 @@ import charliecloud as ch import build import build_cache as bu import filesystem as fs +import image as im import misc +import modify import pull import push @@ -278,6 +280,34 @@ def main(): sp.add_argument("image_ref", metavar="IMAGE_REF", nargs="?", help="print details of this image only") + # modify + sp = ap.add_parser("modify", "foo") + add_opts(sp, modify.main, deps_check=True, stog_init=True) + sp.add_argument("-c", metavar="CMD", action="append", default=[], nargs=1, + help="Run CMD as though specified by a RUN instruction. Can be repeated.") + sp.add_argument("-i", "--interactive", action="store_true", + help="modify in interactive mode, even if stdin is not a TTY") + sp.add_argument("-S", "--shell", metavar="shell", default="/bin/sh", + help="use SHELL instead of the default /bin/sh") + sp.add_argument("image_ref", metavar="IMAGE_REF", help="image to modify") + sp.add_argument("out_image", metavar="OUT_IMAGE", help="destination of modified image") + sp.add_argument("script", metavar="SCRIPT", help="foo", nargs='?') + # Options “modify” shares with “build”. Note that while we could abstract + # this out to avoid repeated lines, as we do for “common_opts”, we’ve decided + # that the tradeoff in code readability wouldn’t be worth it. + sp.add_argument("-b", "--bind", metavar="SRC[:DST]", + action="append", default=[], + help="mount SRC at guest DST (default: same as SRC)") + sp.add_argument("--build-arg", metavar="ARG[=VAL]", + action="append", default=[], + help="set build-time variable ARG to VAL, or $ARG if no VAL") + sp.add_argument("--force", metavar="MODE", nargs="?", default="seccomp", + type=ch.Force_Mode, const="seccomp", + help="inject unprivileged build workarounds") + sp.add_argument("--force-cmd", metavar="CMD,ARG1[,ARG2...]", + action="append", default=[], + help="command arg(s) to add under --force=seccomp") + # pull sp = ap.add_parser("pull", "copy image from remote repository to local filesystem") diff --git a/bin/ch-run.c b/bin/ch-run.c index e207cb016..f0778704e 100644 --- a/bin/ch-run.c +++ b/bin/ch-run.c @@ -228,7 +228,7 @@ int main(int argc, char *argv[]) if (arg_next >= argc - 1) { printf("usage: ch-run [OPTION...] IMAGE -- COMMAND [ARG...]\n"); - FATAL("IMAGE and/or COMMAND not specified"); + FATAL(0, "IMAGE and/or COMMAND not specified"); } args.c.img_ref = argv[arg_next++]; args.c.newroot = realpath_(args.c.newroot, true); @@ -262,11 +262,11 @@ int main(int argc, char *argv[]) break; case IMG_SQUASH: #ifndef HAVE_LIBSQUASHFUSE - FATAL("this ch-run does not support internal SquashFS mounts"); + FATAL(0, "this ch-run does not support internal SquashFS mounts"); #endif break; case IMG_NONE: - FATAL("unknown image type: %s", args.c.img_ref); + FATAL(0, "unknown image type: %s", args.c.img_ref); break; } @@ -497,7 +497,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) #ifdef HAVE_FNM_EXTMATCH exit(0); #else - exit(1); + exit(EXIT_MISC_ERR); #endif } else if (!strcmp(arg, "overlayfs")) { #ifdef HAVE_OVERLAYFS @@ -509,13 +509,13 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) #ifdef HAVE_SECCOMP exit(0); #else - exit(1); + exit(EXIT_MISC_ERR); #endif } else if (!strcmp(arg, "squash")) { #ifdef HAVE_LIBSQUASHFUSE exit(0); #else - exit(1); + exit(EXIT_MISC_ERR); #endif } else if (!strcmp(arg, "tmpfs-xattrs")) { #ifdef HAVE_TMPFS_XATTRS @@ -525,7 +525,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) #endif } else - FATAL("unknown feature: %s", arg); + FATAL(0, "unknown feature: %s", arg); break; case -12: // --home Tf (args->c.host_home = getenv("HOME"), "--home failed: $HOME not set"); @@ -556,7 +556,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) else if (!strcmp(arg, "log-fail")) test_logging(true); else - FATAL("invalid --test argument: %s; see source code", arg); + FATAL(0, "invalid --test argument: %s; see source code", arg); break; case 'b': { // --bind char *src, *dst; diff --git a/bin/ch-test b/bin/ch-test index 2788eed42..b023bc05d 100755 --- a/bin/ch-test +++ b/bin/ch-test @@ -299,16 +299,17 @@ pedantry_set () { if [[ $ch_pedantic == no ]]; then ch_pedantic= # proper boolean fi - # The motivation here is that in pedantic mode, we want to run all the - # tests we reasonably can. So, if the user *has* sudo, then default --sudo - # to yes. What is a little awkward is that “sudo -v” can generate a - # password prompt in the middle of the status output. An alternative is - # “sudo -nv”, which doesn’t; drawbacks are that you have to analyze the - # output (not exit code) and it generates a failed password log message if - # there is not already a sudo session going. + # Motivation here: In pedantic mode, we want to run all the tests we + # reasonably can. So, if the user *has* sudo, then default --sudo to yes. + # What is a little awkward is that “sudo true” can generate a password + # prompt in the middle of the status output. An alternative is “sudo -nv”, + # which doesn’t; drawbacks are that you have to analyze the output (not + # exit code) and it generates a failed password log message if there is + # not already a sudo session going. We also used to use “sudo -v”, which + # prompts for a password even if you have passwordless sudo set up. if [[ -n $ch_pedantic ]] \ && command -v sudo > /dev/null \ - && sudo -v > /dev/null 2>&1; then + && sudo true > /dev/null 2>&1; then use_sudo_default=yes else use_sudo_default= @@ -986,17 +987,6 @@ if [[ -z $phase ]]; then fi printf '\n' -# See issue #1580 -# shellcheck disable=SC2016 -if [[ -d /var/tmp/img ]] || [[ -d /var/tmp/tar ]]; then -printf '\n' - warning 'NOTE: default image and pack directories changed to:' - warning ' CH_TEST_IMGDIR=/var/tmp/${USER}.img' - warning ' CH_TEST_TARDIR=/var/tmp/${USER}.pack' -fi - -printf '\n' - # variable name CLI environment default # desc. width description vset CH_TEST_SCOPE "$scope" "$CH_TEST_SCOPE" standard \ diff --git a/bin/ch_core.c b/bin/ch_core.c index a19dc72d1..7e995121f 100644 --- a/bin/ch_core.c +++ b/bin/ch_core.c @@ -235,7 +235,7 @@ void bind_mount(const char *src, const char *dst, enum bind_dep dep, if (!path_exists(dst_full, NULL, true)) switch (dep) { case BD_REQUIRED: - FATAL("can't bind: destination not found: %s", dst_full); + FATAL(0, "can't bind: destination not found: %s", dst_full); break; case BD_OPTIONAL: return; @@ -422,7 +422,7 @@ enum img_type image_type(const char *ref, const char *storage_dir) return IMG_SQUASH; // Well now we’re stumped. - FATAL("unknown image type: %s", ref); + FATAL(0, "unknown image type: %s", ref); } char *img_name2path(const char *name, const char *storage_dir) @@ -568,7 +568,8 @@ void run_user_command(char *argv[], const char *initial_dir) if (verbose < LL_STDERR) T_ (freopen("/dev/null", "w", stderr)); execvp(argv[0], argv); // only returns if error - Tf (0, "can't execve(2): %s", argv[0]); + ERROR(errno, "can't execve(2): %s", argv[0]); + exit(EXIT_CMD); } /* Set up the fake-syscall seccomp(2) filter. This computes and installs a diff --git a/bin/ch_fuse.c b/bin/ch_fuse.c index ce60bbcc7..22ac1e5ca 100644 --- a/bin/ch_fuse.c +++ b/bin/ch_fuse.c @@ -188,7 +188,7 @@ int sq_loop(void) // Clean up zombie child if exit signal was SIGCHLD. if (!sigchld_received) - exit_code = 0; + exit_code = EXIT_SQUASH; else { Tf (wait(&child_status) >= 0, "can't wait for child"); if (WIFEXITED(child_status)) { @@ -203,7 +203,7 @@ int sq_loop(void) // // [1]: https://codereview.stackexchange.com/a/109349 // [2]: https://man7.org/linux/man-pages/man2/wait.2.html - exit_code = 1; + exit_code = 128 + WTERMSIG(child_status); VERBOSE("child terminated by signal %d", WTERMSIG(child_status)) } } @@ -254,7 +254,7 @@ void sq_mount(const char *img_path, char *mountpt) &OPS, sizeof(OPS), sq.ll)) { break; // success } else if (i <= 0) { - FATAL("too many FUSE errors; giving up"); + FATAL(0, "too many FUSE errors; giving up"); } else { WARNING("FUSE error mounting SquashFS; will retry"); sleep(1); diff --git a/bin/ch_misc.c b/bin/ch_misc.c index bdee7fa20..17e9b1ea0 100644 --- a/bin/ch_misc.c +++ b/bin/ch_misc.c @@ -449,7 +449,7 @@ void test_logging(bool fail) { INFO("info"); WARNING("warning"); if (fail) - FATAL("the program failed inexplicably (\"log-fail\" specified)"); + FATAL(0, "the program failed inexplicably (\"log-fail\" specified)"); exit(0); } @@ -586,6 +586,22 @@ void msg(enum log_level level, const char *file, int line, int errno_, va_end(ap); } +void msg_error(const char *file, int line, int errno_, + const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + /* We print errors at LL_FATAL because, according to our documentation, + errors are never suppressed. Perhaps we need to rename this log level (see + issue #1914). */ + msgv(LL_FATAL, file, line, errno_, fmt, ap); + va_end(ap); +} + +/* Note that msg_fatal doesn’t call msg_error like we do in the Python code + because the variable number of arguments make it easier to simply define + separate functions. */ noreturn void msg_fatal(const char *file, int line, int errno_, const char *fmt, ...) { @@ -595,7 +611,7 @@ noreturn void msg_fatal(const char *file, int line, int errno_, msgv(LL_FATAL, file, line, errno_, fmt, ap); va_end(ap); - exit(EXIT_FAILURE); + exit(EXIT_MISC_ERR); } /* va_list form of msg(). */ diff --git a/bin/ch_misc.h b/bin/ch_misc.h index f590a0890..e2f5dcd47 100644 --- a/bin/ch_misc.h +++ b/bin/ch_misc.h @@ -24,6 +24,11 @@ don’t need to worry about running out of room. */ #define WARNINGS_SIZE (4*1024) +/* Exit codes (see also: test/common.bash, lib/build.py). */ +#define EXIT_MISC_ERR 31 +#define EXIT_CMD 49 +#define EXIT_SQUASH 84 + /* Test some value, and if it's not what we expect, exit with a fatal error. These are macros so we have access to the file and line number. @@ -66,12 +71,13 @@ #define Zf(x, ...) if (x) msg_fatal(__FILE__, __LINE__, errno, __VA_ARGS__) #define Ze(x, ...) if (x) msg_fatal(__FILE__, __LINE__, 0, __VA_ARGS__) -#define FATAL(...) msg_fatal( __FILE__, __LINE__, 0, __VA_ARGS__); -#define WARNING(...) msg(LL_WARNING, __FILE__, __LINE__, 0, __VA_ARGS__); -#define INFO(...) msg(LL_INFO, __FILE__, __LINE__, 0, __VA_ARGS__); -#define VERBOSE(...) msg(LL_VERBOSE, __FILE__, __LINE__, 0, __VA_ARGS__); -#define DEBUG(...) msg(LL_DEBUG, __FILE__, __LINE__, 0, __VA_ARGS__); -#define TRACE(...) msg(LL_TRACE, __FILE__, __LINE__, 0, __VA_ARGS__); +#define FATAL(e, ...) msg_fatal( __FILE__, __LINE__, e, __VA_ARGS__); +#define ERROR(e, ...) msg_error( __FILE__, __LINE__, e, __VA_ARGS__); +#define WARNING(...) msg(LL_WARNING, __FILE__, __LINE__, 0, __VA_ARGS__); +#define INFO(...) msg(LL_INFO, __FILE__, __LINE__, 0, __VA_ARGS__); +#define VERBOSE(...) msg(LL_VERBOSE, __FILE__, __LINE__, 0, __VA_ARGS__); +#define DEBUG(...) msg(LL_DEBUG, __FILE__, __LINE__, 0, __VA_ARGS__); +#define TRACE(...) msg(LL_TRACE, __FILE__, __LINE__, 0, __VA_ARGS__); /** Types **/ @@ -134,6 +140,8 @@ void mkdirs(const char *base, const char *path, char **denylist, const char *scratch); void msg(enum log_level level, const char *file, int line, int errno_, const char *fmt, ...); +void msg_error(const char *file, int line, int errno_, + const char *fmt, ...); noreturn void msg_fatal(const char *file, int line, int errno_, const char *fmt, ...); bool path_exists(const char *path, struct stat *statbuf, bool follow_symlink); diff --git a/doc/_loc.rst b/doc/_loc.rst index e4afa0768..a6c1bf36b 100644 --- a/doc/_loc.rst +++ b/doc/_loc.rst @@ -1,24 +1,24 @@ .. Do not edit this file — it’s auto-generated. We pride ourselves on keeping Charliecloud lightweight and simple. The lines -of code as of version 0.37 is: +of code as of version 0.38 is: .. list-table:: * - Program itself - - 9079 + - 9087 * - Test suite & examples - - 12019 + - 12086 * - Documentation - - 6416 + - 6526 * - Build system - - 1294 + - 1298 * - Packaging - 629 * - Miscellaneous - - 506 + - 509 * - Total - - 29943 + - 30135 These include code only, excluding blank lines and comments. They were counted using `cloc `_ version 1.96. diff --git a/doc/ch-image.rst b/doc/ch-image.rst index 9443d836c..98ba00a96 100644 --- a/doc/ch-image.rst +++ b/doc/ch-image.rst @@ -594,6 +594,8 @@ not allowed in Git branch names), and the empty base of everything common instruction :code:`RUN echo foo`. +.. _ch-image_build: + :code:`build` ============= @@ -2106,6 +2108,213 @@ in the remote registry, so we don’t upload it again.) Delete all images and cache from ch-image builder storage. +:code:`modify` +============== + +Modify an image with shell commands, possibly interactively. + +Synopsis +-------- + +:: + + $ ch-image [...] shell [...] SOURCE DEST [SCRIPT] + +Description +----------- + +This subcommand modifies :code:`SOURCE` using shell commands to create +:code:`DEST`. These commands can be provided either interactively +(discouraged) or non-interactively. In the non-interactive case, the commands +are repackaged internally into a Dockerfile. + +Options: + + :code:`-c CMD` + Run :code:`CMD` as though specified by a :code:`RUN` instruction. Can be + repeated to run multiple commands sequentially. + + :code:`-i` + Execute the shell in interactive mode (by specifying :code:`-i` to it) + even if standard input is not a TTY. + + :code:`-S`, :code:`--shell SHELL` + Use :code:`SHELL` instead of the default :code:`/bin/sh`. + +The following options are shared with :code:`ch-image build`. For more details +about these options, see the section on :ref:`build `. + + :code:`-b`, :code:`--bind SRC[:DST]` + For :code:`RUN` instructions only, bind-mount :code:`SRC` at guest + :code:`DST`. + + :code:`--build-arg KEY[=VALUE]` + Set build-time variable :code:`KEY` defined by :code:`ARG` instruction + to :code:`VALUE`. + + :code:`--force[=MODE]` + Use unprivileged build with root emulation mode :code:`MODE`. + + :code:`--force-cmd=CMD,ARG1[,ARG2...]` + If command :code:`CMD` is found in a :code:`RUN` instruction, add the + comma-separated :code:`ARGs` to it. + +:code:`ch-image modify` operates in one of the following three modes. If the +mode desired is ambiguous, that is an error. + +Non-interactive mode, commands specified with :code:`-c` +-------------------------------------------------------- + +The following are equivalent:: + + $ ch-image modify -S /bin/ash -c 'echo hello' -c 'echo world' foo bar + +and:: + + $ ch-image build -t bar <<'EOF' + FROM foo + SHELL /bin/ash + RUN echo hello + RUN echo world + EOF + +:code:`ch-image` simply builds a Dockerfile internally that uses :code:`foo` as +a base image, starts with an appropriate :code:`SHELL` if :code:`-S` was given, +converts each :code:`-c` to a :code:`RUN` command, and executes this Dockerfile +to produce image :code:`bar`. As with regular Dockerfiles, if any command fails, +the build fails and no further commands are attempted. + +This mode provides detailed image provenance just like a Dockerfile. + +Non-interactive mode using a shell script +----------------------------------------- + +The following are equivalent:: + + $ ch-image modify foo bar /baz/qux.sh + +and:: + + $ ch-image build -t bar -f - / <<'EOF' + FROM foo + COPY /baz/qux.sh /ch/script.sh + RUN /bin/sh /ch/script.sh + +That is, :code:`ch-image` uses :code:`COPY` to put the script inside the +image, then runs it. + +If :code:`SCRIPT` is not provided, standard input is not a TTY, and :code:`-i` +is not specified, a script is read from stdin instead. In this case, standard +input is copied in full to a file in a temporary directory, which is used as the +context. The file’s modification time is set to 1993-10-21T10:00:00Z and its +name to a hash of the content, so the cache hits if the content is the same and +misses if not. That is, the following are equivalent:: + + $ ch-image modify foo bar <<'EOF' + echo hello world + EOF + +and:: + + $ ctx=$(mktemp -d) + $ cat > $ctx/foo <<'EOF' + echo hello world + EOF + $ hash=$(md5sum $ctx/foo) + $ mv $ctx/foo $ctx/$hash + $ touch -d 1993-10-21T10:00:00Z $ctx/$hash + $ ch-image build -t bar -f - $ctx < exit + $ ch-image build-cache --tree + * (baz) MODIFY interactive + * (foo) IMPORT foo + | * (bar) IMPORT bar + |/ + * (root) ROOT + + named images: 4 + state IDs: 4 + large files: 0 + commits: 4 + internal files: 7 K + disk used: 227 MiB + + :code:`undelete` ================ @@ -2135,4 +2344,4 @@ Environment variables .. LocalWords: dlcache graphviz packfile packfiles bigFileThreshold fd Tpdf .. LocalWords: pstats gprof chofile cffd cacdb ARGs NSYNC dst imgroot popt .. LocalWords: globbed ni AHSXpr drwxrwx ctx sym nom newB newC newD dstC -.. LocalWords: dstB dstF dstG upover drwx kexec pdb +.. LocalWords: dstB dstF dstG upover drwx kexec pdb mktemp diff --git a/doc/ch-run.rst b/doc/ch-run.rst index 2771078e4..1515ae5ac 100644 --- a/doc/ch-run.rst +++ b/doc/ch-run.rst @@ -749,15 +749,27 @@ would terminate the string. Exit status =========== -If there is an error during containerization, :code:`ch-run` exits with status -non-zero. If the user command is started successfully, the exit status is that -of the user command, with one exception: if the image is an internally mounted -SquashFS filesystem and the user command is killed by a signal, the exit -status is 1 regardless of the signal value. +If the user command is started successfully and exits normally, +:code:`ch-run`’s exit status is that of the user command. Otherwise, the exit +status is one of: +.. list-table:: + :header-rows: 0 + + * - 31 + - Miscellaneous :code:`ch-run` failure other than the below + * - 49 + - Unable to start user command (i.e., :code:`execvp(2)` failed) + * - 84 + - SquashFUSE loop exited on signal before user command was complete + * - 87 + - Feature queried by :code:`--feature` is not available + * - 128 + *N* + - User command killed by signal *N* .. include:: ./bugs.rst .. include:: ./see_also.rst + .. LocalWords: mtune NEWROOT hugetlbfs UsrMerge fusermount mybox IMG HOSTPATH .. LocalWords: noprofile norc SHLVL PWD kernelnewbies extglob diff --git a/doc/conf.py b/doc/conf.py index 59588572b..7a40c46b3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -65,7 +65,7 @@ # General information about the project. project = u'Charliecloud' -copyright = u'2014–2023, Triad National Security, LLC and others' +copyright = u'Charliecloud a Series of LF Projects, LLC and others (web content and website). For web site terms of use, trademark policy and other project policies please see: https://lfprojects.org' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/doc/dev.rst b/doc/dev.rst index 4a3f53284..73d5e84a6 100644 --- a/doc/dev.rst +++ b/doc/dev.rst @@ -253,12 +253,6 @@ fail or that won’t give you additional information, and not pushing every commit (CI tests only the most recent commit in a pushed group). Avoid making commits merely to trigger CI. -**Purging Docker cache.** :code:`misc/docker-clean.sh` can be used to purge -your Docker cache, either by removing all tags or deleting all containers and -images. The former is generally preferred, as it lets you update only those -base images that have actually changed (the ones that haven’t will be -re-tagged). - Issue labeling -------------- diff --git a/examples/multistage/test.bats b/examples/multistage/test.bats index ae0dcaa45..14d4d3c1a 100644 --- a/examples/multistage/test.bats +++ b/examples/multistage/test.bats @@ -37,7 +37,7 @@ setup () { # Can’t run GCC. run ch-run "$ch_img" -- gcc --version echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_CMD ]] [[ $output = *'gcc: No such file or directory'* ]] # No GCC or Make. diff --git a/lib/Makefile.am b/lib/Makefile.am index 8d8f1378c..0dbff3807 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -13,6 +13,7 @@ dist_mylib_DATA = base.sh \ force.py \ image.py \ misc.py \ + modify.py \ pull.py \ push.py \ registry.py diff --git a/lib/build.py b/lib/build.py index 49fdccc57..091ef1c24 100644 --- a/lib/build.py +++ b/lib/build.py @@ -9,6 +9,7 @@ import os.path import re import shutil +import subprocess import sys import charliecloud as ch @@ -57,6 +58,11 @@ class Environment: of this is just passed through from the image metadata.""" +# Class responsible for traversing the parse tree generated by lark. “Main_Loop” +# visits each node in the parse tree and calls its “__default__” method to +# figure out what to do with the node. This behavior is defined by the parent +# class, “lark.Visitor”, documented here: +# https://lark-parser.readthedocs.io/en/latest/visitors.html class Main_Loop(lark.Visitor): __slots__ = ("instruction_total_ct", @@ -69,6 +75,17 @@ def __init__(self, *args, **kwargs): self.instruction_total_ct = 0 super().__init__(*args, **kwargs) + # The main argument of the “__default__” method is “tree”, which is really + # just the current node being visited (the node is called “tree” because it + # represents the root node of the current subtree). The “tree.data” attribute + # gives the “name of the rule or alias” represented by the node. When a + # “lark.Visitor” instance visits a parse tree node, it checks the node’s + # “data” attribute and tries to call its own attribute (e.g. method) of the + # same name. If no such attribute exists, it calls “__default__”. Note that + # in this class, we don’t define any attributes corresponding to the + # Dockerfile instructions that we want to execute, so “__default__” is always + # called when visiting a node. We rely on instruction classes to execute the + # instructions, rather than attributes of this class. def __default__(self, tree): class_ = tree.data.title() + "_G" if (class_ in globals()): @@ -105,13 +122,34 @@ def __default__(self, tree): self.inst_prev = inst self.instruction_total_ct += 1 - def main(cli_): # CLI namespace. :P global cli cli = cli_ + cli_process_common(cli) + + # Process CLI. Make appropriate modifications to “cli” instance and return + # Dockerfile text. + text = cli_process(cli) + + tree = parse_dockerfile(text) + + # Count the number of stages (i.e., FROM instructions) + global image_ct + image_ct = sum(1 for i in tree.children_("from_")) + + parse_tree_traverse(tree, image_ct, cli) + +## Functions ## + +# Function that processes parsed CLI, modifying the passed “cli” object +# appropriatley as it does. Returns the text of the file used for the build +# operation. Note that Python passes variables to functions by their object +# reference, so changes made to mutable objects (which “cli” is) will persist in +# the scope of the caller.' +def cli_process(cli): # Infer input file if needed. if (cli.file is None): cli.file = cli.context + "/Dockerfile" @@ -141,6 +179,35 @@ def main(cli_): cli.tag = re.sub(r"[^a-z0-9_.-]", "", cli.tag.lower()) ch.INFO("inferred image name: %s" % cli.tag) + + ch.DEBUG(cli) + + # Guess whether the context is a URL, and error out if so. This can be a + # typical looking URL e.g. “https://...” or also something like + # “git@github.com:...”. The line noise in the second line of the regex is + # to match this second form. Username and host characters from + # https://tools.ietf.org/html/rfc3986. + if (re.search(r""" ^((git|git+ssh|http|https|ssh):// + | ^[\w.~%!$&'\(\)\*\+,;=-]+@[\w.~%!$&'\(\)\*\+,;=-]+:)""", + cli.context, re.VERBOSE) is not None): + ch.FATAL("not yet supported: issue #773: URL context: %s" % cli.context) + if (os.path.exists(cli.context + "/.dockerignore")): + ch.WARNING("not yet supported, ignored: issue #777: .dockerignore file") + + # Read input file. + if (cli.file == "-" or cli.context == "-"): + text = ch.ossafe("can’t read stdin", sys.stdin.read) + elif (not os.path.isdir(cli.context)): + ch.FATAL("context must be a directory: %s" % cli.context) + else: + fp = fs.Path(cli.file).open("rt") + text = ch.ossafe("can’t read: %s" % cli.file, fp.read) + ch.close_(fp) + + return text + +# Process common opts between modify and build. +def cli_process_common(cli): # --force and friends. if (cli.force_cmd and cli.force == ch.Force_Mode.FAKEROOT): ch.FATAL("--force-cmd and --force=fakeroot are incompatible") @@ -174,30 +241,8 @@ def build_arg_get(arg): ch.FATAL("--build-arg: %s: no value and not in environment" % kv[0]) return (kv[0], v) cli.build_arg = dict( build_arg_get(i) for i in cli.build_arg ) - ch.DEBUG(cli) - - # Guess whether the context is a URL, and error out if so. This can be a - # typical looking URL e.g. “https://...” or also something like - # “git@github.com:...”. The line noise in the second line of the regex is - # to match this second form. Username and host characters from - # https://tools.ietf.org/html/rfc3986. - if (re.search(r""" ^((git|git+ssh|http|https|ssh):// - | ^[\w.~%!$&'\(\)\*\+,;=-]+@[\w.~%!$&'\(\)\*\+,;=-]+:)""", - cli.context, re.VERBOSE) is not None): - ch.FATAL("not yet supported: issue #773: URL context: %s" % cli.context) - if (os.path.exists(cli.context + "/.dockerignore")): - ch.WARNING("not yet supported, ignored: issue #777: .dockerignore file") - - # Read input file. - if (cli.file == "-" or cli.context == "-"): - text = ch.ossafe("can’t read stdin", sys.stdin.read) - elif (not os.path.isdir(cli.context)): - ch.FATAL("context must be a directory: %s" % cli.context) - else: - fp = fs.Path(cli.file).open("rt") - text = ch.ossafe("can’t read: %s" % cli.file, fp.read) - ch.close_(fp) +def parse_dockerfile(text): # Parse it. parser = lark.Lark(im.GRAMMAR_DOCKERFILE, parser="earley", propagate_positions=True, tree_class=im.Tree) @@ -216,10 +261,6 @@ def build_arg_get(arg): if (cli.parse_only): ch.exit(0) - # Count the number of stages (i.e., FROM instructions) - global image_ct - image_ct = sum(1 for i in tree.children_("from_")) - # If we use RSYNC, error out quickly if appropriate rsync(1) not present. if (tree.child("rsync") is not None): try: @@ -228,21 +269,28 @@ def build_arg_get(arg): ch.ERROR("Dockerfile uses RSYNC, so rsync(1) is required") raise - # Traverse the tree and do what it says. - # - # We don’t actually care whether the tree is traversed breadth-first or - # depth-first, but we *do* care that instruction nodes are visited in - # order. Neither visit() nor visit_topdown() are documented as of - # 2020-06-11 [1], but examining source code [2] shows that visit_topdown() - # uses Tree.iter_trees_topdown(), which *is* documented to be in-order [3]. - # - # This change seems to have been made in 0.8.6 (see PR #761); before then, - # visit() was in order. Therefore, we call that instead, if visit_topdown() - # is not present, to improve compatibility (see issue #792). - # - # [1]: https://lark-parser.readthedocs.io/en/latest/visitors/#visitors - # [2]: https://github.com/lark-parser/lark/blob/445c8d4/lark/visitors.py#L211 - # [3]: https://lark-parser.readthedocs.io/en/latest/classes/#tree + return tree + +# Traverse Lark parse tree and do what it says. +# +# We don’t actually care whether the tree is traversed breadth-first or +# depth-first, but we *do* care that instruction nodes are visited in order. +# Neither visit() nor visit_topdown() are documented as of 2020-06-11 [1], but +# examining source code [2] shows that visit_topdown() uses +# Tree.iter_trees_topdown(), which *is* documented to be in-order [3]. +# +# This change seems to have been made in 0.8.6 (see PR #761); before then, +# visit() was in order. Therefore, we call that instead, if visit_topdown() is +# not present, to improve compatibility (see issue #792). +# +# [1]: https://lark-parser.readthedocs.io/en/latest/visitors/#visitors +# [2]: https://github.com/lark-parser/lark/blob/445c8d4/lark/visitors.py#L211 +# [3]: https://lark-parser.readthedocs.io/en/latest/classes/#tree +def parse_tree_traverse(tree, image_ct_, cli_): + global cli + global image_ct + cli = cli_ + image_ct = image_ct_ ml = Main_Loop() if (hasattr(ml, 'visit_topdown')): ml.visit_topdown(tree) @@ -271,9 +319,6 @@ def build_arg_get(arg): ch.INFO("build slow? consider enabling the build cache", "https://hpc.github.io/charliecloud/command-usage.html#build-cache") - -## Functions ## - def unescape(sl): # FIXME: This is also ugly and should go in the grammar. # @@ -287,7 +332,6 @@ def unescape(sl): assert (len(sl) >= 2 and sl[0] == '"' and sl[-1] == '"' and sl[-2:] != '\\"') return ast.literal_eval(sl) - ## Supporting classes ## class Instruction(abc.ABC): diff --git a/lib/build_cache.py b/lib/build_cache.py index 41ecded9d..92e1ba3a5 100644 --- a/lib/build_cache.py +++ b/lib/build_cache.py @@ -1379,6 +1379,9 @@ def __init__(self, *args): def __str__(self): return "disabled" + def branch_nocheckout(self, src_ref, dest): + pass + def checkout(self, image, git_hash, base_image): ch.INFO("copying image ...") image.unpack_clear() diff --git a/lib/charliecloud.py b/lib/charliecloud.py index 988a4054c..37ceb3283 100644 --- a/lib/charliecloud.py +++ b/lib/charliecloud.py @@ -41,6 +41,12 @@ class Build_Mode(enum.Enum): DISABLED = "disabled" REBUILD = "rebuild" +# ch-run exit codes (see also: bin/ch_misc.h) +class Ch_Run_Retcode(enum.Enum): + EXIT_MISC_ERR = 31 + EXIT_CMD = 49 + EXIT_SQUASH = 84 + # Download cache mode. class Download_Mode(enum.Enum): ENABLED = "enabled" diff --git a/lib/modify.py b/lib/modify.py new file mode 100644 index 000000000..3a59b9459 --- /dev/null +++ b/lib/modify.py @@ -0,0 +1,225 @@ +# implementation of ch-image modify + +import enum +import os +import subprocess +import sys +import tempfile +import uuid + +import charliecloud as ch +import build +import build_cache as bu +import force +import image as im + +lark = im.lark + +class Modify_Mode(enum.Enum): + COMMAND_SEQ = "commands" + INTERACTIVE = "interactive" + SCRIPT = "script" + +def main(cli_): + # In this file, “cli” is used as a global variable + global cli + cli = cli_ + + # CLI opts that “build.py” expects, but that don’t make sense in the context + # of “modify”. We set “parse_only” to “False” because we don’t do any + # parsing, and “context” to the root of the filesystem to ensure that + # necessary files (e.g. the modify script) will always be somewhere in the + # context dir. + cli.parse_only = False + cli.context = os.path.abspath(os.sep) + + build.cli_process_common(cli) + + commands = [] + # “Flatten” commands array + for c in cli.c: + commands += c + src_image = im.Image(im.Reference(cli.image_ref)) + out_image = im.Image(im.Reference(cli.out_image)) + if (not src_image.unpack_exist_p): + ch.FATAL("not in storage: %s" % src_image.ref) + if (cli.out_image == cli.image_ref): + ch.FATAL("output must be different from source image (%s)" % cli.image_ref) + if (cli.script is not None): + if (not ch.Path(cli.script).exists): + ch.FATAL("%s: no such file" % cli.script) + + # This kludge is necessary because cli is a global variable, with cli.tag + # assumed present elsewhere in the file. Here, cli.tag represents the + # destination image. + cli.tag = str(out_image) + + # Determine modify mode based on what is present in command line + if (commands != []): + if (cli.interactive): + ch.FATAL("incompatible opts: “-c”, “-i”") + if (cli.script is not None): + ch.FATAL("script mode incompatible with command mode") + mode = Modify_Mode.COMMAND_SEQ + elif (cli.script is not None): + if (cli.interactive): + ch.FATAL("script mode incompatible with interactive mode") + mode = Modify_Mode.SCRIPT + elif (sys.stdin.isatty() or (cli.interactive)): + mode = Modify_Mode.INTERACTIVE + else: + # Write stdin to tempfile, copy tempfile into container as a script, run + # script. + stdin = sys.stdin.read() + if (stdin == ''): + ch.FATAL("modify mode unclear") + + tmp = tempfile.NamedTemporaryFile() + with open(tmp.name, 'w') as fd: + fd.write(stdin) + + cli.script = tmp.name + + mode = Modify_Mode.SCRIPT + + ch.VERBOSE("modify shell: %s" % cli.shell) + ch.VERBOSE("modify mode: %s" % mode.value) + + if (mode == Modify_Mode.INTERACTIVE): + # Interactive case + + # Generate “fake” SID for build cache. We do this because we can’t compute + # an SID, but we still want to make sure that it’s unique enough that + # we’re unlikely to run into a collision. + fake_sid = uuid.uuid4() + out_image.unpack_clear() + out_image.copy_unpacked(src_image) + bu.cache.worktree_adopt(out_image, src_image.ref.for_path) + bu.cache.ready(out_image) + bu.cache.branch_nocheckout(src_image.ref, out_image.ref) + foo = subprocess.run([ch.CH_BIN + "/ch-run", "--unsafe", "-w"] + + sum([["-b", i] for i in cli.bind], []) + + [str(out_image.ref), "--", cli.shell]) + if (foo.returncode == ch.Ch_Run_Retcode.EXIT_CMD.value): + # FIXME: Write a better error message? + ch.FATAL("can't run shell: %s" % cli.shell) + ch.VERBOSE("using SID %s" % fake_sid) + # FIXME: metadata history stuff? See misc.import_. + if (out_image.metadata["history"] == []): + out_image.metadata["history"].append({ "empty_layer": False, + "command": "ch-image import"}) + out_image.metadata_save() + bu.cache.commit(out_image.unpack_path, fake_sid, "MODIFY interactive", []) + else: + # non-interactive case + if (mode == Modify_Mode.SCRIPT): + # script specified + tree = modify_tree_make_script(src_image.ref, cli.script) + elif (mode == Modify_Mode.COMMAND_SEQ): + # “-c” specified + tree = modify_tree_make(src_image.ref, commands) + else: + assert False, "unreachable code reached" + + # FIXME: pretty printing should prob go here, see issue #1908. + image_ct = sum(1 for i in tree.children_("from_")) + + build.parse_tree_traverse(tree, image_ct, cli) + +def modify_tree_make(src_img, cmds): + """Construct a parse tree corresponding to a set of “ch-image modify” + commands, as though the commands had been specified in a Dockerfile. Note + that because “ch-image modify” simply executes one or more commands inside + a container, the only Dockerfile instructions we need to consider are + “FROM” and “RUN”. E.g. for the command line + + $ ch-image modify -c 'echo foo' -c 'echo bar' -- foo foo2 + + this function produces the following parse tree + + start + dockerfile + from_ + image_ref + IMAGE_REF foo + run + run_shell + LINE_CHUNK echo foo + run + run_shell + LINE_CHUNK echo bar + """ + # Children of dockerfile tree + df_children = [] + # Metadata attribute. We use this attribute in the “_pretty” method for our + # “Tree” class. Constructing a tree without specifying a “Meta” instance that + # has been given a “line” value will result in the attribute not being present, + # which causes an error when we try to access that attribute. Here we give the + # attribute a debug value of -1 to avoid said errors. + meta = lark.tree.Meta() + meta.line = -1 + df_children.append(im.Tree(lark.Token('RULE', 'from_'), + [im.Tree(lark.Token('RULE', 'image_ref'), + [lark.Token('IMAGE_REF', str(src_img))], + meta) + ], meta)) + if (cli.shell is not None): + df_children.append(im.Tree(lark.Token('RULE', 'shell'), + [lark.Token('STRING_QUOTED', '"%s"' % cli.shell), + lark.Token('STRING_QUOTED', '"-c"') + ],meta)) + for cmd in cmds: + df_children.append(im.Tree(lark.Token('RULE', 'run'), + [im.Tree(lark.Token('RULE', 'run_shell'), + [lark.Token('LINE_CHUNK', cmd)], + meta) + ], meta)) + return im.Tree(lark.Token('RULE', 'start'), [im.Tree(lark.Token('RULE','dockerfile'), df_children)], meta) + +def modify_tree_make_script(src_img, path): + """Temporary(?) analog of “modify_tree_make” for the non-interactive version + of “modify” using a script. For the command line: + + $ ch-image modify foo foo2 /path/to/script + + this function produces the following parse tree + + start + dockerfile + from_ + image_ref + IMAGE_REF foo + copy + copy_shell + WORD /path/to/script WORD /ch/script.sh + run + run_shell + LINE_CHUNK /bin/sh /ch/script.sh + """ + # Children of dockerfile tree + df_children = [] + # Metadata attribute. We use this attribute in the “_pretty” method for our + # “Tree” class. Constructing a tree without specifying a “Meta” instance that + # has been given a “line” value will result in the attribute not being present, + # which causes an error when we try to access that attribute. Here we give the + # attribute a debug value of -1 to avoid said errors. + meta = lark.tree.Meta() + meta.line = -1 + df_children.append(im.Tree(lark.Token('RULE', 'from_'), + [im.Tree(lark.Token('RULE', 'image_ref'), + [lark.Token('IMAGE_REF', str(src_img))], + meta) + ], meta)) + df_children.append(im.Tree(lark.Token('RULE', 'copy'), + [im.Tree(lark.Token('RULE', 'copy_shell'), + [lark.Token('WORD', path), + lark.Token('WORD', '/ch/script.sh') + ], meta) + ],meta)) + # FIXME: Add error handling if “cli.shell” doesn’t exist (issue #1913). + df_children.append(im.Tree(lark.Token('RULE', 'run'), + [im.Tree(lark.Token('RULE', 'run_shell'), + [lark.Token('LINE_CHUNK', '%s /ch/script.sh' % cli.shell)], + meta) + ], meta)) + return im.Tree(lark.Token('RULE', 'start'), [im.Tree(lark.Token('RULE','dockerfile'), df_children)], meta) diff --git a/misc/Makefile.am b/misc/Makefile.am index 166b7871f..bd0d800a6 100644 --- a/misc/Makefile.am +++ b/misc/Makefile.am @@ -1 +1 @@ -EXTRA_DIST = docker-clean.sh grep version +EXTRA_DIST = grep version diff --git a/misc/docker-clean.sh b/misc/docker-clean.sh deleted file mode 100755 index 23ea9321a..000000000 --- a/misc/docker-clean.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -# FIXME: Give up after a certain number of iterations. - -set -e - -# Remove all containers. -while true; do - cmd='sudo docker ps -aq' - cs_ct=$($cmd | wc -l) - echo "found $cs_ct containers" - [[ 0 -eq $cs_ct ]] && break - # shellcheck disable=SC2046 - sudo docker rm $($cmd) -done - -# Untag all images. This fails with: -# -# Error response from daemon: invalid reference format -# -# sometimes. I don’t know why. -if [[ $1 != --all ]]; then - while true; do - cmd='sudo docker images --filter dangling=false --format {{.Repository}}:{{.Tag}}' - tag_ct=$($cmd | wc -l) - echo "found $tag_ct tagged images" - [[ 0 -eq $tag_ct ]] && break - # shellcheck disable=SC2046 - sudo docker rmi -f --no-prune $($cmd) - done -fi - -# If --all specified, remove all images. -if [[ $1 = --all ]]; then - while true; do - cmd='sudo docker images -q' - img_ct=$($cmd | wc -l) - echo "found $img_ct images" - [[ 0 -eq $img_ct ]] && break - # shellcheck disable=SC2046 - sudo docker rmi -f $($cmd) - done -fi diff --git a/misc/loc b/misc/loc index eed3ce947..84c5872d5 100755 --- a/misc/loc +++ b/misc/loc @@ -127,6 +127,7 @@ patch plain text filter remove_matches TEXT_HAS_NO_COMMENTS_BUT_A_FILTER_IS_REQUIRED extension txt + filename NOTICE filename PERUSEME filename README filename VERSION @@ -257,6 +258,7 @@ countem "TEST SUITE & EXAMPLES" /tmp/loc.test # documentation find . -type f -a \( \ -path './doc/*.rst' \ + -o -path ./NOTICE \ -o -path ./README.rst \ -o -path ./doc/conf.py \ -o -path ./doc/make-deps-overview \ @@ -283,7 +285,6 @@ find . -type f -a \( \ -path ./.gitattributes \ -o -path ./.gitignore \ -o -path ./VERSION \ - -o -path ./misc/docker-clean.sh \ -o -path ./misc/branches-tidy \ -o -path ./misc/grep \ -o -path ./misc/loc \) | sort > /tmp/loc.misc diff --git a/packaging/fedora/charliecloud.spec b/packaging/fedora/charliecloud.spec index d2eac0262..ed5088da3 100644 --- a/packaging/fedora/charliecloud.spec +++ b/packaging/fedora/charliecloud.spec @@ -154,6 +154,7 @@ ln -s "${sphinxdir}/js" %{buildroot}%{_pkgdocdir}/html/_static/js %{_prefix}/lib/%{name}/lark %{_prefix}/lib/%{name}/lark-1.1.9.dist-info %{_prefix}/lib/%{name}/misc.py +%{_prefix}/lib/%{name}/modify.py %{_prefix}/lib/%{name}/pull.py %{_prefix}/lib/%{name}/push.py %{_prefix}/lib/%{name}/registry.py diff --git a/test/build/50_ch-image.bats b/test/build/50_ch-image.bats index ab8a628f7..290e929b1 100644 --- a/test/build/50_ch-image.bats +++ b/test/build/50_ch-image.bats @@ -864,17 +864,17 @@ EOF @test 'ch-run storage errors' { run ch-run -v -w alpine:3.17 -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *'error: --write invalid when running by name'* ]] run ch-run -v "$CH_IMAGE_STORAGE"/img/alpine+3.17 -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"error: can't run directory images from storage (hint: run by name)"* ]] run ch-run -v -s /doesnotexist alpine:3.17 -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *'warning: storage directory not found: /doesnotexist'* ]] [[ $output = *"error: can't stat: alpine:3.17: No such file or directory"* ]] } @@ -979,3 +979,46 @@ EOF [[ $status -eq 0 ]] [[ $output = *'PWD=/bar/baz'* ]] } + +@test "ch-image modify" { + + # -c success, echo + run ch-image modify -c "echo foo" -c "echo bar" -- alpine:3.17 tmpimg + echo "$output" + [[ $status -eq 0 ]] + [[ $output = *'foo'* ]] + [[ $output = *'bar'* ]] + + # -c success, create file + ch-image modify -c "touch /home/foo" -- alpine:3.17 tmpimg + run ch-run tmpimg -- ls /home + echo "$output" + [[ $status -eq 0 ]] + [[ $output = *'foo'* ]] + + # non-interactive, script + echo "touch /home/bar" >> "${BATS_TMPDIR}/modify-script.sh" + ch-image modify alpine:3.17 tmpimg "${BATS_TMPDIR}/modify-script.sh" + run ch-run tmpimg -- ls /home + echo "$output" + [[ $status -eq 0 ]] + [[ $output = *'bar'* ]] + + # non-interactive, here doc + ch-image modify alpine:3.17 tmpimg <<'EOF' +touch /home/foobar +EOF + [[ -f "$CH_IMAGE_STORAGE/img/tmpimg/home/foobar" ]] + + # -c fail + run ch-image modify -c 'echo foo' -- alpine:3.17 alpine:3.17 + echo "$output" + [[ $status -eq 1 ]] + [[ $output = *'output must be different from source image'* ]] + + # non-existent shell + run ch-image modify -i -S "doesnotexist" -- alpine:3.17 tmpimg + echo "$output" + [[ $status -eq 1 ]] + [[ $output = *"can't run shell:"* ]] +} diff --git a/test/build/50_dockerfile.bats b/test/build/50_dockerfile.bats index e25f99d59..6a10d73a3 100644 --- a/test/build/50_dockerfile.bats +++ b/test/build/50_dockerfile.bats @@ -1160,7 +1160,9 @@ EOF [[ $status -ne 0 ]] [[ $output = *'error: no context because '?'-'?' given'* \ || $output = *'COPY failed: file not found in build context or'* \ - || $output = *'no such file or directory'* ]] + || $output = *'no such file or directory'* + || $output = *'not found'* + || $output = *'failed to compute cache key: failed to calculate checksum of ref'* ]] } diff --git a/test/build/55_cache.bats b/test/build/55_cache.bats index 2c125484a..7de25d4ab 100644 --- a/test/build/55_cache.bats +++ b/test/build/55_cache.bats @@ -1580,3 +1580,61 @@ EOF [[ $status -eq 0 ]] [[ $output != *'image erroneously marked cached, fixing'* ]] } + +@test "${tag}: modify" { + ch-image build-cache --reset + + ch-image pull alpine:3.17 + ch-image modify -c "echo foo" -c "echo bar" -- alpine:3.17 tmpimg + + blessed_out=$(cat << 'EOF' +* (tmpimg) RUN.S echo bar +* RUN.S echo foo +* SHELL ['/bin/sh', '-c'] +* (alpine+3.17) PULL alpine:3.17 +* (root) ROOT +EOF +) + + run ch-image build-cache --tree + echo "$output" + [[ $status -eq 0 ]] + diff -u <(echo "$blessed_out") <(echo "$output" | treeonly) + + echo "touch /home/bar" >> "${BATS_TMPDIR}/script.sh" + chmod 755 "${BATS_TMPDIR}/script.sh" + ch-image modify alpine:3.17 tmpimg "${BATS_TMPDIR}/script.sh" + + blessed_out=$(cat < '/ch/script.sh' +| * RUN.S echo bar +| * RUN.S echo foo +| * SHELL ['/bin/sh', '-c'] +|/ +* (alpine+3.17) PULL alpine:3.17 +* (root) ROOT +EOF +) + + run ch-image build-cache --tree + echo "$output" + [[ $status -eq 0 ]] + diff -u <(echo "$blessed_out") <(echo "$output" | treeonly) + + ch-image build-cache --reset + ch-image pull alpine:3.17 + printf 'echo hello\nexit\n' | ch-image modify -i alpine:3.17 tmpimg + + blessed_out=$(cat </dev/null 2>&1 \ - && sudo -v > /dev/null 2>&1; then + && sudo true > /dev/null 2>&1; then # This isn’t super reliable; it returns true if we have *any* sudo # privileges, not specifically to run the commands we want to run. ch_have_sudo=yes diff --git a/test/force-auto.py.in b/test/force-auto.py.in index 9bfab7a39..173ebbd7d 100644 --- a/test/force-auto.py.in +++ b/test/force-auto.py.in @@ -62,6 +62,7 @@ class Test(abc.ABC): base = None config = None scope = Scope.FULL + skip_reason = None prep_run = None runs = { Run.UNNEEDED_FAIL: "false", Run.UNNEEDED_WIN: "true" } @@ -83,6 +84,17 @@ class Test(abc.ABC): def build2_post_hook(self): return "" + @property + def build_from_hook(self): + return "" + + @property + def skip(self): + if (self.skip_reason is None): + return "" + else: + return "skip '%s'" % self.skip_reason + def as_grep_files(self, grep_files, image, invert=False): cmds = [] for (re, path) in grep_files: @@ -139,6 +151,7 @@ class Test(abc.ABC): build1 = f"""\ run ch-image -v build -t tmpimg -f - . << 'EOF' FROM {self.base} +{self.build_from_hook} RUN {self.prep_run} EOF echo "$output" @@ -168,6 +181,7 @@ echo "$output" # emit the test print(f""" @test "ch-image --force: {self}" {{ +{self.skip} scope {scope} {arch_excludes} @@ -177,6 +191,7 @@ scope {scope} # build 2: image we're testing run ch-image -v build {force} -t tmpimg2 -f - . << 'EOF' FROM {build2_base} +{self.build_from_hook} RUN {run} EOF echo "$output" @@ -219,6 +234,13 @@ class T_CentOS_7(RHEL7, EPEL_Mixin): base = "centos:7" prep_run = "yum install -y epel-release" + @property + def build_from_hook(self): + return f"""\ +RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo \ + && sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo \ + && sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo""" + class RHEL8(Test): config = "rhel8" @@ -234,6 +256,7 @@ class T_RHEL_UBI_8(RHEL8): class CentOS_8(RHEL8, EPEL_Mixin): prep_run = "dnf install -y epel-release" class T_CentOS_8_Stream(CentOS_8): + skip_reason = "issue #1904" # CentOS Stream pulls from quay.io per the CentOS wiki: # https://wiki.centos.org/FAQ/CentOSStream#What_artifacts_are_built.3F base = "quay.io/centos/centos:stream8" @@ -254,6 +277,7 @@ class T_Rocky_8(CentOS_8): class Fedora(RHEL8): config = "fedora" class T_Fedora_26(Fedora): + skip_reason = "issue #1904" # We would prefer to test the lowest supported --force version, 24, # but the ancient version of dnf it has doesn't fail the transaction when # a package fails so we test with 26 instead. diff --git a/test/run/ch-run_escalated.bats b/test/run/ch-run_escalated.bats index 9bac753f6..6501104b7 100644 --- a/test/run/ch-run_escalated.bats +++ b/test/run/ch-run_escalated.bats @@ -15,7 +15,7 @@ load ../common [[ -g $ch_run_tmp ]] run "$ch_run_tmp" --version echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *': please report this bug ('* ]] rm "$ch_run_tmp" } @@ -32,7 +32,7 @@ load ../common [[ -u $ch_run_tmp ]] run "$ch_run_tmp" --version echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *': please report this bug ('* ]] sudo rm "$ch_run_tmp" } @@ -71,7 +71,7 @@ load ../common fi run sudo -u root -g "$(id -gn)" "$ch_runfile" -v --version echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *'please report this bug ('* ]] } diff --git a/test/run/ch-run_join.bats b/test/run/ch-run_join.bats index b87175f25..0ff9c07cd 100644 --- a/test/run/ch-run_join.bats +++ b/test/run/ch-run_join.bats @@ -267,36 +267,36 @@ unset_vars () { # --join but no join count run ch-run --join "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ 'join: no valid peer group size found' ]] ipc_clean_p # join count no digits run ch-run --join-ct=a "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ 'join-ct: no digits found' ]] SLURM_CPUS_ON_NODE=a run ch-run --join "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ 'SLURM_CPUS_ON_NODE: no digits found' ]] ipc_clean_p # join count empty string run ch-run --join-ct='' "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ '--join-ct: no digits found' ]] SLURM_CPUS_ON_NODE=-1 run ch-run --join "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ 'join: no valid peer group size found' ]] ipc_clean_p # --join-ct digits followed by extra goo (OK from environment variable) run ch-run --join-ct=1a "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ '--join-ct: extra characters after digits' ]] ipc_clean_p @@ -306,48 +306,48 @@ unset_vars () { # join count above INT_MAX run ch-run --join-ct=2147483648 "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ $range_re ]] SLURM_CPUS_ON_NODE=2147483648 \ run ch-run --join "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ $range_re ]] ipc_clean_p # join count below INT_MIN run ch-run --join-ct=-2147483649 "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ $range_re ]] SLURM_CPUS_ON_NODE=-2147483649 \ run ch-run --join "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ $range_re ]] ipc_clean_p # join count above LONG_MAX run ch-run --join-ct=9223372036854775808 "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ $range_re ]] SLURM_CPUS_ON_NODE=9223372036854775808 \ run ch-run --join "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ $range_re ]] ipc_clean_p # join count below LONG_MIN run ch-run --join-ct=-9223372036854775809 "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ $range_re ]] SLURM_CPUS_ON_NODE=-9223372036854775809 \ run ch-run --join "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ $range_re ]] ipc_clean_p } @@ -361,11 +361,11 @@ unset_vars () { # join tag empty string run ch-run --join-tag='' "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ 'join: peer group tag cannot be empty string' ]] SLURM_STEP_ID='' run ch-run --join "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ 'join: peer group tag cannot be empty string' ]] ipc_clean_p } @@ -466,14 +466,14 @@ unset_vars () { # Can’t join namespaces of processes we don’t own. run ch-run -v --join-pid=1 "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"join: can't open /proc/1/ns/user: Permission denied"* ]] # Can’t join namespaces of processes that don’t exist. pid=2147483647 run ch-run -v --join-pid="$pid" "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"join: no PID ${pid}: /proc/${pid}/ns/user not found"* ]] } diff --git a/test/run/ch-run_misc.bats b/test/run/ch-run_misc.bats index b5f2ad991..e33941eea 100644 --- a/test/run/ch-run_misc.bats +++ b/test/run/ch-run_misc.bats @@ -86,7 +86,7 @@ EOF [[ $USER ]] # default: no change - # shellcheck disable=SC2016 + # shellcheck disable=SC2016,SC2154 run ch-run "${ch_imgdir}"/quick -- /bin/sh -c 'echo $HOME' echo "$output" [[ $status -eq 0 ]] @@ -121,7 +121,7 @@ EOF run ch-run --home "$ch_timg" -- /bin/sh -c 'echo $HOME' export HOME="$home_tmp" echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] # shellcheck disable=SC2016 [[ $output = *'--home failed: $HOME not set'* ]] @@ -132,7 +132,7 @@ EOF run ch-run --home "$ch_timg" -- /bin/sh -c 'echo $HOME' export USER=$user_tmp echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] # shellcheck disable=SC2016 [[ $output = *'$USER not set'* ]] } @@ -142,6 +142,7 @@ EOF scope quick echo "$PATH" # if /bin is in $PATH, latter passes through unchanged + # shellcheck disable=SC2154 PATH2="$ch_bin:/bin:/usr/bin" echo "$PATH2" # shellcheck disable=SC2016 @@ -171,6 +172,7 @@ EOF scope standard old_path=$PATH unset PATH + # shellcheck disable=SC2154 run "$ch_runfile" "$ch_timg" -- \ /usr/bin/python3 -c 'import os; print(os.getenv("PATH") is None)' PATH=$old_path @@ -210,7 +212,7 @@ EOF # Error if directory does not exist. run ch-run --cd /goops "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output =~ "can't cd to /goops: No such file or directory" ]] } @@ -269,6 +271,7 @@ EOF } rm-img + # shellcheck disable=SC2154 ch-convert "$ch_tardir"/chtest.* "$img" ls -l "$img" mkdir "$img"/foo @@ -318,112 +321,112 @@ EOF # empty argument to --bind run ch-run -b '' "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *'--bind: no source provided'* ]] # source not provided run ch-run -b :/mnt/9 "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *'--bind: no source provided'* ]] # destination not provided run ch-run -b "${bind1_dir}:" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *'--bind: no destination provided'* ]] # destination is / run ch-run -b "${bind1_dir}:/" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"--bind: destination can't be /"* ]] # destination is relative run ch-run -b "${bind1_dir}:foo" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"--bind: destination must be absolute"* ]] # destination climbs out of image, exists run ch-run -b "${bind1_dir}:/.." "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't bind: "*"/${USER}.ch not subdirectory of "*"/${USER}.ch/mnt"* ]] # destination climbs out of image, does not exist run ch-run -b "${bind1_dir}:/../doesnotexist/a" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't mkdir: "*"/${USER}.ch/doesnotexist not subdirectory of "*"/${USER}.ch/mnt"* ]] [[ ! -e ${ch_imgdir}/doesnotexist ]] # source does not exist run ch-run -b "/doesnotexist" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't bind: source not found: /doesnotexist"* ]] # destination does not exist and image is not writeable run ch-run -b "${bind1_dir}:/doesnotexist" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't mkdir: "*"/${USER}.ch/mnt/doesnotexist: Read-only file system"* ]] # neither source nor destination exist run ch-run -b /doesnotexist-out:/doesnotexist-in "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't bind: source not found: /doesnotexist-out"* ]] # correct bind followed by source does not exist run ch-run -b "${bind1_dir}:/mnt/0" -b /doesnotexist "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't bind: source not found: /doesnotexist"* ]] # correct bind followed by destination does not exist run ch-run -b "${bind1_dir}:/mnt/0" -b "${bind2_dir}:/doesnotexist" \ "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't mkdir: "*"/${USER}.ch/mnt/doesnotexist: Read-only file system"* ]] # destination is broken symlink run ch-run -b "${bind1_dir}:/mnt/link-b0rken-abs" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't mkdir: symlink not relative: "*"/${USER}.ch/mnt/mnt/link-b0rken-abs"* ]] # destination is absolute symlink outside image run ch-run -b "${bind1_dir}:/mnt/link-bad-abs" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't bind: "*" not subdirectory of"* ]] # destination relative symlink outside image run ch-run -b "${bind1_dir}:/mnt/link-bad-rel" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't bind: "*" not subdirectory of"* ]] # mkdir(2) under existing bind-mount, default, first level run ch-run -b "${bind1_dir}:/proc/doesnotexist" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't mkdir: "*"/${USER}.ch/mnt/proc/doesnotexist under existing bind-mount "*"/${USER}.ch/mnt/proc "* ]] # mkdir(2) under existing bind-mount, user-supplied, first level run ch-run -b "${bind1_dir}:/mnt/0" \ -b "${bind2_dir}:/mnt/0/foo" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't mkdir: "*"/${USER}.ch/mnt/mnt/0/foo under existing bind-mount "*"/${USER}.ch/mnt/mnt/0 "* ]] # mkdir(2) under existing bind-mount, default, 2nd level run ch-run -b "${bind1_dir}:/proc/sys/doesnotexist" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't mkdir: "*"/${USER}.ch/mnt/proc/sys/doesnotexist under existing bind-mount "*"/${USER}.ch/mnt/proc "* ]] } @@ -620,13 +623,13 @@ EOF # file does not exist run ch-run --set-env=doesnotexist.txt "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't open: doesnotexist.txt: No such file or directory"* ]] # /ch/environment missing run ch-run --set-env "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't open: /ch/environment: No such file or directory"* ]] # Note: I’m not sure how to test an error during reading, i.e., getline(3) @@ -636,14 +639,14 @@ EOF echo 'FOO bar' > "$f_in" run ch-run --set-env="$f_in" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't parse variable: no delimiter: ${f_in}:1"* ]] # invalid line: no name echo '=bar' > "$f_in" run ch-run --set-env="$f_in" "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *"can't parse variable: empty name: ${f_in}:1"* ]] } @@ -662,7 +665,7 @@ EOF # missing environment variable run ch-run --set-env='$PATH:foo' "$ch_timg" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *'$PATH:foo: No such file or directory'* ]] } @@ -709,7 +712,7 @@ EOF printf '\n# Empty string\n\n' run ch-run --unset-env= "$ch_timg" -- env echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *'--unset-env: GLOB must have non-zero length'* ]] } @@ -912,7 +915,7 @@ EOF # image is file but not sqfs run ch-run -vv ./fixtures/README -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output = *'magic expected: 6873 7173; actual: 596f 7520'* ]] [[ $output = *'unknown image type: '*'/fixtures/README'* ]] @@ -948,7 +951,7 @@ EOF # This should start up the container OK but fail to find the user command. run ch-run "$img" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_CMD ]] [[ $output = *"can't execve(2): /bin/true: No such file or directory"* ]] # For each required file, we want a correct error if it’s missing. @@ -959,7 +962,7 @@ EOF run ch-run "$img" -- /bin/true touch "${img}/${f}" # restore before test fails for idempotency echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] r="can't bind: destination not found: .+/${f}" echo "expected: ${r}" [[ $output =~ $r ]] @@ -972,7 +975,7 @@ EOF run ch-run "$img" -- /bin/true touch "${img}/${f}" # restore before test fails for idempotency echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_CMD ]] [[ $output = *"can't execve(2): /bin/true: No such file or directory"* ]] done @@ -985,7 +988,7 @@ EOF rmdir "${img}/${f}" # restore before test fails for idempotency touch "${img}/${f}" echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] r="can't bind .+ to /.+/${f}: Not a directory" echo "expected: ${r}" [[ $output =~ $r ]] @@ -998,7 +1001,7 @@ EOF run ch-run "$img" -- /bin/true mkdir "${img}/${d}" # restore before test fails for idempotency echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] r="can't bind: destination not found: .+/${d}" echo "expected: ${r}" [[ $output =~ $r ]] @@ -1013,7 +1016,7 @@ EOF rm "${img}/${d}" # restore before test fails for idempotency mkdir "${img}/${d}" echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] r="can't bind .+ to /.+/${d}: Not a directory" echo "expected: ${r}" [[ $output =~ $r ]] @@ -1024,7 +1027,7 @@ EOF run ch-run --private-tmp "$img" -- /bin/true mkdir "${img}/tmp" # restore before test fails for idempotency echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] r="can't mount tmpfs at /.+/tmp: No such file or directory" echo "expected: ${r}" [[ $output =~ $r ]] @@ -1034,13 +1037,13 @@ EOF run ch-run "$img" -- /bin/true mkdir "${img}/home" # restore before test fails for idempotency echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_CMD ]] [[ $output = *"can't execve(2): /bin/true: No such file or directory"* ]] # Everything should be restored and back to the original error. run ch-run "$img" -- /bin/true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_CMD ]] [[ $output = *"can't execve(2): /bin/true: No such file or directory"* ]] # At this point, there should be exactly two each of passwd and group @@ -1144,7 +1147,7 @@ EOF # subprocess failure at quiet level 2 run ch-run -qq "$ch_timg" -- doesnotexist echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_CMD ]] [[ $output = *"error: can't execve(2): doesnotexist: No such file or directory"* ]] # quiet level 3 @@ -1157,13 +1160,12 @@ EOF # subprocess failure at quiet level 3 run ch-run -qqq "$ch_timg" -- doesnotexist echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_CMD ]] [[ $output != *"error: can't execve(2): doesnotexist: No such file or directory"* ]] # failure at quiet level 3 run ch-run -qqq --test=log-fail - echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output != *'info'* ]] [[ $output != *'warning: warning'* ]] [[ $output = *'error: the program failed inexplicably'* ]] @@ -1175,6 +1177,6 @@ EOF # bad tmpfs size run ch-run --write-fake=foo "$ch_timg" -- true echo "$output" - [[ $status -eq 1 ]] + [[ $status -eq $CH_ERR_MISC ]] [[ $output == *'cannot mount tmpfs for overlay: Invalid argument'* ]] }