├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── test ├── README.md ├── container-runners.py ├── requirements.txt └── wait-for-it.py └── wait-for-it.sh /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.pyc 2 | .pydevproject 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | 5 | script: 6 | - python test/wait-for-it.py 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Giles Hall 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wait-for-it 2 | 3 | `wait-for-it.sh` is a pure bash script that will wait on the availability of a 4 | host and TCP port. It is useful for synchronizing the spin-up of 5 | interdependent services, such as linked docker containers. Since it is a pure 6 | bash script, it does not have any external dependencies. 7 | 8 | ## Usage 9 | 10 | ```text 11 | wait-for-it.sh host:port [-s] [-t timeout] [-- command args] 12 | -h HOST | --host=HOST Host or IP under test 13 | -p PORT | --port=PORT TCP port under test 14 | Alternatively, you specify the host and port as host:port 15 | -s | --strict Only execute subcommand if the test succeeds 16 | -q | --quiet Don't output any status messages 17 | -t TIMEOUT | --timeout=TIMEOUT 18 | Timeout in seconds, zero for no timeout 19 | -- COMMAND ARGS Execute command with args after the test finishes 20 | ``` 21 | 22 | ## Examples 23 | 24 | For example, let's test to see if we can access port 80 on `www.google.com`, 25 | and if it is available, echo the message `google is up`. 26 | 27 | ```text 28 | $ ./wait-for-it.sh www.google.com:80 -- echo "google is up" 29 | wait-for-it.sh: waiting 15 seconds for www.google.com:80 30 | wait-for-it.sh: www.google.com:80 is available after 0 seconds 31 | google is up 32 | ``` 33 | 34 | You can set your own timeout with the `-t` or `--timeout=` option. Setting 35 | the timeout value to 0 will disable the timeout: 36 | 37 | ```text 38 | $ ./wait-for-it.sh -t 0 www.google.com:80 -- echo "google is up" 39 | wait-for-it.sh: waiting for www.google.com:80 without a timeout 40 | wait-for-it.sh: www.google.com:80 is available after 0 seconds 41 | google is up 42 | ``` 43 | 44 | The subcommand will be executed regardless if the service is up or not. If you 45 | wish to execute the subcommand only if the service is up, add the `--strict` 46 | argument. In this example, we will test port 81 on `www.google.com` which will 47 | fail: 48 | 49 | ```text 50 | $ ./wait-for-it.sh www.google.com:81 --timeout=1 --strict -- echo "google is up" 51 | wait-for-it.sh: waiting 1 seconds for www.google.com:81 52 | wait-for-it.sh: timeout occurred after waiting 1 seconds for www.google.com:81 53 | wait-for-it.sh: strict mode, refusing to execute subprocess 54 | ``` 55 | 56 | If you don't want to execute a subcommand, leave off the `--` argument. This 57 | way, you can test the exit condition of `wait-for-it.sh` in your own scripts, 58 | and determine how to proceed: 59 | 60 | ```text 61 | $ ./wait-for-it.sh www.google.com:80 62 | wait-for-it.sh: waiting 15 seconds for www.google.com:80 63 | wait-for-it.sh: www.google.com:80 is available after 0 seconds 64 | $ echo $? 65 | 0 66 | $ ./wait-for-it.sh www.google.com:81 67 | wait-for-it.sh: waiting 15 seconds for www.google.com:81 68 | wait-for-it.sh: timeout occurred after waiting 15 seconds for www.google.com:81 69 | $ echo $? 70 | 124 71 | ``` 72 | 73 | ## Community 74 | 75 | *Debian*: There is a [Debian package](https://tracker.debian.org/pkg/wait-for-it). 76 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vishnubob/wait-for-it", 3 | "description": "Pure bash script to test and wait on the availability of a TCP host and port", 4 | "type": "library", 5 | "license": "MIT", 6 | "bin": ["wait-for-it.sh"] 7 | } 8 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests for wait-for-it 2 | 3 | * wait-for-it.py - pytests for wait-for-it.sh 4 | * container-runners.py - Runs wait-for-it.py tests in multiple containers 5 | * requirements.txt - pip requirements for container-runners.py 6 | 7 | To run the basic tests: 8 | 9 | ``` 10 | python wait-for-it.py 11 | ``` 12 | 13 | Many of the issues encountered have been related to differences between operating system versions. The container-runners.py script provides an easy way to run the python wait-for-it.py tests against multiple system configurations: 14 | 15 | ``` 16 | pip install -r requirements.txt 17 | python container-runners.py 18 | ``` 19 | -------------------------------------------------------------------------------- /test/container-runners.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Unit tests to run wait-for-it.py unit tests in several different docker images 4 | 5 | import unittest 6 | import os 7 | import docker 8 | from parameterized import parameterized 9 | 10 | client = docker.from_env() 11 | app_path = os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..')) 12 | volumes = {app_path: {'bind': '/app', 'mode': 'ro'}} 13 | 14 | class TestContainers(unittest.TestCase): 15 | """ 16 | Test multiple container types with the test cases in wait-for-it.py 17 | """ 18 | 19 | @parameterized.expand([ 20 | "python:3.5-buster", 21 | "python:3.5-stretch", 22 | "dougg/alpine-busybox:alpine-3.11.3_busybox-1.30.1", 23 | "dougg/alpine-busybox:alpine-3.11.3_busybox-1.31.1" 24 | ]) 25 | def test_image(self, image): 26 | print(image) 27 | command="/app/test/wait-for-it.py" 28 | container = client.containers.run(image, command=command, volumes=volumes, detach=True) 29 | result = container.wait() 30 | logs = container.logs() 31 | container.remove() 32 | self.assertEqual(result["StatusCode"], 0) 33 | 34 | if __name__ == '__main__': 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | docker>=4.0.0 2 | parameterized>=0.7.0 3 | -------------------------------------------------------------------------------- /test/wait-for-it.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import unittest 4 | import shlex 5 | from subprocess import Popen, PIPE 6 | import os 7 | import sys 8 | import socket 9 | import re 10 | 11 | MISSING_ARGS_TEXT = "Error: you need to provide a host and port to test." 12 | HELP_TEXT = "Usage:" # Start of help text 13 | DIVIDE_LINE = '-'*71 # Output line of dashes 14 | 15 | 16 | class TestWaitForIt(unittest.TestCase): 17 | """ 18 | TestWaitForIt tests the wait-for-it.sh shell script. 19 | The wait-for-it.sh script is assumed to be in the parent directory to 20 | the test script. 21 | """ 22 | 23 | def execute(self, cmd): 24 | """Executes a command and returns exit code, STDOUT, STDERR""" 25 | args = shlex.split(cmd) 26 | proc = Popen(args, stdout=PIPE, stderr=PIPE) 27 | out, err = proc.communicate() 28 | exitcode = proc.returncode 29 | return exitcode, out.decode('utf-8'), err.decode('utf-8') 30 | 31 | def open_local_port(self, timeout=5): 32 | s = socket.socket() 33 | s.bind(('', 0)) 34 | s.listen(timeout) 35 | return s, s.getsockname()[1] 36 | 37 | def check_args(self, args, stdout_regex, stderr_regex, should_succeed): 38 | command = self.wait_script + " " + args 39 | exitcode, out, err = self.execute(command) 40 | 41 | # Check stderr 42 | msg = ("Failed check that STDERR:\n" + 43 | DIVIDE_LINE + "\n" + err + "\n" + DIVIDE_LINE + 44 | "\nmatches:\n" + 45 | DIVIDE_LINE + "\n" + stderr_regex + "\n" + DIVIDE_LINE) 46 | self.assertIsNotNone(re.match(stderr_regex, err, re.DOTALL), msg) 47 | 48 | # Check STDOUT 49 | msg = ("Failed check that STDOUT:\n" + 50 | DIVIDE_LINE + "\n" + out + "\n" + DIVIDE_LINE + 51 | "\nmatches:\n" + 52 | DIVIDE_LINE + "\n" + stdout_regex + "\n" + DIVIDE_LINE) 53 | self.assertIsNotNone(re.match(stdout_regex, out, re.DOTALL), msg) 54 | 55 | # Check exit code 56 | self.assertEqual(should_succeed, exitcode == 0) 57 | 58 | def setUp(self): 59 | script_path = os.path.dirname(sys.argv[0]) 60 | parent_path = os.path.abspath(os.path.join(script_path, os.pardir)) 61 | self.wait_script = os.path.join(parent_path, "wait-for-it.sh") 62 | 63 | def test_no_args(self): 64 | """ 65 | Check that no aruments returns the missing args text and the 66 | correct return code 67 | """ 68 | self.check_args( 69 | "", 70 | "^$", 71 | MISSING_ARGS_TEXT, 72 | False 73 | ) 74 | # Return code should be 1 when called with no args 75 | exitcode, out, err = self.execute(self.wait_script) 76 | self.assertEqual(exitcode, 1) 77 | 78 | def test_help(self): 79 | """ Check that help text is printed with --help argument """ 80 | self.check_args( 81 | "--help", 82 | "", 83 | HELP_TEXT, 84 | False 85 | ) 86 | 87 | def test_no_port(self): 88 | """ Check with missing port argument """ 89 | self.check_args( 90 | "--host=localhost", 91 | "", 92 | MISSING_ARGS_TEXT, 93 | False 94 | ) 95 | 96 | def test_no_host(self): 97 | """ Check with missing hostname argument """ 98 | self.check_args( 99 | "--port=80", 100 | "", 101 | MISSING_ARGS_TEXT, 102 | False 103 | ) 104 | 105 | def test_host_port(self): 106 | """ Check that --host and --port args work correctly """ 107 | soc, port = self.open_local_port() 108 | self.check_args( 109 | "--host=localhost --port={0} --timeout=1".format(port), 110 | "", 111 | "wait-for-it.sh: waiting 1 seconds for localhost:{0}".format(port), 112 | True 113 | ) 114 | soc.close() 115 | 116 | def test_combined_host_port(self): 117 | """ 118 | Tests that wait-for-it.sh returns correctly after establishing a 119 | connectionm using combined host and ports 120 | """ 121 | soc, port = self.open_local_port() 122 | self.check_args( 123 | "localhost:{0} --timeout=1".format(port), 124 | "", 125 | "wait-for-it.sh: waiting 1 seconds for localhost:{0}".format(port), 126 | True 127 | ) 128 | soc.close() 129 | 130 | 131 | def test_port_failure_with_timeout(self): 132 | """ 133 | Note exit status of 124 is exected, passed from the timeout command 134 | """ 135 | self.check_args( 136 | "localhost:8929 --timeout=1", 137 | "", 138 | ".*timeout occurred after waiting 1 seconds for localhost:8929", 139 | False 140 | ) 141 | 142 | def test_command_execution(self): 143 | """ 144 | Checks that a command executes correctly after a port test passes 145 | """ 146 | soc, port = self.open_local_port() 147 | self.check_args( 148 | "localhost:{0} -- echo \"CMD OUTPUT\"".format(port), 149 | "CMD OUTPUT", 150 | ".*wait-for-it.sh: localhost:{0} is available after 0 seconds".format(port), 151 | True 152 | ) 153 | soc.close() 154 | 155 | def test_failed_command_execution(self): 156 | """ 157 | Check command failure. The command in question outputs STDERR and 158 | an exit code of 2 159 | """ 160 | soc, port = self.open_local_port() 161 | self.check_args( 162 | "localhost:{0} -- ls not_real_file".format(port), 163 | "", 164 | ".*No such file or directory\n", 165 | False 166 | ) 167 | soc.close() 168 | 169 | def test_command_after_connection_failure(self): 170 | """ 171 | Test that a command still runs even if a connection times out 172 | and that the return code is correct for the comand being run 173 | """ 174 | self.check_args( 175 | "localhost:8929 --timeout=1 -- echo \"CMD OUTPUT\"", 176 | "CMD OUTPUT", 177 | ".*timeout occurred after waiting 1 seconds for localhost:8929", 178 | True 179 | ) 180 | 181 | if __name__ == '__main__': 182 | unittest.main() 183 | -------------------------------------------------------------------------------- /wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | WAITFORIT_cmdname=${0##*/} 5 | 6 | echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 28 | echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 29 | else 30 | echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" 31 | fi 32 | WAITFORIT_start_ts=$(date +%s) 33 | while : 34 | do 35 | if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then 36 | nc -z $WAITFORIT_HOST $WAITFORIT_PORT 37 | WAITFORIT_result=$? 38 | else 39 | (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 40 | WAITFORIT_result=$? 41 | fi 42 | if [[ $WAITFORIT_result -eq 0 ]]; then 43 | WAITFORIT_end_ts=$(date +%s) 44 | echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" 45 | break 46 | fi 47 | sleep 1 48 | done 49 | return $WAITFORIT_result 50 | } 51 | 52 | wait_for_wrapper() 53 | { 54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 55 | if [[ $WAITFORIT_QUIET -eq 1 ]]; then 56 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 57 | else 58 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 59 | fi 60 | WAITFORIT_PID=$! 61 | trap "kill -INT -$WAITFORIT_PID" INT 62 | wait $WAITFORIT_PID 63 | WAITFORIT_RESULT=$? 64 | if [[ $WAITFORIT_RESULT -ne 0 ]]; then 65 | echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 66 | fi 67 | return $WAITFORIT_RESULT 68 | } 69 | 70 | # process arguments 71 | while [[ $# -gt 0 ]] 72 | do 73 | case "$1" in 74 | *:* ) 75 | WAITFORIT_hostport=(${1//:/ }) 76 | WAITFORIT_HOST=${WAITFORIT_hostport[0]} 77 | WAITFORIT_PORT=${WAITFORIT_hostport[1]} 78 | shift 1 79 | ;; 80 | --child) 81 | WAITFORIT_CHILD=1 82 | shift 1 83 | ;; 84 | -q | --quiet) 85 | WAITFORIT_QUIET=1 86 | shift 1 87 | ;; 88 | -s | --strict) 89 | WAITFORIT_STRICT=1 90 | shift 1 91 | ;; 92 | -h) 93 | WAITFORIT_HOST="$2" 94 | if [[ $WAITFORIT_HOST == "" ]]; then break; fi 95 | shift 2 96 | ;; 97 | --host=*) 98 | WAITFORIT_HOST="${1#*=}" 99 | shift 1 100 | ;; 101 | -p) 102 | WAITFORIT_PORT="$2" 103 | if [[ $WAITFORIT_PORT == "" ]]; then break; fi 104 | shift 2 105 | ;; 106 | --port=*) 107 | WAITFORIT_PORT="${1#*=}" 108 | shift 1 109 | ;; 110 | -t) 111 | WAITFORIT_TIMEOUT="$2" 112 | if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi 113 | shift 2 114 | ;; 115 | --timeout=*) 116 | WAITFORIT_TIMEOUT="${1#*=}" 117 | shift 1 118 | ;; 119 | --) 120 | shift 121 | WAITFORIT_CLI=("$@") 122 | break 123 | ;; 124 | --help) 125 | usage 126 | ;; 127 | *) 128 | echoerr "Unknown argument: $1" 129 | usage 130 | ;; 131 | esac 132 | done 133 | 134 | if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then 135 | echoerr "Error: you need to provide a host and port to test." 136 | usage 137 | fi 138 | 139 | WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} 140 | WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} 141 | WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} 142 | WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} 143 | 144 | # Check to see if timeout is from busybox? 145 | WAITFORIT_TIMEOUT_PATH=$(type -p timeout) 146 | WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) 147 | 148 | WAITFORIT_BUSYTIMEFLAG="" 149 | if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then 150 | WAITFORIT_ISBUSY=1 151 | # Check if busybox timeout uses -t flag 152 | # (recent Alpine versions don't support -t anymore) 153 | if timeout &>/dev/stdout | grep -q -e '-t '; then 154 | WAITFORIT_BUSYTIMEFLAG="-t" 155 | fi 156 | else 157 | WAITFORIT_ISBUSY=0 158 | fi 159 | 160 | if [[ $WAITFORIT_CHILD -gt 0 ]]; then 161 | wait_for 162 | WAITFORIT_RESULT=$? 163 | exit $WAITFORIT_RESULT 164 | else 165 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 166 | wait_for_wrapper 167 | WAITFORIT_RESULT=$? 168 | else 169 | wait_for 170 | WAITFORIT_RESULT=$? 171 | fi 172 | fi 173 | 174 | if [[ $WAITFORIT_CLI != "" ]]; then 175 | if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then 176 | echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" 177 | exit $WAITFORIT_RESULT 178 | fi 179 | exec "${WAITFORIT_CLI[@]}" 180 | else 181 | exit $WAITFORIT_RESULT 182 | fi 183 | --------------------------------------------------------------------------------