Skip to content

Commit 1464cc7

Browse files
committed
fix(RELEASE-1491): cleanup_tags supports multiple repos
this PR fixes the case when the image has multiple repositories and ends up not cleaning tags of all of them. Signed-off-by: Leandro Mendes <lmendes@redhat.com>
1 parent 9633be6 commit 1464cc7

2 files changed

Lines changed: 72 additions & 28 deletions

File tree

pyxis/cleanup_tags.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def parse_arguments() -> argparse.Namespace: # pragma: no cover
4040
),
4141
help="Pyxis Graphql endpoint",
4242
)
43+
parser.add_argument("--repository", required=True, help="Repository to cleanup tags from")
4344
parser.add_argument(
4445
"image_id",
4546
help="Pyxis Container Image ID",
@@ -58,14 +59,15 @@ def parse_arguments() -> argparse.Namespace: # pragma: no cover
5859
def cleanup_tags_with_retry(
5960
graphql_api: str,
6061
image_id: str,
62+
target_repository: str,
6163
retries: int = 3,
6264
backoff_factor: float = 5.0,
6365
):
6466
last_err = RuntimeError()
6567
for attempt in range(retries):
6668
try:
6769
time.sleep(backoff_factor * attempt)
68-
cleanup_tags(graphql_api, image_id)
70+
cleanup_tags(graphql_api, image_id, target_repository)
6971
return
7072
except RuntimeError as e:
7173
LOGGER.warning(f"Attempt {attempt+1} failed.")
@@ -74,13 +76,14 @@ def cleanup_tags_with_retry(
7476
raise last_err
7577

7678

77-
def cleanup_tags(graphql_api, image_id: str):
79+
def cleanup_tags(graphql_api, image_id: str, target_repository: str):
7880
image = get_image(graphql_api, image_id)
7981

80-
registry, repository, tags = get_rh_registry_image_properties(image)
82+
registry, repository, tags = get_rh_registry_image_properties(image, target_repository)
8183

8284
LOGGER.info(f"Image id: {image['_id']}")
8385
LOGGER.info(f"Image architecture: {image['architecture']}")
86+
LOGGER.info(f"Repository: {repository}")
8487
LOGGER.info(f"Image tags: {tags}")
8588

8689
images_for_cleanup = {}
@@ -102,7 +105,7 @@ def cleanup_tags(graphql_api, image_id: str):
102105
images_for_cleanup[id] = candidate
103106

104107
LOGGER.info(f"Found {len(images_for_cleanup)} images for cleanup.")
105-
update_images(graphql_api, tags, images_for_cleanup)
108+
update_images(graphql_api, tags, images_for_cleanup, repository)
106109

107110

108111
def get_image(graphql_api: str, image_id: str) -> dict:
@@ -137,19 +140,23 @@ def get_image(graphql_api: str, image_id: str) -> dict:
137140
return image
138141

139142

140-
def get_rh_registry_image_properties(image: Dict):
143+
def get_rh_registry_image_properties(image: Dict, target_repository: str):
141144
"""Get the registry.access.redhat.com repository properties of the image
142145
needed to search for related images.
143146
144-
Returns (registry, repository, tags)
147+
:return: (registry, repository, tags)
145148
"""
146149
for repo in image["repositories"]:
147-
if repo["registry"] == "registry.access.redhat.com":
150+
if (
151+
repo["registry"] == "registry.access.redhat.com"
152+
and repo["repository"] == target_repository
153+
):
148154
if repo["tags"] is None:
149155
tags = []
150156
else:
151157
tags = [tag["name"] for tag in repo["tags"]]
152158
return repo["registry"], repo["repository"], tags
159+
153160
raise RuntimeError(
154161
"Cannot find the registry.access.redhat.com repository entry for the image"
155162
)
@@ -246,7 +253,7 @@ def get_candidates_for_cleanup(
246253
return images
247254

248255

249-
def update_images(graphql_api: str, tags: List[str], images: Dict):
256+
def update_images(graphql_api: str, tags: List[str], images: Dict, target_repository: str):
250257
"""Update images to remove unwanted tags from them
251258
252259
For each image in `images` it will remove all `tags`
@@ -257,13 +264,16 @@ def update_images(graphql_api: str, tags: List[str], images: Dict):
257264
LOGGER.info(f"Updating image {image['_id']} with architecture {image['architecture']}")
258265
LOGGER.info("Repositories and tags before update:")
259266
for repository in image["repositories"]:
260-
repo_tags = [tag["name"] for tag in repository["tags"]]
267+
repo_tags = [tag["name"] for tag in repository.get("tags") or []]
261268
LOGGER.info(f" {repository['registry']}/{repository['repository']}: {repo_tags}")
262269
for i in range(len(image["repositories"])):
263-
repo_tags = image["repositories"][i]["tags"]
264-
image["repositories"][i]["tags"] = [
265-
tag for tag in repo_tags if tag["name"] not in tags
266-
]
270+
# clean up tags only only from the given repository
271+
if image["repositories"][i]["repository"] == target_repository:
272+
repo_tags = image["repositories"][i]["tags"]
273+
image["repositories"][i]["tags"] = [
274+
tag for tag in repo_tags if tag["name"] not in tags
275+
]
276+
267277
# When we load the images for patching, we request all fields of
268278
# the ContainerRepository objects because otherwise we might remove some data with
269279
# the update. But that means that fields that are not used will be null/None and
@@ -354,9 +364,9 @@ def main(): # pragma: no cover
354364
LOGGER.debug(f"Pyxis GraphQL API: {args.pyxis_graphql_api}")
355365

356366
if args.retry:
357-
cleanup_tags_with_retry(args.pyxis_graphql_api, args.image_id)
367+
cleanup_tags_with_retry(args.pyxis_graphql_api, args.image_id, args.repository)
358368
else:
359-
cleanup_tags(args.pyxis_graphql_api, args.image_id)
369+
cleanup_tags(args.pyxis_graphql_api, args.image_id, args.repository)
360370

361371

362372
if __name__ == "__main__": # pragma: no cover

pyxis/test_cleanup_tags.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@
2121
@patch("cleanup_tags.cleanup_tags")
2222
def test_cleanup_tags_with_retry__success(mock_cleanup_tags):
2323
"""cleanup_tags succeeds on first attempt"""
24-
cleanup_tags_with_retry(GRAPHQL_API, IMAGE_ID)
24+
cleanup_tags_with_retry(GRAPHQL_API, IMAGE_ID, REPOSITORY)
2525

26-
mock_cleanup_tags.assert_called_once_with(GRAPHQL_API, IMAGE_ID)
26+
mock_cleanup_tags.assert_called_once_with(GRAPHQL_API, IMAGE_ID, REPOSITORY)
2727

2828

2929
@patch("cleanup_tags.cleanup_tags")
3030
def test_cleanup_tags_with_retry__success_after_one_attempt(mock_cleanup_tags):
3131
"""cleanup_tags succeeds after one retry"""
3232
mock_cleanup_tags.side_effect = [RuntimeError("error"), None]
3333

34-
cleanup_tags_with_retry(GRAPHQL_API, IMAGE_ID, backoff_factor=0)
34+
cleanup_tags_with_retry(GRAPHQL_API, IMAGE_ID, REPOSITORY, backoff_factor=0)
3535

3636
assert mock_cleanup_tags.call_count == 2
3737

@@ -42,7 +42,7 @@ def test_cleanup_tags_with_retry__fails(mock_cleanup_tags):
4242
mock_cleanup_tags.side_effect = RuntimeError("error")
4343

4444
with pytest.raises(RuntimeError):
45-
cleanup_tags_with_retry(GRAPHQL_API, IMAGE_ID, retries=2, backoff_factor=0)
45+
cleanup_tags_with_retry(GRAPHQL_API, IMAGE_ID, REPOSITORY, retries=2, backoff_factor=0)
4646

4747
assert mock_cleanup_tags.call_count == 2
4848

@@ -72,7 +72,7 @@ def test_cleanup_tags__success(
7272
[image1, image3],
7373
]
7474

75-
cleanup_tags(GRAPHQL_API, "1111")
75+
cleanup_tags(GRAPHQL_API, "1111", REPOSITORY)
7676

7777
mock_get_image.assert_called_once_with(GRAPHQL_API, "1111")
7878
assert mock_get_candidates_for_cleanup.call_args_list == [
@@ -81,7 +81,7 @@ def test_cleanup_tags__success(
8181
call(GRAPHQL_API, REGISTRY, REPOSITORY, "9.4-1111"),
8282
]
8383
mock_update_images.assert_called_once_with(
84-
GRAPHQL_API, ["latest", "9.4", "9.4-1111"], {image2["_id"]: image2}
84+
GRAPHQL_API, ["latest", "9.4", "9.4-1111"], {image2["_id"]: image2}, REPOSITORY
8585
)
8686

8787

@@ -108,15 +108,17 @@ def test_cleanup_tags__nothing_to_cleanup(
108108
[image1, image2],
109109
]
110110

111-
cleanup_tags(GRAPHQL_API, "1111")
111+
cleanup_tags(GRAPHQL_API, "1111", REPOSITORY)
112112

113113
mock_get_image.assert_called_once_with(GRAPHQL_API, "1111")
114114
assert mock_get_candidates_for_cleanup.call_args_list == [
115115
call(GRAPHQL_API, REGISTRY, REPOSITORY, "latest"),
116116
call(GRAPHQL_API, REGISTRY, REPOSITORY, "9.4"),
117117
call(GRAPHQL_API, REGISTRY, REPOSITORY, "9.4-1111"),
118118
]
119-
mock_update_images.assert_called_once_with(GRAPHQL_API, ["latest", "9.4", "9.4-1111"], {})
119+
mock_update_images.assert_called_once_with(
120+
GRAPHQL_API, ["latest", "9.4", "9.4-1111"], {}, REPOSITORY
121+
)
120122

121123

122124
@patch("pyxis.graphql_query")
@@ -137,7 +139,7 @@ def test_get_rh_registry_image_properties__success():
137139
"""
138140
image = generate_image("1111", "amd64", ["latest"])
139141

140-
registry, repository, tags = get_rh_registry_image_properties(image)
142+
registry, repository, tags = get_rh_registry_image_properties(image, REPOSITORY)
141143

142144
assert registry == REGISTRY
143145
assert repository == REPOSITORY
@@ -152,13 +154,25 @@ def test_get_rh_registry_image_properties__no_tags():
152154
image["repositories"][0]["tags"] = None
153155
image["repositories"][1]["tags"] = None
154156

155-
registry, repository, tags = get_rh_registry_image_properties(image)
157+
registry, repository, tags = get_rh_registry_image_properties(image, REPOSITORY)
156158

157159
assert registry == REGISTRY
158160
assert repository == REPOSITORY
159161
assert tags == []
160162

161163

164+
def test_get_rh_registry_image_properties__multiple_images__repository_set__success():
165+
"""Basic scenario where the function parses the image with multiple repositories
166+
and returns the expected values
167+
"""
168+
image = generate_image("1111", "amd64", ["latest"], True)
169+
registry, repository, tags = get_rh_registry_image_properties(image, REPOSITORY)
170+
171+
assert registry == REGISTRY
172+
assert repository == REPOSITORY
173+
assert tags == ["latest"]
174+
175+
162176
def test_get_rh_registry_image_properties__failure():
163177
"""The Red Hat registry repository is not found in the image,
164178
so an exception is raised
@@ -176,7 +190,7 @@ def test_get_rh_registry_image_properties__failure():
176190
]
177191

178192
with pytest.raises(RuntimeError):
179-
get_rh_registry_image_properties(image)
193+
get_rh_registry_image_properties(image, REPOSITORY)
180194

181195

182196
@patch("pyxis.graphql_query")
@@ -203,16 +217,26 @@ def test_update_images__success(mock_update_image):
203217
"""Happy path scenario:
204218
There are 2 images on input and both have the correct tags removed
205219
"""
220+
221+
lasting_tags = {
222+
"image1": [{"name": "latest"}, {"name": "9.4"}, {"name": "9.4-1111"}],
223+
"image2": [{"name": "9.4"}, {"name": "9.4-2222"}],
224+
}
225+
206226
image1 = generate_image("1111", "amd64", ["latest", "9.4", "9.4-1111"])
207227
image1_new = generate_image("1111", "amd64", ["9.4-1111"])
228+
image1_new["repositories"][0]["tags"] = lasting_tags["image1"]
229+
208230
image2 = generate_image("2222", "amd64", ["9.4", "9.4-2222"])
209231
image2_new = generate_image("2222", "amd64", ["9.4-2222"])
232+
image2_new["repositories"][0]["tags"] = lasting_tags["image2"]
233+
210234
images = {
211235
image1["_id"]: image1,
212236
image2["_id"]: image2,
213237
}
214238

215-
update_images(GRAPHQL_API, ["latest", "9.4", "9.4-0000"], images)
239+
update_images(GRAPHQL_API, ["latest", "9.4", "9.4-0000"], images, REPOSITORY)
216240

217241
assert mock_update_image.call_args_list == [
218242
call(GRAPHQL_API, image1_new),
@@ -252,7 +276,7 @@ def test_remove_none_values__success():
252276
assert remove_none_values(data) == expected_result
253277

254278

255-
def generate_image(id, architecture, tags):
279+
def generate_image(id, architecture, tags, multiple_repos=False):
256280
image = {
257281
"_id": id,
258282
"architecture": architecture,
@@ -269,6 +293,16 @@ def generate_image(id, architecture, tags):
269293
},
270294
],
271295
}
296+
297+
if multiple_repos is not False:
298+
image["repositories"].append(
299+
{
300+
"registry": REGISTRY,
301+
"repository": "redhat-nonprod/myproduct----myimage",
302+
"tags": [{"name": tag} for tag in tags],
303+
}
304+
)
305+
272306
return image
273307

274308

0 commit comments

Comments
 (0)