-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathprune.sh
executable file
·417 lines (367 loc) · 15.3 KB
/
prune.sh
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
#!/usr/bin/env sh
# Shell sanity
set -eu
# Dynamic vars
cmdname=$(basename "${0}")
appname=${cmdname%.*}
# Root directory of the script
ROOT_DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )
# Our library for scripts and dependencies. Overly complex, but built with
# installation flexibility in mind.
LIB_DIR=
for _lib in lib libexec "share/$appname"; do
[ -z "${LIB_DIR}" ] && [ -d "${ROOT_DIR}/$_lib" ] && LIB_DIR="${ROOT_DIR}/$_lib"
[ -z "${LIB_DIR}" ] && [ -d "${ROOT_DIR}/../$_lib" ] && LIB_DIR="${ROOT_DIR}/../$_lib"
done
[ -z "$LIB_DIR" ] && echo "Cannot find library directory!" >&2 && exit 1
# Top directory for yu.sh
YUSH_DIR="$LIB_DIR/yu.sh"
! [ -d "$YUSH_DIR" ] && echo "Cannot find yu.sh directory!" >&2 && exit 1
# shellcheck disable=SC1090
. "$YUSH_DIR/log.sh"
# shellcheck disable=SC1090
. "$YUSH_DIR/date.sh"
# All (good?) defaults
DRYRUN=0
BUSYBOX=${BUSYBOX:-busybox:1.31.0-musl}
MAXFILES=${MAXFILES:-0}
NAMES=${NAMES:-}
EXCLUDE=${EXCLUDE:-}
RESOURCES=${RESOURCES:-"images volumes containers"}
AGE=${AGE:-"6m"}
ANCIENT=${ANCIENT:-}
NAMESGEN=https://raw.githubusercontent.com/moby/moby/master/pkg/namesgenerator/names-generator.go
TIMEOUT=${TIMEOUT:-"30s"}
INTERMEDIATE=0
# Print usage on stderr and exit
usage() {
[ -n "$1" ] && echo "$1" >&2
exitcode="${2:-1}"
cat << USAGE >&2
Description:
$cmdname performs some conservative Docker system pruning
Usage:
$cmdname [-option arg --long-option(=)arg] [--] command
where all dash-led options are as follows (long options can be followed by
an equal sign):
--dry(-)run Do not remove, print out only.
-r | --resources Space separated list of Docker resources to consider for
removal, defaults to "images volumes containers".
-l | --limit Maximum number of files in a dangling volume to consider
it "empty" and consider it for removal (default: 0)
-n | --names Regular expression matching names of dangling volumes and
exited containers to consider for removal (default: empty,
e.g. all)
-x | --exclude Regular expression to exclude from names selected above,
this eases selecting away important containers/volumes.
-a | --age Age of dangling image to consider it for removal (default:
6m). The age can be expressed in yush_human_period-readable
format, e.g. 6m (== 6 months), 3 days, etc.
--ancient Age of ancient container. Matching or unnamed containers
at least this old will be forced removed. Default is empty,
no removal at all!
-t | --timeout Timeout to wait for created containers to change status,
they will be consideyush_red as stale and removed if status
has not changed. This can be expressed in human-readable
format. Default is 30 seconds, e.g. 30s.
--busybox Docker busybox image tag to be used for volume content
collection.
--namesgen URL to go implementation for Docker container names
generator, defaults to latest at Moby GitHub project.
-v | --verbose Specify verbosity level: from error, down to trace
-h | --help Print this helt and exit
Everything that follows these options, preferably separated from the options
using -- is any command that will be executed, if present, at the end of the
script.
USAGE
exit "$exitcode"
}
while [ $# -gt 0 ]; do
case "$1" in
-l | --limit)
MAXFILES="$2"; shift 2;;
--limit=*)
MAXFILES="${1#*=}"; shift 1;;
-n | --names)
NAMES="$2"; shift 2;;
--names=*)
NAMES="${1#*=}"; shift 1;;
-x | --exclude)
EXCLUDE="$2"; shift 2;;
--exclude=*)
EXCLUDE="${1#*=}"; shift 1;;
-r | --resources)
RESOURCES="$2"; shift 2;;
--resources=*)
RESOURCES="${1#*=}"; shift 1;;
-a | --age)
AGE="$2"; shift 2;;
--age=*)
AGE="${1#*=}"; shift 1;;
--ancient)
ANCIENT="$2"; shift 2;;
--ancient=*)
ANCIENT="${1#*=}"; shift 1;;
-t | --timeout)
TIMEOUT="$2"; shift 2;;
--timeout=*)
TIMEOUT="${1#*=}"; shift 1;;
--busybox)
BUSYBOX="$2"; shift 2;;
--busybox=*)
BUSYBOX="${1#*=}"; shift 1;;
--dry-run | --dryrun)
DRYRUN=1; shift 1;;
--intermediate)
INTERMEDIATE=1; shift 1;;
--names-gen | --names-generator | --namesgen)
NAMESGEN="$2"; shift 2;;
--names-gen=* | --names-generator=* | --namesgen=*)
NAMESGEN="${1#*=}"; shift 1;;
-v | --verbose)
# shellcheck disable=SC2034
YUSH_LOG_LEVEL=$2; shift 2;;
--verbose=*)
# shellcheck disable=SC2034
YUSH_LOG_LEVEL="${1#*=}"; shift 1;;
--non-interactive | --no-colour | --no-color)
# shellcheck disable=SC2034
YUSH_LOG_COLOUR=0; shift 1;;
-h | --help)
usage "" 0;;
--)
shift; break;;
-*)
usage "Unknown option: $1 !";;
*)
break;;
esac
done
abort() {
yush_error "$1"
exit 1
}
# Given a name passed as argument, return 0 if it shouldn't be considered for
# removal, 1 otherwise. This implements the logic behind the --names and
# --exclude command-line options. The second argument should be the type of the
# resource to consider for removal and is only used for logging.
consider() {
CONSIDER=0
if [ -n "$NAMES" ]; then
if printf %s\\n "$1"|grep -Eqo "$NAMES"; then
if [ -z "$EXCLUDE" ]; then
yush_info "Considering $2 $1 for removal, matching $NAMES"
CONSIDER=1
elif [ -n "$EXCLUDE" ] && printf %s\\n "$1"|grep -Eqov "$EXCLUDE"; then
yush_info "Considering $2 $1 for removal, matching $NAMES but not $EXCLUDE"
CONSIDER=1
else
yush_info "Skipping removal of $2 $(yush_green "$1"), matching $NAMES but also matching $EXCLUDE"
fi
else
yush_info "Skipping removal of $2 $(yush_green "$1"), does not match $NAMES"
fi
else
yush_info "Considering $2 $1 for removal"
CONSIDER=1
fi
echo "$CONSIDER"
}
rm_container() {
CONSIDER=0
# Try matching the name of the container against the latest list of names
# used by Docker to generate good random names.
if printf %s\\n "$1" | grep -Eqo '\w+_\w+'; then
if [ -n "$NAMES_DICTIONARY" ]; then
left=$(printf %s\\n "$1" | sed -E 's/(\w+)_(\w+)/\1/')
right=$(printf %s\\n "$1" | sed -E 's/(\w+)_(\w+)/\2/')
if printf %s\\n "$NAMES_DICTIONARY" | grep -qo "$left" && printf %s\\n "$NAMES_DICTIONARY" | grep -qo "$right"; then
yush_info "Container $1 has an automatically generated name considering it for removal"
CONSIDER=1
fi
else
yush_warn "Container $1 could be a generated one, but no names dictionary to detect"
fi
fi
if [ "$CONSIDER" = "0" ]; then
CONSIDER=$(consider "$1" container)
fi
if [ "$CONSIDER" = "1" ]; then
if [ "$DRYRUN" = "1" ]; then
yush_info "Would remove container $(yush_yellow "$1")"
else
# Try removing, let Docker decide upon the final result
yush_notice "Removing exited container $(yush_red "$1")"
docker container rm --force --volumes "$1" || true
fi
else
yush_debug "Keeping container $(yush_green "$1")"
fi
}
rm_image() {
now=$(date -u +'%s')
# Collect image information for improved logging
tags=$(docker image inspect --format '{{.RepoTags}}' "$1"|sed -e 's/^\[//' -e 's/\]$//')
digests=$(docker image inspect --format '{{.RepoDigests}}' "$1"|sed -e 's/^\[//' -e 's/\]$//')
# Compute time from image creation in seconds, old images will be considered
# for removal.
CONSIDER=0
creation=$(docker image inspect --format '{{.Created}}' "$1")
howold=$(( now - $(yush_iso8601 "$creation") ))
if [ -z "$tags" ] && [ -z "$digests" ]; then
CONSIDER=1
elif [ -n "$AGE" ] && [ "$howold" -ge "$AGE" ]; then
CONSIDER=1
fi
if [ "$CONSIDER" = "1" ]; then
if [ "$DRYRUN" = "1" ]; then
yush_info "Would remove $2 image $(yush_yellow "$1") (from $(printf %s\\n "$digests" | sed -E -e 's/@sha256:[0-9a-f]{64}//g')), $(yush_human_period "$howold")old"
else
# Removing an image might fail if it is in use, this is normal.
yush_notice "Removing $2 image $(yush_red "$1") (from $(printf %s\\n "$digests" | sed -E -e 's/@sha256:[0-9a-f]{64}//g')), $(yush_human_period "$howold")old"
# Try removing, let Docker decide upon the final result
docker image rm --force "$1" || true
fi
else
yush_debug "Keeping $2 image $(yush_green "$1"), $(yush_human_period "$howold")old"
fi
}
to_seconds() {
if [ -n "$1" ]; then
if printf %s\\n "$1"|grep -Eq '[0-9]+[[:space:]]*[A-Za-z]+'; then
NEWAGE=$(yush_howlong "$1")
if [ -n "$NEWAGE" ]; then
yush_debug "Converted human-readable age $1 to $NEWAGE seconds"
printf %d\\n "$NEWAGE"
else
abort "Could not convert human-readable $1 to a period!"
fi
fi
fi
}
# Convert human-readable periods
AGE=$(to_seconds "$AGE")
ANCIENT=$(to_seconds "$ANCIENT")
NAMES_DICTIONARY=
if [ -n "$NAMESGEN" ]; then
yush_debug "Reading random container names database from $NAMESGEN"
if [ -x "$(command -v curl)" ]; then
NAMES_DICTIONARY=$(curl -qsSL -o- "$NAMESGEN"|grep -E '^\s*\"(\w+)\",'|sed -Ee 's/^\s*\"(\w+)\",/\1/g')
elif [ -x "$(command -v wget)" ]; then
NAMES_DICTIONARY=$(wget -q -O- "$NAMESGEN"|grep -E '^\s*\"(\w+)\",'|sed -Ee 's/^\s*\"(\w+)\",/\1/g')
else
yush_warn "Cannot load container name dictionary, neither curl, nor wget available"
fi
fi
# Start by cleaning up containers so we can free as many (dependent) resources
# as possible.
if printf %s\\n "$RESOURCES" | grep -qo "container"; then
yush_notice "Cleaning up exited, dead and ancient containers..."
for cnr in $(docker container ls -a --filter status=exited --filter status=dead --format '{{.Names}}'); do
rm_container "$cnr"
done
created=$(docker container ls -a --filter status=created --format '{{.Names}}')
if [ -n "$created" ]; then
# Convert yush_human_period-readable timeout, if necessary.
if printf %s\\n "$TIMEOUT"|grep -Eq '[0-9]+[[:space:]]*[A-Za-z]+'; then
NEWTMOUT=$(yush_howlong "$TIMEOUT")
yush_debug "Converted human-readable timeout $TIMEOUT to $NEWTMOUT seconds"
TIMEOUT=$NEWTMOUT
fi
# Wait for timeout seconds and see which containers still are in the
# created state. Try remove logic on the ones that remained in the
# created state for those timeout seconds.
yush_notice "Cleaning up stale created containers (waiting for $TIMEOUT sec(s))..."
sleep "$TIMEOUT"
for cnr in $(docker container ls -a --filter status=created --format '{{.Names}}'); do
if printf %s\\n "$created" | grep -qo "$cnr"; then
rm_container "$cnr"
else
yush_debug "Skipping container $(yush_green "$cnr"), changed state within the past $TIMEOUT sec(s)"
fi
done
fi
if [ -n "$ANCIENT" ]; then
now=$(date -u +'%s')
for cnr in $(docker container ls --filter status=running --format '{{.Names}}'); do
started=$(docker container inspect --format '{{.State.StartedAt}}' "$cnr")
started_secs=$(yush_iso8601 "$started")
howold=$(( now - started_secs ))
if [ "$howold" -gt "$ANCIENT" ]; then
yush_info "Container $cnr is $(yush_human_period "$howold")ancient"
rm_container "$cnr"
else
yush_debug "Container $cnr is still young"
fi
done
fi
fi
if printf %s\\n "$RESOURCES" | grep -qo "volume"; then
yush_notice "Cleaning up dangling volumes..."
for vol in $(docker volume ls -qf dangling=true); do
CONSIDER=0
if printf %s\\n "$vol" | grep -Eqo '[0-9a-f]{64}'; then
CONSIDER=1
yush_debug "Counting files in unnamed, dangling volume: $vol"
else
CONSIDER=$(consider "$vol" volume)
fi
if [ "$CONSIDER" = "1" ]; then
files=$(docker run --rm -v "${vol}":/data "$BUSYBOX" find /data -type f -xdev -print | wc -l)
if [ "$files" -le "$MAXFILES" ]; then
if [ "$DRYRUN" = "1" ]; then
yush_info "Would remove dangling volume $(yush_yellow "$vol") with less than $MAXFILES file(s)"
else
# Try removing, let Docker decide upon the final result
yush_notice "Removing dangling volume $(yush_red "$vol"), with less than $MAXFILES file(s)"
docker volume rm --force "${vol}" || true
fi
else
yush_info "Keeping dangling volume $(yush_green "$vol") with $files file(s)"
fi
fi
done
fi
if printf %s\\n "$RESOURCES" | grep -qo "image"; then
yush_notice "Cleaning up dangling images..."
for img in $(docker image ls -qf dangling=true); do
rm_image "$img" "dangling"
done
yush_info "Cleaning up orphan images..."
# Get SHA256 of image used by all existing containers. This isn't the same
# as docker container ls -aq --format {{.Image}} as this command omits the
# tag and tries to resolve to names. We want the SHA256 to guarantee
# uniqueness.
yush_info "Collecting images used by existing containers (whichever status), this may take time..."
in_use=""
for cnr in $(docker container ls -aq); do
nm=$(docker container inspect --format '{{.Name}}' "$cnr")
yush_debug "Collecting images and dependencies for container $nm ($cnr)"
img=$(docker container inspect --format '{{.Image}}' "$cnr")
in_use=$(printf -- "%s\n%s" "$in_use" "$img")
for dep in $(docker image history -q --no-trunc "$img" | grep -v 'missing'); do
[ "$img" != "$dep" ] && in_use=$(printf -- "%s\n\t%s" "$in_use" "$dep")
done
done
# Try remove logic on all images that are not in use in any container.
if [ "$INTERMEDIATE" = "0" ]; then
images=$(docker image ls -q)
else
images=$(docker image ls -qa)
fi
for img in $images; do
sha256=$(docker inspect --format '{{.Id}}' "$img")
tags=$(docker inspect --format '{{.RepoTags}}' "$img")
if printf %s\\n "$in_use" | grep -qo "$sha256"; then
yush_debug "Keeping image $(yush_green "$img") ($tags), used by existing container"
else
rm_image "$img" "orphan"
fi
done
fi
[ -n "$RESOURCES" ] && yush_info "Done cleaning: $RESOURCES"
# Execute remaining arguments as a command, if any
if [ $# -ne "0" ]; then
yush_info "Executing $*"
exec "$@"
fi