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 /
|
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]
|
cd [path-where-your-docker-compose.yml]
|
||||||
docker-compose stop registry
|
docker-compose stop registry
|
||||||
docker-compose run --rm \
|
docker-compose run --rm \
|
||||||
registry bin/registry garbage-collect \
|
registry bin/registry garbage-collect --delete-untagged \
|
||||||
/etc/docker/registry/config.yml
|
/etc/docker/registry/config.yml
|
||||||
docker-compose up -d registry
|
docker-compose up -d registry
|
||||||
```
|
```
|
||||||
or (if you are not using docker-compose):
|
or (if you are not using docker-compose):
|
||||||
```
|
```
|
||||||
docker stop registry:2
|
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
|
/etc/docker/registry/config.yml
|
||||||
docker start registry:2
|
docker start registry:2
|
||||||
```
|
```
|
||||||
|
|||||||
92
registry.py
92
registry.py
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
######
|
######
|
||||||
# github repository: https://github.com/andrey-pohilko/registry-cli
|
# github repository: https://github.com/andrey-pohilko/registry-cli
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
import ast
|
import ast
|
||||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
import urllib3
|
||||||
|
from urllib3.exceptions import InsecureRequestWarning
|
||||||
import json
|
import json
|
||||||
import pprint
|
import pprint
|
||||||
import base64
|
import base64
|
||||||
@@ -148,7 +149,7 @@ def decode_base64(data):
|
|||||||
missing_padding = len(data) % 4
|
missing_padding = len(data) % 4
|
||||||
if missing_padding != 0:
|
if missing_padding != 0:
|
||||||
data += b'='* (4 - missing_padding)
|
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):
|
def get_error_explanation(context, error_code):
|
||||||
@@ -178,10 +179,9 @@ def get_auth_schemes(r,path):
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
print('[debug][docker] Auth schemes found:{0}'.format([m for m in oauth]))
|
print('[debug][docker] Auth schemes found:{0}'.format([m for m in oauth]))
|
||||||
return [m.lower() for m in oauth]
|
return [m.lower() for m in oauth]
|
||||||
else:
|
if DEBUG:
|
||||||
if DEBUG:
|
print('[debug][docker] No Auth schemes found')
|
||||||
print('[debug][docker] No Auth schemes found')
|
return []
|
||||||
return []
|
|
||||||
|
|
||||||
# class to manipulate registry
|
# class to manipulate registry
|
||||||
class Registry:
|
class Registry:
|
||||||
@@ -265,11 +265,20 @@ class Registry:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def list_images(self):
|
def list_images(self):
|
||||||
result = self.send('/v2/_catalog?n=10000')
|
images = []
|
||||||
if result is None:
|
last = ""
|
||||||
return []
|
# 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 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):
|
def list_tags(self, image_name):
|
||||||
result = self.send("/v2/{0}/tags/list".format(image_name))
|
result = self.send("/v2/{0}/tags/list".format(image_name))
|
||||||
@@ -364,10 +373,7 @@ class Registry:
|
|||||||
if json_result['schemaVersion'] == 1:
|
if json_result['schemaVersion'] == 1:
|
||||||
print("Docker schemaVersion 1 isn't supported for deleting by age now")
|
print("Docker schemaVersion 1 isn't supported for deleting by age now")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
return json_result['config']
|
||||||
tag_config = json_result['config']
|
|
||||||
|
|
||||||
return tag_config
|
|
||||||
|
|
||||||
def get_image_age(self, image_name, image_config):
|
def get_image_age(self, image_name, image_config):
|
||||||
container_header = {"Accept": "{0}".format(
|
container_header = {"Accept": "{0}".format(
|
||||||
@@ -393,10 +399,9 @@ class Registry:
|
|||||||
self.last_error = None
|
self.last_error = None
|
||||||
image_age = json.loads(response.text)
|
image_age = json.loads(response.text)
|
||||||
return image_age['created']
|
return image_age['created']
|
||||||
else:
|
print(" blob not found: {0}".format(self.last_error))
|
||||||
print(" blob not found: {0}".format(self.last_error))
|
self.last_error = response.status_code
|
||||||
self.last_error = response.status_code
|
return []
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args(args=None):
|
def parse_args(args=None):
|
||||||
@@ -559,6 +564,13 @@ for more detail on garbage collection read here:
|
|||||||
'Useful if your tag names are not in a fixed order.'),
|
'Useful if your tag names are not in a fixed order.'),
|
||||||
action='store_true'
|
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)
|
return parser.parse_args(args)
|
||||||
|
|
||||||
|
|
||||||
@@ -605,22 +617,24 @@ def delete_tags(
|
|||||||
# registry.delete_tag_layer(image_name, layer_digest, dry_run)
|
# 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()
|
result = set()
|
||||||
for tag_like in args_tags_like:
|
for tag_like in args_tags_like:
|
||||||
print("tag like: {0}".format(tag_like))
|
if not plain:
|
||||||
|
print("tag like: {0}".format(tag_like))
|
||||||
for tag in tags_list:
|
for tag in tags_list:
|
||||||
if re.search(tag_like, tag):
|
if re.search(tag_like, tag):
|
||||||
print("Adding {0} to tags list".format(tag))
|
if not plain:
|
||||||
|
print("Adding {0} to tags list".format(tag))
|
||||||
result.add(tag)
|
result.add(tag)
|
||||||
return result
|
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
|
# check if there are args for special tags
|
||||||
result = set()
|
result = set()
|
||||||
if tags_like:
|
if tags_like:
|
||||||
result = get_tags_like(tags_like, all_tags_list)
|
result = get_tags_like(tags_like, all_tags_list, plain)
|
||||||
else:
|
else:
|
||||||
result.update(all_tags_list)
|
result.update(all_tags_list)
|
||||||
|
|
||||||
@@ -672,10 +686,8 @@ def get_newer_tags(registry, image_name, hours, tags_list):
|
|||||||
print("Keeping tag: {0} timestamp: {1}".format(
|
print("Keeping tag: {0} timestamp: {1}".format(
|
||||||
tag, image_age))
|
tag, image_age))
|
||||||
return tag
|
return tag
|
||||||
else:
|
print("Will delete tag: {0} timestamp: {1}".format(tag, image_age))
|
||||||
print("Will delete tag: {0} timestamp: {1}".format(
|
return None
|
||||||
tag, image_age))
|
|
||||||
return None
|
|
||||||
|
|
||||||
print('---------------------------------')
|
print('---------------------------------')
|
||||||
p = ThreadPool(4)
|
p = ThreadPool(4)
|
||||||
@@ -685,7 +697,7 @@ def get_newer_tags(registry, image_name, hours, tags_list):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_datetime_tags(registry, image_name, tags_list):
|
def get_datetime_tags(registry, image_name, tags_list, plain):
|
||||||
def newer(tag):
|
def newer(tag):
|
||||||
image_config = registry.get_tag_config(image_name, tag)
|
image_config = registry.get_tag_config(image_name, tag)
|
||||||
if image_config == []:
|
if image_config == []:
|
||||||
@@ -700,7 +712,8 @@ def get_datetime_tags(registry, image_name, tags_list):
|
|||||||
"datetime": parse(image_age).astimezone(tzutc())
|
"datetime": parse(image_age).astimezone(tzutc())
|
||||||
}
|
}
|
||||||
|
|
||||||
print('---------------------------------')
|
if not plain:
|
||||||
|
print('---------------------------------')
|
||||||
p = ThreadPool(4)
|
p = ThreadPool(4)
|
||||||
result = list(x for x in p.map(newer, tags_list) if x)
|
result = list(x for x in p.map(newer, tags_list) if x)
|
||||||
p.close()
|
p.close()
|
||||||
@@ -741,7 +754,7 @@ def main_loop(args):
|
|||||||
keep_last_versions = int(args.num)
|
keep_last_versions = int(args.num)
|
||||||
|
|
||||||
if args.no_validate_ssl:
|
if args.no_validate_ssl:
|
||||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
urllib3.disable_warnings(InsecureRequestWarning)
|
||||||
|
|
||||||
if args.read_password:
|
if args.read_password:
|
||||||
if args.login is None:
|
if args.login is None:
|
||||||
@@ -788,20 +801,27 @@ def main_loop(args):
|
|||||||
# loop through registry's images
|
# loop through registry's images
|
||||||
# or through the ones given in command line
|
# or through the ones given in command line
|
||||||
for image_name in image_list:
|
for image_name in image_list:
|
||||||
print("---------------------------------")
|
if not args.plain:
|
||||||
print("Image: {0}".format(image_name))
|
print("---------------------------------")
|
||||||
|
print("Image: {0}".format(image_name))
|
||||||
all_tags_list = registry.list_tags(image_name)
|
all_tags_list = registry.list_tags(image_name)
|
||||||
|
|
||||||
if not all_tags_list:
|
if not all_tags_list:
|
||||||
print(" no tags!")
|
print(" no tags!")
|
||||||
continue
|
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
|
# print(tags and optionally layers
|
||||||
for tag in tags_list:
|
for tag in tags_list:
|
||||||
print(" tag: {0}".format(tag))
|
if not args.plain:
|
||||||
|
print(" tag: {0}".format(tag))
|
||||||
|
else:
|
||||||
|
print("{0}:{1}".format(image_name, tag))
|
||||||
|
|
||||||
if args.layers:
|
if args.layers:
|
||||||
for layer in registry.list_tag_layers(image_name, tag):
|
for layer in registry.list_tag_layers(image_name, tag):
|
||||||
if 'size' in layer:
|
if 'size' in layer:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
certifi==2017.7.27.1
|
certifi>=2017.7.27.1
|
||||||
chardet==3.0.4
|
chardet>=3.0.4
|
||||||
idna>=2.5
|
idna>=2.5
|
||||||
python-dateutil==2.8.0
|
python-dateutil>=2.8.0
|
||||||
requests>=2.20.0
|
requests>=2.20.0
|
||||||
urllib3>=1.23
|
urllib3>=1.23
|
||||||
www-authenticate==0.9.2
|
www-authenticate>=0.9.2
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
mock
|
|
||||||
coverage
|
coverage
|
||||||
certifi
|
certifi
|
||||||
chardet
|
chardet
|
||||||
|
|||||||
44
test.py
44
test.py
@@ -7,7 +7,7 @@ from dateutil.tz import tzutc, tzoffset
|
|||||||
from registry import Registry, Requests, get_tags, parse_args, \
|
from registry import Registry, Requests, get_tags, parse_args, \
|
||||||
delete_tags, delete_tags_by_age, get_error_explanation, get_newer_tags, \
|
delete_tags, delete_tags_by_age, get_error_explanation, get_newer_tags, \
|
||||||
keep_images_like, main_loop, get_datetime_tags, get_ordered_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
|
import requests
|
||||||
|
|
||||||
|
|
||||||
@@ -231,18 +231,18 @@ class TestListTags(unittest.TestCase):
|
|||||||
def test_list_tags_like_various(self):
|
def test_list_tags_like_various(self):
|
||||||
tags_list = set(['FINAL_0.1', 'SNAPSHOT_0.1',
|
tags_list = set(['FINAL_0.1', 'SNAPSHOT_0.1',
|
||||||
"0.1.SNAP", "1.0.0_FINAL"])
|
"0.1.SNAP", "1.0.0_FINAL"])
|
||||||
self.assertEqual(get_tags(tags_list, "", set(
|
for plain in [True, False]:
|
||||||
["FINAL"])), set(["FINAL_0.1", "1.0.0_FINAL"]))
|
self.assertEqual(get_tags(tags_list, "", set(
|
||||||
self.assertEqual(get_tags(tags_list, "", set(
|
["FINAL"]), plain), set(["FINAL_0.1", "1.0.0_FINAL"]))
|
||||||
["SNAPSHOT"])), set(['SNAPSHOT_0.1']))
|
self.assertEqual(get_tags(tags_list, "", set(
|
||||||
self.assertEqual(get_tags(tags_list, "", set()),
|
["SNAPSHOT"]), plain), set(['SNAPSHOT_0.1']))
|
||||||
set(['FINAL_0.1', 'SNAPSHOT_0.1', "0.1.SNAP", "1.0.0_FINAL"]))
|
self.assertEqual(get_tags(tags_list, "", set(), plain),
|
||||||
self.assertEqual(get_tags(tags_list, "", set(["ABSENT"])), set())
|
set(['FINAL_0.1', 'SNAPSHOT_0.1', "0.1.SNAP", "1.0.0_FINAL"]))
|
||||||
|
self.assertEqual(get_tags(tags_list, "", set(["ABSENT"]), plain), set())
|
||||||
self.assertEqual(
|
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(
|
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):
|
class TestListDigest(unittest.TestCase):
|
||||||
@@ -760,17 +760,19 @@ class TestGetDatetimeTags(unittest.TestCase):
|
|||||||
self.registry.http.reset_return_value(200, "MOCK_DIGEST")
|
self.registry.http.reset_return_value(200, "MOCK_DIGEST")
|
||||||
|
|
||||||
def test_get_datetime_tags(self):
|
def test_get_datetime_tags(self):
|
||||||
self.assertEqual(
|
for plain in [True, False]:
|
||||||
get_datetime_tags(self.registry, "imagename", ["latest"]),
|
self.assertEqual(
|
||||||
[{"tag": "latest", "datetime": datetime(2017, 12, 27, 12, 47, 33, 511765, tzinfo=tzutc())}]
|
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):
|
def test_get_non_utc_datetime_tags(self):
|
||||||
self.registry.get_image_age.return_value = "2019-07-18T16:33:15.864962122+02:00"
|
self.registry.get_image_age.return_value = "2019-07-18T16:33:15.864962122+02:00"
|
||||||
self.assertEqual(
|
for plain in [True, False]:
|
||||||
get_datetime_tags(self.registry, "imagename", ["latest"]),
|
self.assertEqual(
|
||||||
[{"tag": "latest", "datetime": datetime(2019, 7, 18, 16, 33, 15, 864962, tzinfo=tzoffset(None, 7200))}]
|
get_datetime_tags(self.registry, "imagename", ["latest"], plain),
|
||||||
)
|
[{"tag": "latest", "datetime": datetime(2019, 7, 18, 16, 33, 15, 864962, tzinfo=tzoffset(None, 7200))}]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestGetOrderedTags(unittest.TestCase):
|
class TestGetOrderedTags(unittest.TestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user