From 1683f45e940a25a87b989c0e121a03018661e097 Mon Sep 17 00:00:00 2001 From: ik1ne <3996272+ik1ne@users.noreply.github.com> Date: Thu, 22 Aug 2019 03:35:55 +0900 Subject: [PATCH] Update docker commands. (#940) * Add: Tests for newer version docker support. * Add: Support for newer versions of docker (Modified rules.docker_not_command). * Fix: Updated disabling memoize. * Change: removed empty list check. * Fix: _parse_commands now uses line.strip() internally and ends_with arg now doesn't end with newline. * Change: Replaced disable_memoize in favor of no_memoize fixture. * Fix: removed unused import. --- tests/rules/test_docker_not_command.py | 166 ++++++++++++++++++++++++- thefuck/rules/docker_not_command.py | 32 +++-- 2 files changed, 190 insertions(+), 8 deletions(-) diff --git a/tests/rules/test_docker_not_command.py b/tests/rules/test_docker_not_command.py index 697c0f31..d13ecb96 100644 --- a/tests/rules/test_docker_not_command.py +++ b/tests/rules/test_docker_not_command.py @@ -4,6 +4,46 @@ from thefuck.types import Command from thefuck.rules.docker_not_command import get_new_command, match +_DOCKER_SWARM_OUTPUT = ''' +Usage: docker swarm COMMAND + +Manage Swarm + +Commands: + ca Display and rotate the root CA + init Initialize a swarm + join Join a swarm as a node and/or manager + join-token Manage join tokens + leave Leave the swarm + unlock Unlock swarm + unlock-key Manage the unlock key + update Update the swarm + +Run 'docker swarm COMMAND --help' for more information on a command. +''' +_DOCKER_IMAGE_OUTPUT = ''' +Usage: docker image COMMAND + +Manage images + +Commands: + build Build an image from a Dockerfile + history Show the history of an image + import Import the contents from a tarball to create a filesystem image + inspect Display detailed information on one or more images + load Load an image from a tar archive or STDIN + ls List images + prune Remove unused images + pull Pull an image or a repository from a registry + push Push an image or a repository to a registry + rm Remove one or more images + save Save one or more images to a tar archive (streamed to STDOUT by default) + tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE + +Run 'docker image COMMAND --help' for more information on a command. +''' + + @pytest.fixture def docker_help(mocker): help = b'''Usage: docker [OPTIONS] COMMAND [arg...] @@ -104,6 +144,94 @@ Run 'docker COMMAND --help' for more information on a command. return mock +@pytest.fixture +def docker_help_new(mocker): + helptext_new = b''' +Usage: docker [OPTIONS] COMMAND + +A self-sufficient runtime for containers + +Options: + --config string Location of client config files (default "/Users/ik1ne/.docker") + -c, --context string Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var + and default context set with "docker context use") + -D, --debug Enable debug mode + -H, --host list Daemon socket(s) to connect to + -l, --log-level string Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info") + --tls Use TLS; implied by --tlsverify + --tlscacert string Trust certs signed only by this CA (default "/Users/ik1ne/.docker/ca.pem") + --tlscert string Path to TLS certificate file (default "/Users/ik1ne/.docker/cert.pem") + --tlskey string Path to TLS key file (default "/Users/ik1ne/.docker/key.pem") + --tlsverify Use TLS and verify the remote + -v, --version Print version information and quit + +Management Commands: + builder Manage builds + config Manage Docker configs + container Manage containers + context Manage contexts + image Manage images + network Manage networks + node Manage Swarm nodes + plugin Manage plugins + secret Manage Docker secrets + service Manage services + stack Manage Docker stacks + swarm Manage Swarm + system Manage Docker + trust Manage trust on Docker images + volume Manage volumes + +Commands: + attach Attach local standard input, output, and error streams to a running container + build Build an image from a Dockerfile + commit Create a new image from a container's changes + cp Copy files/folders between a container and the local filesystem + create Create a new container + diff Inspect changes to files or directories on a container's filesystem + events Get real time events from the server + exec Run a command in a running container + export Export a container's filesystem as a tar archive + history Show the history of an image + images List images + import Import the contents from a tarball to create a filesystem image + info Display system-wide information + inspect Return low-level information on Docker objects + kill Kill one or more running containers + load Load an image from a tar archive or STDIN + login Log in to a Docker registry + logout Log out from a Docker registry + logs Fetch the logs of a container + pause Pause all processes within one or more containers + port List port mappings or a specific mapping for the container + ps List containers + pull Pull an image or a repository from a registry + push Push an image or a repository to a registry + rename Rename a container + restart Restart one or more containers + rm Remove one or more containers + rmi Remove one or more images + run Run a command in a new container + save Save one or more images to a tar archive (streamed to STDOUT by default) + search Search the Docker Hub for images + start Start one or more stopped containers + stats Display a live stream of container(s) resource usage statistics + stop Stop one or more running containers + tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE + top Display the running processes of a container + unpause Unpause all processes within one or more containers + update Update configuration of one or more containers + version Show the Docker version information + wait Block until one or more containers stop, then print their exit codes + +Run 'docker COMMAND --help' for more information on a command. +''' + mock = mocker.patch('subprocess.Popen') + mock.return_value.stdout = BytesIO(b'') + mock.return_value.stderr = BytesIO(helptext_new) + return mock + + def output(cmd): return "docker: '{}' is not a docker command.\n" \ "See 'docker --help'.".format(cmd) @@ -113,6 +241,24 @@ def test_match(): assert match(Command('docker pes', output('pes'))) +# tests docker (management command) +@pytest.mark.usefixtures('no_memoize') +@pytest.mark.parametrize('script, output', [ + ('docker swarn', output('swarn')), + ('docker imge', output('imge'))]) +def test_match_management_cmd(script, output): + assert match(Command(script, output)) + + +# tests docker (management cmd) (management subcmd) +@pytest.mark.usefixtures('no_memoize') +@pytest.mark.parametrize('script, output', [ + ('docker swarm int', _DOCKER_SWARM_OUTPUT), + ('docker image la', _DOCKER_IMAGE_OUTPUT)]) +def test_match_management_subcmd(script, output): + assert match(Command(script, output)) + + @pytest.mark.parametrize('script, output', [ ('docker ps', ''), ('cat pes', output('pes'))]) @@ -120,10 +266,28 @@ def test_not_match(script, output): assert not match(Command(script, output)) -@pytest.mark.usefixtures('docker_help') +@pytest.mark.usefixtures('no_memoize', 'docker_help') @pytest.mark.parametrize('wrong, fixed', [ ('pes', ['ps', 'push', 'pause']), ('tags', ['tag', 'stats', 'images'])]) def test_get_new_command(wrong, fixed): command = Command('docker {}'.format(wrong), output(wrong)) assert get_new_command(command) == ['docker {}'.format(x) for x in fixed] + + +@pytest.mark.usefixtures('no_memoize', 'docker_help_new') +@pytest.mark.parametrize('wrong, fixed', [ + ('swarn', ['swarm', 'start', 'search']), + ('inage', ['image', 'images', 'rename'])]) +def test_get_new_management_command(wrong, fixed): + command = Command('docker {}'.format(wrong), output(wrong)) + assert get_new_command(command) == ['docker {}'.format(x) for x in fixed] + + +@pytest.mark.usefixtures('no_memoize', 'docker_help_new') +@pytest.mark.parametrize('wrong, fixed, output', [ + ('swarm int', ['swarm init', 'swarm join', 'swarm join-token'], _DOCKER_SWARM_OUTPUT), + ('image la', ['image load', 'image ls', 'image tag'], _DOCKER_IMAGE_OUTPUT)]) +def test_get_new_management_command_subcommand(wrong, fixed, output): + command = Command('docker {}'.format(wrong), output) + assert get_new_command(command) == ['docker {}'.format(x) for x in fixed] diff --git a/thefuck/rules/docker_not_command.py b/thefuck/rules/docker_not_command.py index 9aa6a220..d9eb1c24 100644 --- a/thefuck/rules/docker_not_command.py +++ b/thefuck/rules/docker_not_command.py @@ -8,16 +8,30 @@ from thefuck.specific.sudo import sudo_support @sudo_support @for_app('docker') def match(command): - return 'is not a docker command' in command.output + return 'is not a docker command' in command.output or 'Usage: docker' in command.output + + +def _parse_commands(lines, starts_with): + lines = dropwhile(lambda line: not line.startswith(starts_with), lines) + lines = islice(lines, 1, None) + lines = list(takewhile(lambda line: line.strip(), lines)) + return [line.strip().split(' ')[0] for line in lines] def get_docker_commands(): - proc = subprocess.Popen('docker', stdout=subprocess.PIPE) - lines = [line.decode('utf-8') for line in proc.stdout.readlines()] - lines = dropwhile(lambda line: not line.startswith('Commands:'), lines) - lines = islice(lines, 1, None) - lines = list(takewhile(lambda line: line != '\n', lines)) - return [line.strip().split(' ')[0] for line in lines] + proc = subprocess.Popen('docker', stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # Old version docker returns its output to stdout, while newer version returns to stderr. + lines = proc.stdout.readlines() or proc.stderr.readlines() + lines = [line.decode('utf-8') for line in lines] + + # Only newer versions of docker have management commands in the help text. + if 'Management Commands:\n' in lines: + management_commands = _parse_commands(lines, 'Management Commands:') + else: + management_commands = [] + regular_commands = _parse_commands(lines, 'Commands:') + return management_commands + regular_commands if which('docker'): @@ -26,6 +40,10 @@ if which('docker'): @sudo_support def get_new_command(command): + if 'Usage:' in command.output and len(command.script_parts) > 1: + management_subcommands = _parse_commands(command.output.split('\n'), 'Commands:') + return replace_command(command, command.script_parts[2], management_subcommands) + wrong_command = re.findall( r"docker: '(\w+)' is not a docker command.", command.output)[0] return replace_command(command, wrong_command, get_docker_commands())