Compare commits
12 Commits
038e03315e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc625e350 | ||
|
|
c1afcc86f3 | ||
|
|
15f0c00252 | ||
|
|
85bdf89027 | ||
|
|
fb02b47053 | ||
|
|
54b17d3ed7 | ||
|
|
efc61888ac | ||
|
|
2e09704909 | ||
|
|
f6131f7bdd | ||
|
|
a52692cd59 | ||
|
|
9ba20e7487 | ||
|
|
18d45066c6 |
@@ -1,62 +0,0 @@
|
||||
# Python CircleCI 2.0 configuration file
|
||||
#
|
||||
# Check https://circleci.com/docs/2.0/language-python/ for more details
|
||||
#
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
# use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers`
|
||||
- image: circleci/python:3.6.1
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||
# - image: circleci/postgres:9.4
|
||||
|
||||
working_directory: ~/repo
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "requirements-ci.txt" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
|
||||
- run:
|
||||
name: install dependencies
|
||||
command: |
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -r requirements-ci.txt
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- ./venv
|
||||
key: v1-dependencies-{{ checksum "requirements-ci.txt" }}
|
||||
|
||||
# run tests!
|
||||
# this example uses Django's built-in test-runner
|
||||
# other common Python testing frameworks include pytest and nose
|
||||
# https://pytest.org
|
||||
# https://nose.readthedocs.io
|
||||
- run:
|
||||
name: run tests
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
coverage run --include=test.py,registry.py test.py
|
||||
|
||||
- run:
|
||||
name: generate report
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
mkdir -p /tmp/coverage
|
||||
coverage html -d /tmp/coverage
|
||||
mv .coverage /tmp/coverage
|
||||
|
||||
- store_artifacts:
|
||||
path: /tmp/coverage
|
||||
37
.gitea/workflows/build.yaml
Normal file
37
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Build and Publish registry cleaner
|
||||
run-name: ${{ gitea.actor }} is runs ci pipeline
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: https://github.com/docker/setup-buildx-action@v3
|
||||
env:
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
with:
|
||||
config-inline: |
|
||||
[registry."docker.office.clintonambulance.com"]
|
||||
- name: Log in to Docker Registry
|
||||
uses: https://github.com/docker/login-action@v3
|
||||
with:
|
||||
registry: docker.office.clintonambulance.com
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push Docker image
|
||||
uses: https://github.com/docker/build-push-action@v5
|
||||
env:
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/arm64
|
||||
tags: |
|
||||
docker.office.clintonambulance.com/registry-cli:${{ gitea.sha }}
|
||||
docker.office.clintonambulance.com/registry-cli:latest
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:2.7-alpine
|
||||
FROM python:alpine
|
||||
|
||||
ADD requirements-build.txt /
|
||||
|
||||
|
||||
@@ -165,14 +165,14 @@ garbage collection. So, make sure you run something like below
|
||||
cd [path-where-your-docker-compose.yml]
|
||||
docker-compose stop registry
|
||||
docker-compose run --rm \
|
||||
registry bin/registry garbage-collect \
|
||||
registry bin/registry garbage-collect --delete-untagged \
|
||||
/etc/docker/registry/config.yml
|
||||
docker-compose up -d registry
|
||||
```
|
||||
or (if you are not using docker-compose):
|
||||
```
|
||||
docker stop registry:2
|
||||
docker run --rm registry:2 bin/registry garbage-collect \
|
||||
docker run --rm registry:2 bin/registry garbage-collect --delete-untagged \
|
||||
/etc/docker/registry/config.yml
|
||||
docker start registry:2
|
||||
```
|
||||
|
||||
64
registry.py
64
registry.py
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
######
|
||||
# github repository: https://github.com/andrey-pohilko/registry-cli
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
import requests
|
||||
import ast
|
||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||
import urllib3
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
import json
|
||||
import pprint
|
||||
import base64
|
||||
@@ -148,7 +149,7 @@ def decode_base64(data):
|
||||
missing_padding = len(data) % 4
|
||||
if missing_padding != 0:
|
||||
data += b'='* (4 - missing_padding)
|
||||
return base64.decodestring(data)
|
||||
return base64.decodebytes(data.encode() if isinstance(data, str) else data)
|
||||
|
||||
|
||||
def get_error_explanation(context, error_code):
|
||||
@@ -178,7 +179,6 @@ def get_auth_schemes(r,path):
|
||||
if DEBUG:
|
||||
print('[debug][docker] Auth schemes found:{0}'.format([m for m in oauth]))
|
||||
return [m.lower() for m in oauth]
|
||||
else:
|
||||
if DEBUG:
|
||||
print('[debug][docker] No Auth schemes found')
|
||||
return []
|
||||
@@ -265,11 +265,20 @@ class Registry:
|
||||
return None
|
||||
|
||||
def list_images(self):
|
||||
result = self.send('/v2/_catalog?n=10000')
|
||||
images = []
|
||||
last = ""
|
||||
# loop through all pages and get 10 records every time
|
||||
while True:
|
||||
result = self.send('/v2/_catalog?n=10&last=' + last)
|
||||
if result is None:
|
||||
return []
|
||||
return images
|
||||
repos = json.loads(result.text)['repositories']
|
||||
if len(repos) == 0:
|
||||
break
|
||||
images += repos
|
||||
last = repos[-1]
|
||||
|
||||
return json.loads(result.text)['repositories']
|
||||
return images
|
||||
|
||||
def list_tags(self, image_name):
|
||||
result = self.send("/v2/{0}/tags/list".format(image_name))
|
||||
@@ -364,10 +373,7 @@ class Registry:
|
||||
if json_result['schemaVersion'] == 1:
|
||||
print("Docker schemaVersion 1 isn't supported for deleting by age now")
|
||||
sys.exit(1)
|
||||
else:
|
||||
tag_config = json_result['config']
|
||||
|
||||
return tag_config
|
||||
return json_result['config']
|
||||
|
||||
def get_image_age(self, image_name, image_config):
|
||||
container_header = {"Accept": "{0}".format(
|
||||
@@ -393,7 +399,6 @@ class Registry:
|
||||
self.last_error = None
|
||||
image_age = json.loads(response.text)
|
||||
return image_age['created']
|
||||
else:
|
||||
print(" blob not found: {0}".format(self.last_error))
|
||||
self.last_error = response.status_code
|
||||
return []
|
||||
@@ -559,6 +564,13 @@ for more detail on garbage collection read here:
|
||||
'Useful if your tag names are not in a fixed order.'),
|
||||
action='store_true'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--plain',
|
||||
help=('Turn plain output, one image:tag per line.'
|
||||
'Useful if your want to send the results to another command.'),
|
||||
action='store_true',
|
||||
default=False
|
||||
)
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
@@ -605,22 +617,24 @@ def delete_tags(
|
||||
# registry.delete_tag_layer(image_name, layer_digest, dry_run)
|
||||
|
||||
|
||||
def get_tags_like(args_tags_like, tags_list):
|
||||
def get_tags_like(args_tags_like, tags_list, plain):
|
||||
result = set()
|
||||
for tag_like in args_tags_like:
|
||||
if not plain:
|
||||
print("tag like: {0}".format(tag_like))
|
||||
for tag in tags_list:
|
||||
if re.search(tag_like, tag):
|
||||
if not plain:
|
||||
print("Adding {0} to tags list".format(tag))
|
||||
result.add(tag)
|
||||
return result
|
||||
|
||||
|
||||
def get_tags(all_tags_list, image_name, tags_like):
|
||||
def get_tags(all_tags_list, image_name, tags_like, plain):
|
||||
# check if there are args for special tags
|
||||
result = set()
|
||||
if tags_like:
|
||||
result = get_tags_like(tags_like, all_tags_list)
|
||||
result = get_tags_like(tags_like, all_tags_list, plain)
|
||||
else:
|
||||
result.update(all_tags_list)
|
||||
|
||||
@@ -672,9 +686,7 @@ def get_newer_tags(registry, image_name, hours, tags_list):
|
||||
print("Keeping tag: {0} timestamp: {1}".format(
|
||||
tag, image_age))
|
||||
return tag
|
||||
else:
|
||||
print("Will delete tag: {0} timestamp: {1}".format(
|
||||
tag, image_age))
|
||||
print("Will delete tag: {0} timestamp: {1}".format(tag, image_age))
|
||||
return None
|
||||
|
||||
print('---------------------------------')
|
||||
@@ -685,7 +697,7 @@ def get_newer_tags(registry, image_name, hours, tags_list):
|
||||
return result
|
||||
|
||||
|
||||
def get_datetime_tags(registry, image_name, tags_list):
|
||||
def get_datetime_tags(registry, image_name, tags_list, plain):
|
||||
def newer(tag):
|
||||
image_config = registry.get_tag_config(image_name, tag)
|
||||
if image_config == []:
|
||||
@@ -700,6 +712,7 @@ def get_datetime_tags(registry, image_name, tags_list):
|
||||
"datetime": parse(image_age).astimezone(tzutc())
|
||||
}
|
||||
|
||||
if not plain:
|
||||
print('---------------------------------')
|
||||
p = ThreadPool(4)
|
||||
result = list(x for x in p.map(newer, tags_list) if x)
|
||||
@@ -741,7 +754,7 @@ def main_loop(args):
|
||||
keep_last_versions = int(args.num)
|
||||
|
||||
if args.no_validate_ssl:
|
||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||
urllib3.disable_warnings(InsecureRequestWarning)
|
||||
|
||||
if args.read_password:
|
||||
if args.login is None:
|
||||
@@ -788,20 +801,27 @@ def main_loop(args):
|
||||
# loop through registry's images
|
||||
# or through the ones given in command line
|
||||
for image_name in image_list:
|
||||
if not args.plain:
|
||||
print("---------------------------------")
|
||||
print("Image: {0}".format(image_name))
|
||||
|
||||
all_tags_list = registry.list_tags(image_name)
|
||||
|
||||
if not all_tags_list:
|
||||
print(" no tags!")
|
||||
continue
|
||||
|
||||
tags_list = get_tags(all_tags_list, image_name, args.tags_like)
|
||||
if args.order_by_date:
|
||||
tags_list = get_ordered_tags(registry, image_name, all_tags_list, args.order_by_date)
|
||||
else:
|
||||
tags_list = get_tags(all_tags_list, image_name, args.tags_like, args.plain)
|
||||
|
||||
# print(tags and optionally layers
|
||||
for tag in tags_list:
|
||||
if not args.plain:
|
||||
print(" tag: {0}".format(tag))
|
||||
else:
|
||||
print("{0}:{1}".format(image_name, tag))
|
||||
|
||||
if args.layers:
|
||||
for layer in registry.list_tag_layers(image_name, tag):
|
||||
if 'size' in layer:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
certifi==2017.7.27.1
|
||||
chardet==3.0.4
|
||||
certifi>=2017.7.27.1
|
||||
chardet>=3.0.4
|
||||
idna>=2.5
|
||||
python-dateutil==2.8.0
|
||||
python-dateutil>=2.8.0
|
||||
requests>=2.20.0
|
||||
urllib3>=1.23
|
||||
www-authenticate==0.9.2
|
||||
www-authenticate>=0.9.2
|
||||
@@ -1,4 +1,3 @@
|
||||
mock
|
||||
coverage
|
||||
certifi
|
||||
chardet
|
||||
|
||||
22
test.py
22
test.py
@@ -7,7 +7,7 @@ from dateutil.tz import tzutc, tzoffset
|
||||
from registry import Registry, Requests, get_tags, parse_args, \
|
||||
delete_tags, delete_tags_by_age, get_error_explanation, get_newer_tags, \
|
||||
keep_images_like, main_loop, get_datetime_tags, get_ordered_tags
|
||||
from mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
import requests
|
||||
|
||||
|
||||
@@ -231,18 +231,18 @@ class TestListTags(unittest.TestCase):
|
||||
def test_list_tags_like_various(self):
|
||||
tags_list = set(['FINAL_0.1', 'SNAPSHOT_0.1',
|
||||
"0.1.SNAP", "1.0.0_FINAL"])
|
||||
for plain in [True, False]:
|
||||
self.assertEqual(get_tags(tags_list, "", set(
|
||||
["FINAL"])), set(["FINAL_0.1", "1.0.0_FINAL"]))
|
||||
["FINAL"]), plain), set(["FINAL_0.1", "1.0.0_FINAL"]))
|
||||
self.assertEqual(get_tags(tags_list, "", set(
|
||||
["SNAPSHOT"])), set(['SNAPSHOT_0.1']))
|
||||
self.assertEqual(get_tags(tags_list, "", set()),
|
||||
["SNAPSHOT"]), plain), set(['SNAPSHOT_0.1']))
|
||||
self.assertEqual(get_tags(tags_list, "", set(), plain),
|
||||
set(['FINAL_0.1', 'SNAPSHOT_0.1', "0.1.SNAP", "1.0.0_FINAL"]))
|
||||
self.assertEqual(get_tags(tags_list, "", set(["ABSENT"])), set())
|
||||
|
||||
self.assertEqual(get_tags(tags_list, "", set(["ABSENT"]), plain), set())
|
||||
self.assertEqual(
|
||||
get_tags(tags_list, "IMAGE:TAG00", ""), set(["TAG00"]))
|
||||
get_tags(tags_list, "IMAGE:TAG00", "", plain), set(["TAG00"]))
|
||||
self.assertEqual(get_tags(tags_list, "IMAGE:TAG00", set(
|
||||
["WILL_NOT_BE_CONSIDERED"])), set(["TAG00"]))
|
||||
["WILL_NOT_BE_CONSIDERED"]), plain), set(["TAG00"]))
|
||||
|
||||
|
||||
class TestListDigest(unittest.TestCase):
|
||||
@@ -760,15 +760,17 @@ class TestGetDatetimeTags(unittest.TestCase):
|
||||
self.registry.http.reset_return_value(200, "MOCK_DIGEST")
|
||||
|
||||
def test_get_datetime_tags(self):
|
||||
for plain in [True, False]:
|
||||
self.assertEqual(
|
||||
get_datetime_tags(self.registry, "imagename", ["latest"]),
|
||||
get_datetime_tags(self.registry, "imagename", ["latest"], plain),
|
||||
[{"tag": "latest", "datetime": datetime(2017, 12, 27, 12, 47, 33, 511765, tzinfo=tzutc())}]
|
||||
)
|
||||
|
||||
def test_get_non_utc_datetime_tags(self):
|
||||
self.registry.get_image_age.return_value = "2019-07-18T16:33:15.864962122+02:00"
|
||||
for plain in [True, False]:
|
||||
self.assertEqual(
|
||||
get_datetime_tags(self.registry, "imagename", ["latest"]),
|
||||
get_datetime_tags(self.registry, "imagename", ["latest"], plain),
|
||||
[{"tag": "latest", "datetime": datetime(2019, 7, 18, 16, 33, 15, 864962, tzinfo=tzoffset(None, 7200))}]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user