-
Notifications
You must be signed in to change notification settings - Fork 68
/
docker-rollout
executable file
Β·204 lines (171 loc) Β· 5.78 KB
/
docker-rollout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#!/bin/sh
set -e
# Defaults
HEALTHCHECK_TIMEOUT=60
NO_HEALTHCHECK_TIMEOUT=10
WAIT_AFTER_HEALTHY_DELAY=0
# Print metadata for Docker CLI plugin
if [ "$1" = "docker-cli-plugin-metadata" ]; then
cat <<EOF
{
"SchemaVersion": "0.1.0",
"Vendor": "Karol Musur",
"Version": "v0.9",
"ShortDescription": "Rollout new Compose service version"
}
EOF
exit
fi
# Save docker arguments, i.e. arguments before "rollout"
while [ $# -gt 0 ]; do
if [ "$1" = "rollout" ]; then
shift
break
fi
DOCKER_ARGS="$DOCKER_ARGS $1"
shift
done
# Check if compose v2 is available
if docker compose >/dev/null 2>&1; then
# shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments
COMPOSE_COMMAND="docker $DOCKER_ARGS compose"
elif docker-compose >/dev/null 2>&1; then
COMPOSE_COMMAND="docker-compose"
else
echo "docker compose or docker-compose is required"
exit 1
fi
usage() {
cat <<EOF
Usage: docker rollout [OPTIONS] SERVICE
Rollout new Compose service version.
Options:
-h, --help Print usage
-f, --file FILE Compose configuration files
-t, --timeout N Healthcheck timeout (default: $HEALTHCHECK_TIMEOUT seconds)
-w, --wait N When no healthcheck is defined, wait for N seconds
before stopping old container (default: $NO_HEALTHCHECK_TIMEOUT seconds)
--wait-after-healthy N When healthcheck is defined and succeeds, wait for additional N seconds
before stopping the old container (default: 0 seconds)
--env-file FILE Specify an alternate environment file
EOF
}
exit_with_usage() {
usage
exit 1
}
healthcheck() {
# shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments
docker $DOCKER_ARGS inspect --format='{{json .State.Health.Status}}' "$1" | grep -v "unhealthy" | grep -q "healthy"
}
scale() {
# shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files
$COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --scale "$1=$2" --no-recreate "$1"
}
main() {
# shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files
if [ -z "$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE")" ]; then
echo "==> Service '$SERVICE' is not running. Starting the service."
$COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --no-recreate "$SERVICE"
exit 0
fi
# shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files
OLD_CONTAINER_IDS_STRING=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE" | tr '\n' '|' | sed 's/|$//')
OLD_CONTAINER_IDS=$(echo "$OLD_CONTAINER_IDS_STRING" | tr '|' ' ')
SCALE=$(echo "$OLD_CONTAINER_IDS" | wc -w | tr -d ' ')
SCALE_TIMES_TWO=$((SCALE * 2))
echo "==> Scaling '$SERVICE' to '$SCALE_TIMES_TWO' instances"
scale "$SERVICE" $SCALE_TIMES_TWO
# Create a variable that contains the IDs of the new containers, but not the old ones
# shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files
NEW_CONTAINER_IDS=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE" | grep -Ev "$OLD_CONTAINER_IDS_STRING" | tr '\n' ' ')
# Check if first container has healthcheck
# shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments
if docker $DOCKER_ARGS inspect --format='{{json .State.Health}}' "$(echo $OLD_CONTAINER_IDS | cut -d\ -f 1)" | grep -q "Status"; then
echo "==> Waiting for new containers to be healthy (timeout: $HEALTHCHECK_TIMEOUT seconds)"
for _ in $(seq 1 "$HEALTHCHECK_TIMEOUT"); do
SUCCESS=0
for NEW_CONTAINER_ID in $NEW_CONTAINER_IDS; do
if healthcheck "$NEW_CONTAINER_ID"; then
SUCCESS=$((SUCCESS + 1))
fi
done
if [ "$SUCCESS" = "$SCALE" ]; then
break
fi
sleep 1
done
SUCCESS=0
for NEW_CONTAINER_ID in $NEW_CONTAINER_IDS; do
if healthcheck "$NEW_CONTAINER_ID"; then
SUCCESS=$((SUCCESS + 1))
fi
done
if [ "$SUCCESS" != "$SCALE" ]; then
echo "==> New containers are not healthy. Rolling back." >&2
docker $DOCKER_ARGS stop $NEW_CONTAINER_IDS
docker $DOCKER_ARGS rm $NEW_CONTAINER_IDS
exit 1
fi
if [ "$WAIT_AFTER_HEALTHY_DELAY" != "0" ]; then
echo "==> Waiting for healthy containers to settle down ($WAIT_AFTER_HEALTHY_DELAY seconds)"
sleep $WAIT_AFTER_HEALTHY_DELAY
fi
else
echo "==> Waiting for new containers to be ready ($NO_HEALTHCHECK_TIMEOUT seconds)"
sleep "$NO_HEALTHCHECK_TIMEOUT"
fi
echo "==> Stopping and removing old containers"
# shellcheck disable=SC2086 # DOCKER_ARGS and OLD_CONTAINER_IDS must be unquoted to allow multiple arguments
docker $DOCKER_ARGS stop $OLD_CONTAINER_IDS
# shellcheck disable=SC2086 # DOCKER_ARGS and OLD_CONTAINER_IDS must be unquoted to allow multiple arguments
docker $DOCKER_ARGS rm $OLD_CONTAINER_IDS
}
while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
usage
exit 0
;;
-f | --file)
COMPOSE_FILES="$COMPOSE_FILES -f $2"
shift 2
;;
--env-file)
ENV_FILES="$ENV_FILES --env-file $2"
shift 2
;;
-t | --timeout)
HEALTHCHECK_TIMEOUT="$2"
shift 2
;;
-w | --wait)
NO_HEALTHCHECK_TIMEOUT="$2"
shift 2
;;
--wait-after-healthy)
WAIT_AFTER_HEALTHY_DELAY="$2"
shift 2
;;
-*)
echo "Unknown option: $1"
exit_with_usage
;;
*)
if [ -n "$SERVICE" ]; then
echo "SERVICE is already set to '$SERVICE'"
if [ "$SERVICE" != "$1" ]; then
exit_with_usage
fi
fi
SERVICE="$1"
shift
;;
esac
done
# Require SERVICE argument
if [ -z "$SERVICE" ]; then
echo "SERVICE is missing"
exit_with_usage
fi
main