|
@@ -5,13 +5,13 @@ It works on the whole registry or the specified repositories.
|
5
|
5
|
The optional -x flag may be used to completely remove the specified repositories or tagged images.
|
6
|
6
|
|
7
|
7
|
NOTES:
|
8
|
|
- - This script stops the Registry container during cleanup to prevent corruption,
|
|
8
|
+ - This script pauses the Registry container during cleanup to prevent corruption,
|
9
|
9
|
making it temporarily unavailable to clients.
|
10
|
10
|
- This script assumes local storage (the filesystem storage driver).
|
11
|
11
|
- This script may run stand-alone (on local setups) or dockerized (which supports remote Docker setups).
|
12
|
12
|
- This script is Python 3 only.
|
13
|
13
|
|
14
|
|
-v1.3.2 by Ricardo Branco
|
|
14
|
+v1.4 by Ricardo Branco
|
15
|
15
|
|
16
|
16
|
MIT License
|
17
|
17
|
"""
|
|
@@ -34,7 +34,7 @@ from docker.errors import APIError, NotFound, TLSParameterError
|
34
|
34
|
|
35
|
35
|
import yaml
|
36
|
36
|
|
37
|
|
-VERSION = "1.3.2"
|
|
37
|
+VERSION = "1.4"
|
38
|
38
|
REGISTRY_DIR = "REGISTRY_STORAGE_FILESYSTEM_ROOTREGISTRY_DIR"
|
39
|
39
|
args = None
|
40
|
40
|
|
|
@@ -151,14 +151,14 @@ def check_name(image):
|
151
|
151
|
|
152
|
152
|
class RegistryCleaner():
|
153
|
153
|
'''Simple callable class for Docker Registry cleaning duties'''
|
154
|
|
- def __init__(self, container=None, volume=None):
|
|
154
|
+ def __init__(self, container_or_service=None, volume=None):
|
155
|
155
|
try:
|
156
|
156
|
self.docker = docker.from_env()
|
157
|
157
|
except TLSParameterError as err:
|
158
|
158
|
error(err)
|
159
|
159
|
|
160
|
|
- if container is None:
|
161
|
|
- self.container = None
|
|
160
|
+ if volume:
|
|
161
|
+ self.containers = None
|
162
|
162
|
try:
|
163
|
163
|
self.volume = self.docker.volumes.get(volume)
|
164
|
164
|
self.registry_dir = self.volume.attrs['Mountpoint']
|
|
@@ -171,14 +171,10 @@ class RegistryCleaner():
|
171
|
171
|
self.registry_dir = "/var/lib/registry"
|
172
|
172
|
return
|
173
|
173
|
|
174
|
|
- try:
|
175
|
|
- self.info = self.docker.api.inspect_container(container)
|
176
|
|
- self.container = self.info['Id']
|
177
|
|
- except (APIError, exceptions.ConnectionError) as err:
|
178
|
|
- error(err)
|
|
174
|
+ self._get_info(container_or_service)
|
179
|
175
|
|
180
|
176
|
if not re.match("registry:2(@sha256:[0-9a-f]{64})?$", self.info['Config']['Image']):
|
181
|
|
- error("The container %s is not running the registry:2 image" % (container))
|
|
177
|
+ error("%s is not running the registry:2 image" % (container_or_service))
|
182
|
178
|
|
183
|
179
|
if LooseVersion(self.get_image_version()) < LooseVersion("v2.4.0"):
|
184
|
180
|
error("You're not running Docker Registry 2.4.0+")
|
|
@@ -194,8 +190,11 @@ class RegistryCleaner():
|
194
|
190
|
except FileNotFoundError as err:
|
195
|
191
|
error(err)
|
196
|
192
|
|
197
|
|
- if self.container is not None:
|
198
|
|
- self.docker.api.stop(self.container)
|
|
193
|
+ # We could use stop() but the orchestrator would start another container
|
|
194
|
+ if self.containers is not None:
|
|
195
|
+ for container in self.containers:
|
|
196
|
+ if self.info['State']['Running']:
|
|
197
|
+ self.docker.api.pause(container)
|
199
|
198
|
|
200
|
199
|
images = args.images or \
|
201
|
200
|
map(os.path.dirname, iglob("**/_manifests", recursive=True))
|
|
@@ -208,17 +207,52 @@ class RegistryCleaner():
|
208
|
207
|
if not self.garbage_collect():
|
209
|
208
|
exit_status = 1
|
210
|
209
|
|
211
|
|
- if self.container is not None:
|
212
|
|
- self.docker.api.start(self.container)
|
|
210
|
+ if self.containers is not None:
|
|
211
|
+ for container in self.containers:
|
|
212
|
+ if self.info['State']['Running']:
|
|
213
|
+ self.docker.api.unpause(container)
|
213
|
214
|
|
214
|
215
|
self.docker.close()
|
215
|
216
|
return exit_status
|
216
|
217
|
|
|
218
|
+ def _get_info(self, container_or_service):
|
|
219
|
+ self.containers = []
|
|
220
|
+
|
|
221
|
+ # Is a service?
|
|
222
|
+ try:
|
|
223
|
+ service = self.docker.services.get(container_or_service)
|
|
224
|
+ except NotFound:
|
|
225
|
+ pass
|
|
226
|
+ except (APIError, exceptions.ConnectionError) as err:
|
|
227
|
+ error(err)
|
|
228
|
+ else:
|
|
229
|
+ # Get list of containers to pause them all
|
|
230
|
+ try:
|
|
231
|
+ tasks = service.tasks(filters={'desired-state': "running"})
|
|
232
|
+ except (APIError, NotFound, exceptions.ConnectionError) as err:
|
|
233
|
+ error(err)
|
|
234
|
+ self.containers = [
|
|
235
|
+ item['Status']['ContainerStatus']['ContainerID']
|
|
236
|
+ for _, item in enumerate(tasks)
|
|
237
|
+ ]
|
|
238
|
+
|
|
239
|
+ # Get information from the first container in list.
|
|
240
|
+ # We can't get the source of /var/lib/registry from inspect_service()
|
|
241
|
+ # if a bind mount was not specified.
|
|
242
|
+ try:
|
|
243
|
+ self.info = self.docker.api.inspect_container(
|
|
244
|
+ self.containers[0] if self.containers else container_or_service
|
|
245
|
+ )
|
|
246
|
+ except (APIError, NotFound, exceptions.ConnectionError) as err:
|
|
247
|
+ error(err)
|
|
248
|
+ if not self.containers:
|
|
249
|
+ self.containers = [self.info['Id']]
|
|
250
|
+
|
217
|
251
|
def get_file(self, path):
|
218
|
252
|
'''Returns the contents of the specified file from the container'''
|
219
|
253
|
try:
|
220
|
254
|
with BytesIO(b"".join(
|
221
|
|
- _ for _ in self.docker.api.get_archive(self.container, path)[0]
|
|
255
|
+ _ for _ in self.docker.api.get_archive(self.containers[0], path)[0]
|
222
|
256
|
)) as buf, tarfile.open(fileobj=buf) \
|
223
|
257
|
as tar, tar.extractfile(os.path.basename(path)) \
|
224
|
258
|
as infile:
|
|
@@ -263,11 +297,11 @@ class RegistryCleaner():
|
263
|
297
|
'''Gets the Docker distribution version running on the container'''
|
264
|
298
|
if self.info['State']['Running']:
|
265
|
299
|
data = self.docker.containers.get(
|
266
|
|
- self.container
|
|
300
|
+ self.containers[0]
|
267
|
301
|
).exec_run("/bin/registry --version").output
|
268
|
302
|
else:
|
269
|
303
|
data = self.docker.containers.run(
|
270
|
|
- self.info["Image"], command="--version", remove=True
|
|
304
|
+ self.info['Config']['Image'], command="--version", remove=True
|
271
|
305
|
)
|
272
|
306
|
return data.decode('utf-8').split()[2]
|
273
|
307
|
|
|
@@ -308,7 +342,7 @@ class RegistryCleaner():
|
308
|
342
|
def main():
|
309
|
343
|
'''Main function'''
|
310
|
344
|
progname = os.path.basename(sys.argv[0])
|
311
|
|
- usage = "\rUsage: " + progname + " [OPTIONS] VOLUME|CONTAINER [REPOSITORY[:TAG]]..." + """
|
|
345
|
+ usage = "\rUsage: " + progname + " [OPTIONS] SERVICE|CONTAINER|VOLUME [REPOSITORY[:TAG]]..." + """
|
312
|
346
|
Options:
|
313
|
347
|
-x, --remove Remove the specified images or repositories.
|
314
|
348
|
-v, --volume Specify a volume instead of container.
|
|
@@ -321,7 +355,7 @@ Options:
|
321
|
355
|
parser.add_argument('-x', '--remove', action='store_true')
|
322
|
356
|
parser.add_argument('-v', '--volume', action='store_true')
|
323
|
357
|
parser.add_argument('-V', '--version', action='store_true')
|
324
|
|
- parser.add_argument('container_or_volume', nargs='?')
|
|
358
|
+ parser.add_argument('foobar', nargs='?')
|
325
|
359
|
parser.add_argument('images', nargs='*')
|
326
|
360
|
global args
|
327
|
361
|
args = parser.parse_args()
|
|
@@ -332,7 +366,7 @@ Options:
|
332
|
366
|
elif args.version:
|
333
|
367
|
print(progname + " " + VERSION)
|
334
|
368
|
sys.exit(0)
|
335
|
|
- elif not args.container_or_volume:
|
|
369
|
+ elif not args.foobar:
|
336
|
370
|
print('usage: ' + usage)
|
337
|
371
|
sys.exit(1)
|
338
|
372
|
|
|
@@ -344,9 +378,9 @@ Options:
|
344
|
378
|
error("The -x option requires that you specify at least one repository...")
|
345
|
379
|
|
346
|
380
|
if args.volume:
|
347
|
|
- cleaner = RegistryCleaner(volume=args.container_or_volume)
|
|
381
|
+ cleaner = RegistryCleaner(volume=args.foobar)
|
348
|
382
|
else:
|
349
|
|
- cleaner = RegistryCleaner(container=args.container_or_volume)
|
|
383
|
+ cleaner = RegistryCleaner(container_or_service=args.foobar)
|
350
|
384
|
|
351
|
385
|
sys.exit(cleaner())
|
352
|
386
|
|