diff --git a/Atomic/help.py b/Atomic/help.py new file mode 100644 index 0000000..23e3c42 --- /dev/null +++ b/Atomic/help.py @@ -0,0 +1,95 @@ +from . import Atomic +import subprocess +from pydoc import pager +import os +from . import mount +import sys +from . import util + + +class AtomicHelp(Atomic): + + def __init__(self): + super(AtomicHelp, self).__init__() + self.mount_location = '/run/atomic' + self.help_file_name = 'help.1' + self.docker_object = None + self.is_container = True + self.use_pager = True + self.alt_help_cmd = None + + def help(self): + """ + Displays help text for a container. + :return: None + """ + self.docker_object = self.args.image + docker_id = self.get_input_id(self.docker_object) + self.inspect = self._inspect_container(docker_id) + if self.inspect is None: # docker_id is an image + self.inspect = self._inspect_image(docker_id) + self.is_container = False + else: + # The docker object is a container, need to set + # its image + self.image = self.inspect['Image'] + + # Check if an alternate help command is provided + labels = self._get_labels() + self.alt_help_cmd = None if len(labels) == 0 else labels.get('HELP') + + if self.alt_help_cmd is not None: + self.display_alt_help() + else: + self.display_man_help(docker_id) + + def display_man_help(self, docker_id): + """ + Display the help for a container or image using the default + method of displaying a man formatted page + :param docker_id: docker object to get help for + :return: None + """ + if not os.path.exists(self.mount_location): + os.mkdir(self.mount_location) + # Set the pager to less -R + enc = sys.getdefaultencoding() + if sys.stdout.isatty(): + os.environ['PAGER'] = '/usr/bin/less -R' + else: + # There is no tty + self.use_pager = False + dm = mount.DockerMount(self.mount_location, mnt_mkdir=True) + mnt_path = dm.mount(docker_id) + try: + help_file = open(os.path.join(mnt_path, self.help_file_name)) + except IOError: + pass + try: + help_file = open(os.path.join(mnt_path, 'rootfs', self.help_file_name)) + except IOError: + dm.unmount(path=mnt_path) + raise ValueError("Unable to find help file for {}".format(self.docker_object)) + + cmd2 = ['groff', '-man', '-Tascii'] + c2 = subprocess.Popen(cmd2, stdin=help_file, stdout=subprocess.PIPE) + result = c2.communicate()[0].decode(enc) + help_file.close() + if not self.use_pager: + util.writeOut("\n{}\n".format(result)) + else: + # Call the pager + pager(result) + + # Clean up + dm.unmount(path=mnt_path) + + def display_alt_help(self): + """ + Displays help when the HELP LABEL override is being used. + :return: None + """ + cmd = self.gen_cmd(self.alt_help_cmd.split(" ")) + self.display(cmd) + subprocess.check_call(cmd, env=self.cmd_env, shell=True) + diff --git a/Atomic/mount.py b/Atomic/mount.py index 29791d3..185a294 100644 --- a/Atomic/mount.py +++ b/Atomic/mount.py @@ -208,12 +208,18 @@ class DockerMount(Mount): except docker.errors.APIError as ex: raise MountError('Error creating temporary container:\n%s' % str(ex)) - def _clone(self, cid): + def _clone(self, cid, image_only=False): """ - Create a temporary image snapshot from a given cid. + Create a temporary image snapshot from a given cid and then + create temporary container from the temporary image. Temporary image snapshots are marked with a sentinel label so that they can be cleaned on unmount. + + image_only: Create the image from the container only + + Return: the id of the temporary container unless image_only=True + in which case it returns the image cloned image id. """ try: iid = self.client.commit( @@ -227,7 +233,10 @@ class DockerMount(Mount): except docker.errors.APIError as ex: raise MountError(str(ex)) self.tmp_image = iid - return self._create_temp_container(iid) + if image_only: + return iid + else: + return self._create_temp_container(iid) def _is_container_running(self, cid): cinfo = self.client.inspect_container(cid) @@ -469,14 +478,17 @@ class DockerMount(Mount): if self.tmp_image is not None: self.client.remove_image(self.tmp_image, noprune=True) - def unmount(self): + def unmount(self, path=None): """ Unmounts and cleans-up after a previous mount(). """ driver = self.client.info()['Driver'] driver_unmount_fn = getattr(self, "_unmount_" + driver, self._unsupported_backend) - driver_unmount_fn() + if path is not None: + driver_unmount_fn(path=path) + else: + driver_unmount_fn() def _get_all_cids(self): ''' diff --git a/atomic b/atomic index 8bda4af..743a184 100755 --- a/atomic +++ b/atomic @@ -32,6 +32,7 @@ import Atomic from Atomic.diff import Diff from Atomic.top import Top from Atomic.verify import Verify +from Atomic.help import AtomicHelp PROGNAME = "atomic" gettext.bindtextdomain(PROGNAME, "/usr/share/locale") @@ -129,6 +130,13 @@ if __name__ == '__main__': diffp.add_argument("-v", "--verbose", default=False, action='store_true', help=_("Show verbose output, listing all RPMs")) + #atomic help + helpp = subparser.add_parser( + "help", help=_("Display help associated with the image"), + epilog="atomic help 'image'") + helpp.set_defaults(_class=AtomicHelp, func='help') + helpp.add_argument("image", help=_("Image ID or name")) + if os.path.exists("/usr/bin/rpm-ostree"): # atomic host hostp = subparser.add_parser("host", help=_("execute Atomic host " diff --git a/bash/atomic b/bash/atomic index 6b612c1..1a6bdc9 100644 --- a/bash/atomic +++ b/bash/atomic @@ -525,6 +525,27 @@ _atomic_host_host() { esac } +_atomic_help() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "$all_options" -- "$cur" ) ) + ;; + *) + + local counter=$( __atomic_pos_first_nonflag $( __atomic_to_alternatives "$options_with_args" ) ) + + if [ $cword -eq $counter ]; then + __atomic_containers_and_images + return 0 + fi + + COMPREPLY=( $( compgen -d "$cur" ) ) + return 0 + ;; + esac + return 0 +} + _atomic_host() { local commands=( deploy diff --git a/docs/atomic-help.1.md b/docs/atomic-help.1.md new file mode 100644 index 0000000..f0e369c --- /dev/null +++ b/docs/atomic-help.1.md @@ -0,0 +1,32 @@ +% ATOMIC(1) Atomic Man Pages +% Brent Baude +% January 2016 +# NAME +atomic-help - Display help associated with a container or image +# SYNOPSIS +**atomic help** +[**-h**|**--help**] +IMAGE|CONTAINER + +# DESCRIPTION + +**Atomic help** displays a help file associated with a container or image. + +If a container or image has a help file (in man format) embedded in itself, atomic help will display +the help file in a pager similar to man. The default location for a help file is /image_help.1 but +the location of the help can be overridden with the HELP LABEL. If you choose to override the default +location, ensure the path provided is a fully-qualified path that includes the help file itself. + +The help file can be written using the middleman markup and the converted using the go-md2man utility +as follows: +``` +go-md2man -in image_help.1.md -out image_help.1 +``` +You can also use any of the many options to create the help file including using native man tagging. + +# OPTIONS +**-h** **--help** + Print usage statement + +# HISTORY +January 2016, Originally written by Brent Baude (bbaude at redhat dot com) diff --git a/docs/atomic.1.md b/docs/atomic.1.md index 4849678..3641203 100644 --- a/docs/atomic.1.md +++ b/docs/atomic.1.md @@ -20,6 +20,9 @@ Atomic Management Tool **atomic-diff(1)** show the differences between two images|containers' RPMs +**atomic-help(1)** +show help associated with a container or image + **atomic-host(1)** execute Atomic commands diff --git a/test.sh b/test.sh index 954b78b..c9d5cf7 100755 --- a/test.sh +++ b/test.sh @@ -60,7 +60,6 @@ make_docker_images () { chksum=$(_checksum ${df}) IFS=$'.' read -a split <<< "${BASE_NAME}" iname="atomic-test-${split[1]}" - # If there is a matching Dockerfile.X.d, then include its contents # in the checksum data. chksum="${chksum}$(_checksum ${df}.d)" @@ -82,6 +81,16 @@ make_docker_images () { cp ${df} ${df_cp} printf "\nLABEL \"Checksum\"=\"${chksum}" >> ${df_cp} + # Copy help.1 into atomic-test-1 + if [[ ${iname} = "atomic-test-1" ]]; then + cp ./tests/test-images/help.1 ${WORK_DIR} + fi + + # Copy help.sh into atomic-test-3 + if [[ ${iname} = "atomic-test-3" ]]; then + cp ./tests/test-images/help.sh ${WORK_DIR} + fi + # Remove the old image... Though there may not be one. set +e ${DOCKER} rmi ${iname} &>> ${LOG} diff --git a/tests/integration/test_help.sh b/tests/integration/test_help.sh new file mode 100755 index 0000000..4ea5a30 --- /dev/null +++ b/tests/integration/test_help.sh @@ -0,0 +1,32 @@ +#!/bin/bash -x +set -euo pipefail +IFS=$'\n\t' + +# Test scripts run with PWD=tests/.. + +# The test harness exports some variables into the environment during +# testing: PYTHONPATH (python module import path +# WORK_DIR (a directory that is safe to modify) +# DOCKER (the docker executable location) +# ATOMIC (an invocation of 'atomic' which measures code coverage) +# SECRET (a generated sha256 hash inserted into test containers) + +# In addition, the test harness creates some images for use in testing. +# See tests/test-images/ + +OUTPUT=$(/bin/true) + +# Test standard help in man format +${ATOMIC} help --no_pager atomic-test-1 1>/dev/null + +# Test override label +${ATOMIC} help atomic-test-3 1>/dev/null + +rc=0 +${ATOMIC} help centos:latest 1>/dev/null || rc=$? +if [[ ${rc} != 1 ]]; then + # Test failed + echo "This test should result in a return code of 1" + exit 1 +fi + diff --git a/tests/test-images/Dockerfile.1 b/tests/test-images/Dockerfile.1 index fc954e2..942297f 100644 --- a/tests/test-images/Dockerfile.1 +++ b/tests/test-images/Dockerfile.1 @@ -7,3 +7,5 @@ LABEL "Name"="atomic-test-1" LABEL RUN "/usr/bin/docker run -t --user \${SUDO_UID}:\${SUDO_GID} \${OPT1} -v /var/log/\${NAME}:/var/log -v /var/lib/\${NAME}:/var/lib \$OPT2 --name \${NAME} \${IMAGE} \$OPT3 echo I am the run label." LABEL INSTALL "/usr/bin/docker \${OPT1} run -v /etc/\${NAME}:/etc -v /var/log/\${NAME}:/var/log -v /var/lib/\${NAME}:/var/lib \$OPT2 --name \${NAME} \${IMAGE} \$OPT3 echo I am the install label." + +COPY help.1 / diff --git a/tests/test-images/Dockerfile.3 b/tests/test-images/Dockerfile.3 new file mode 100644 index 0000000..0ca82a9 --- /dev/null +++ b/tests/test-images/Dockerfile.3 @@ -0,0 +1,12 @@ +FROM centos +MAINTAINER "Sally O'Malley +ENV container docker + +LABEL "Name"="atomic-test-3" + +LABEL RUN "/usr/bin/docker run -t --user \${SUDO_UID}:\${SUDO_GID} \${OPT1} -v /var/log/\${NAME}:/var/log -v /var/lib/\${NAME}:/var/lib \$OPT2 --name \${NAME} \${IMAGE} \$OPT3 echo I am the run label." + +LABEL INSTALL "/usr/bin/docker \${OPT1} run -v /etc/\${NAME}:/etc -v /var/log/\${NAME}:/var/log -v /var/lib/\${NAME}:/var/lib \$OPT2 --name \${NAME} \${IMAGE} \$OPT3 echo I am the install label." + +LABEL HELP "docker run --rm IMAGE /usr/bin/bash /help.sh" +COPY help.sh / diff --git a/tests/test-images/help.1 b/tests/test-images/help.1 new file mode 100644 index 0000000..9c705e7 --- /dev/null +++ b/tests/test-images/help.1 @@ -0,0 +1,18 @@ +.TH "ATOMIC" "1" " Atomic Man Pages" "Brent Baude" "December 2015" "" + + +.SH DESCRIPTION +.PP +Simple \ftest\fP case + + +.SH USAGE +.PP +test + +.PP +.RS + +.SH HISTORY +.PP +December 2015, Originally written by Brent Baude (bbaude at redhat dot com) diff --git a/tests/test-images/help.sh b/tests/test-images/help.sh new file mode 100644 index 0000000..30ffc5a --- /dev/null +++ b/tests/test-images/help.sh @@ -0,0 +1 @@ +echo "Testing help"