Compare commits

..

11 Commits

Author SHA1 Message Date
Eugene Howe
c1afcc86f3 python 3
Some checks failed
Build and Publish registry cleaner / publish (push) Failing after 37s
2025-11-04 06:29:05 -05:00
Tarek M. Sayed
15f0c00252 fix: registry-2.8.2 by loop on pages (#121)
LGTM!
2025-08-21 09:46:50 +02:00
Juha Ylitalo
85bdf89027 Unnecessary "else" after "return" (#115)
* Unnecessary "else" after "return"
2024-02-23 19:47:23 +01:00
Ivan Pavlushin
fb02b47053 Merge pull request #118 from jylitalo/fix_tests
Fix test.py to reflect --plain
2024-02-23 19:37:41 +01:00
Juha Ylitalo
54b17d3ed7 Fix test.py to reflect --plain 2024-02-23 15:19:51 +02:00
andrey-pohilko
efc61888ac Merge pull request #109 from tostt/plain-option
add --plain option
2023-01-24 16:31:05 +06:00
tostt
2e09704909 compatibility with flag --tags-like 2023-01-24 10:54:35 +01:00
tostt
f6131f7bdd add --plain option
This option lists all images with their tags and without any other formatiing
2022-02-22 15:42:52 +01:00
andrey-pohilko
a52692cd59 Update README.md
add flag to delete untagged images
2021-09-15 18:01:01 +06:00
Ivan Pavlushin
9ba20e7487 Merge pull request #94 from m0rtez4/master
Fix --order-by-date flag not working when trying to get images list
2020-10-25 22:45:37 +01:00
Morteza Ghasempour
18d45066c6 Fix --order-by-date flag not working when getting images list 2020-10-12 12:27:49 +03:30
8 changed files with 124 additions and 127 deletions

View File

@@ -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

View File

@@ -0,0 +1,38 @@
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: .
file: frontend/Dockerfile
push: true
platforms: linux/arm64
tags: |
docker.office.clintonambulance.com/registry-cli:${{ gitea.sha }}
docker.office.clintonambulance.com/registry-cli:latest

View File

@@ -1,4 +1,4 @@
FROM python:2.7-alpine
FROM python:alpine
ADD requirements-build.txt /

View File

@@ -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
```

View File

@@ -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,10 +179,9 @@ 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 []
if DEBUG:
print('[debug][docker] No Auth schemes found')
return []
# class to manipulate registry
class Registry:
@@ -265,11 +265,20 @@ class Registry:
return None
def list_images(self):
result = self.send('/v2/_catalog?n=10000')
if result is None:
return []
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 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,10 +399,9 @@ 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 []
print(" blob not found: {0}".format(self.last_error))
self.last_error = response.status_code
return []
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.'),
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:
print("tag like: {0}".format(tag_like))
if not plain:
print("tag like: {0}".format(tag_like))
for tag in tags_list:
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)
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,10 +686,8 @@ 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))
return None
print("Will delete tag: {0} timestamp: {1}".format(tag, image_age))
return None
print('---------------------------------')
p = ThreadPool(4)
@@ -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,7 +712,8 @@ def get_datetime_tags(registry, image_name, tags_list):
"datetime": parse(image_age).astimezone(tzutc())
}
print('---------------------------------')
if not plain:
print('---------------------------------')
p = ThreadPool(4)
result = list(x for x in p.map(newer, tags_list) if x)
p.close()
@@ -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:
print("---------------------------------")
print("Image: {0}".format(image_name))
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:
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:
for layer in registry.list_tag_layers(image_name, tag):
if 'size' in layer:

View File

@@ -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

View File

@@ -1,4 +1,3 @@
mock
coverage
certifi
chardet

44
test.py
View File

@@ -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"])
self.assertEqual(get_tags(tags_list, "", set(
["FINAL"])), 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()),
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, "IMAGE:TAG00", ""), set(["TAG00"]))
self.assertEqual(get_tags(tags_list, "IMAGE:TAG00", set(
["WILL_NOT_BE_CONSIDERED"])), set(["TAG00"]))
for plain in [True, False]:
self.assertEqual(get_tags(tags_list, "", set(
["FINAL"]), plain), set(["FINAL_0.1", "1.0.0_FINAL"]))
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"]), plain), set())
self.assertEqual(
get_tags(tags_list, "IMAGE:TAG00", "", plain), set(["TAG00"]))
self.assertEqual(get_tags(tags_list, "IMAGE:TAG00", set(
["WILL_NOT_BE_CONSIDERED"]), plain), set(["TAG00"]))
class TestListDigest(unittest.TestCase):
@@ -760,17 +760,19 @@ class TestGetDatetimeTags(unittest.TestCase):
self.registry.http.reset_return_value(200, "MOCK_DIGEST")
def test_get_datetime_tags(self):
self.assertEqual(
get_datetime_tags(self.registry, "imagename", ["latest"]),
[{"tag": "latest", "datetime": datetime(2017, 12, 27, 12, 47, 33, 511765, tzinfo=tzutc())}]
)
for plain in [True, False]:
self.assertEqual(
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"
self.assertEqual(
get_datetime_tags(self.registry, "imagename", ["latest"]),
[{"tag": "latest", "datetime": datetime(2019, 7, 18, 16, 33, 15, 864962, tzinfo=tzoffset(None, 7200))}]
)
for plain in [True, False]:
self.assertEqual(
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):