Compare commits

...

12 Commits

Author SHA1 Message Date
Eugene Howe
6fc625e350 fix dockerfile
All checks were successful
Build and Publish registry cleaner / publish (push) Successful in 48s
2025-11-04 06:30:37 -05:00
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 123 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,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

View File

@@ -1,4 +1,4 @@
FROM python:2.7-alpine FROM python:alpine
ADD requirements-build.txt / 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] 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
``` ```

View File

@@ -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,7 +179,6 @@ 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 []
@@ -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 = []
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: 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): 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,7 +399,6 @@ 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 []
@@ -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:
if not plain:
print("tag like: {0}".format(tag_like)) 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):
if not plain:
print("Adding {0} to tags list".format(tag)) 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,9 +686,7 @@ 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(
tag, image_age))
return None return None
print('---------------------------------') print('---------------------------------')
@@ -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,6 +712,7 @@ def get_datetime_tags(registry, image_name, tags_list):
"datetime": parse(image_age).astimezone(tzutc()) "datetime": parse(image_age).astimezone(tzutc())
} }
if not plain:
print('---------------------------------') 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)
@@ -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:
if not args.plain:
print("---------------------------------") print("---------------------------------")
print("Image: {0}".format(image_name)) 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:
if not args.plain:
print(" tag: {0}".format(tag)) 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:

View File

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

View File

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

22
test.py
View File

@@ -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"])
for plain in [True, False]:
self.assertEqual(get_tags(tags_list, "", set( 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( self.assertEqual(get_tags(tags_list, "", set(
["SNAPSHOT"])), set(['SNAPSHOT_0.1'])) ["SNAPSHOT"]), plain), set(['SNAPSHOT_0.1']))
self.assertEqual(get_tags(tags_list, "", set()), self.assertEqual(get_tags(tags_list, "", set(), plain),
set(['FINAL_0.1', 'SNAPSHOT_0.1', "0.1.SNAP", "1.0.0_FINAL"])) 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( 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,15 +760,17 @@ 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):
for plain in [True, False]:
self.assertEqual( 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())}] [{"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"
for plain in [True, False]:
self.assertEqual( 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))}] [{"tag": "latest", "datetime": datetime(2019, 7, 18, 16, 33, 15, 864962, tzinfo=tzoffset(None, 7200))}]
) )