Skip to content

Commit

Permalink
refactor: patching ContainerImage to add repositories
Browse files Browse the repository at this point in the history
This is a followup of what was done in #303

Most notably:

- rename the image_already_exists to find_image
  to reflect what it does - it used to return bool, now it returns
  the image if found.
- only include the repositories field in the patch payload
- for consistency, create the same two repo entries when adding
  a new repository like we do when creating a brand new image

Signed-off-by: Martin Malina <[email protected]>
  • Loading branch information
mmalina committed Nov 8, 2024
1 parent 6963f58 commit 3ce907f
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 70 deletions.
127 changes: 64 additions & 63 deletions pyxis/create_container_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,21 +140,15 @@ def proxymap(repository: str) -> str:
return repository.split("/")[-1].replace("----", "/")


def image_already_exists(args, digest: str, repository: str) -> Any:
"""Function to check if a containerImage with the given digest and repository
already exists in the pyxis instance
def find_image(args, digest: str) -> Any:
"""Function to find a containerImage with the given digest.
If `repository` is None, then the return True if the image exists at all.
:return: the image id, if one exists, else None if not found
:return: the image, if one exists, else None if none found
"""

# quote is needed to urlparse the quotation marks
raw_filter = f'repositories.manifest_schema2_digest=="{digest}";not(deleted==true)'
if repository:
raw_filter += f';repositories.repository=="{proxymap(repository)}"'
# quote is needed to urlparse the quotation marks
filter_str = quote(raw_filter)

check_url = urljoin(args.pyxis_url, f"v1/images?page_size=1&filter={filter_str}")

# Get the list of the ContainerImages with given parameters
Expand All @@ -174,6 +168,18 @@ def image_already_exists(args, digest: str, repository: str) -> Any:
return query_results[0]


def repo_in_image(repository_str: str, image: Dict[str, Any]) -> bool:
"""Check if a repository already exists in the ContainerImage
:return: True if repository_str string is found in the ContainerImage repositories,
False otherwise
"""
for repository in image["repositories"]:
if repository["repository"] == repository_str:
return True
return False


def prepare_parsed_data(args) -> Dict[str, Any]:
"""Function to extract the data this script needs from provided oras manifest fetch output
Expand Down Expand Up @@ -250,19 +256,16 @@ def create_container_image(args, parsed_data: Dict[str, Any]):

LOGGER.info("Creating new container image")

date_now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f+00:00")

if "digest" not in parsed_data:
raise Exception("Digest was not found in the passed oras manifest json")
if "name" not in parsed_data:
raise Exception("Name was not found in the passed oras manifest json")
docker_image_digest = parsed_data["digest"]

repositories = construct_repositories(args, parsed_data)

# digest isn't accepted in the parsed_data payload to pyxis
del parsed_data["digest"]

image_name = parsed_data["name"]
image_registry = image_name.split("/")[0]
image_repo = image_name.split("/", 1)[1]
# name isn't accepted in the parsed_data payload to pyxis
del parsed_data["name"]

Expand All @@ -278,15 +281,7 @@ def create_container_image(args, parsed_data: Dict[str, Any]):
upload_url = urljoin(args.pyxis_url, "v1/images")

container_image_payload = {
"repositories": [
{
"published": False,
"registry": image_registry,
"repository": image_repo,
"push_date": date_now,
"tags": pyxis_tags(args, date_now),
}
],
"repositories": repositories,
"certified": json.loads(args.certified.lower()),
"image_id": args.architecture_digest,
"architecture": parsed_data["architecture"],
Expand All @@ -296,24 +291,6 @@ def create_container_image(args, parsed_data: Dict[str, Any]):
"uncompressed_top_layer_id": uncompressed_top_layer_id,
}

container_image_payload["repositories"][0].update(
repository_digest_values(args, docker_image_digest)
)

# For images released to registry.redhat.io we need a second repository item
# with published=true and registry and repository converted.
# E.g. if the name in the oras manifest result is
# "quay.io/redhat-prod/rhtas-tech-preview----cosign-rhel9",
# repository will be "rhtas-tech-preview/cosign-rhel9"
if not args.rh_push == "true":
LOGGER.info("--rh-push is not set. Skipping public registry association.")
else:
repo = container_image_payload["repositories"][0].copy()
repo["published"] = True
repo["registry"] = "registry.access.redhat.com"
repo["repository"] = proxymap(image_name)
container_image_payload["repositories"].append(repo)

rsp = pyxis.post(upload_url, container_image_payload).json()

# Make sure container metadata was successfully added to Pyxis
Expand All @@ -331,25 +308,12 @@ def add_container_image_repository(args, parsed_data: Dict[str, Any], image: Dic
identifier = image["_id"]
LOGGER.info(f"Adding repository to container image {identifier}")

date_now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f+00:00")

image_name = parsed_data["name"]
docker_image_digest = parsed_data["digest"]

patch_url = urljoin(args.pyxis_url, f"v1/images/id/{identifier}")

image["repositories"].append(
{
"published": True,
"registry": "registry.access.redhat.com",
"repository": proxymap(image_name),
"push_date": date_now,
"tags": pyxis_tags(args, date_now),
}
)
image["repositories"][-1].update(repository_digest_values(args, docker_image_digest))
payload = {"repositories": image["repositories"]}
payload["repositories"].extend(construct_repositories(args, parsed_data))

rsp = pyxis.patch(patch_url, image).json()
rsp = pyxis.patch(patch_url, payload).json()

# Make sure container metadata was successfully added to Pyxis
if "_id" in rsp:
Expand All @@ -358,6 +322,43 @@ def add_container_image_repository(args, parsed_data: Dict[str, Any], image: Dic
raise Exception("Image metadata was not successfully added to Pyxis.")


def construct_repositories(args, parsed_data):
image_name = parsed_data["name"]
image_registry = image_name.split("/")[0]
image_repo = image_name.split("/", 1)[1]

date_now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f+00:00")
docker_image_digest = parsed_data["digest"]

repos = [{
"published": False,
"registry": image_registry,
"repository": image_repo,
"push_date": date_now,
"tags": pyxis_tags(args, date_now),
}]

repos[0].update(
repository_digest_values(args, docker_image_digest)
)

# For images released to registry.redhat.io we need a second repository item
# with published=true and registry and repository converted.
# E.g. if the name in the oras manifest result is
# "quay.io/redhat-prod/rhtas-tech-preview----cosign-rhel9",
# repository will be "rhtas-tech-preview/cosign-rhel9"
if not args.rh_push == "true":
LOGGER.info("--rh-push is not set. Skipping public registry association.")
else:
rh_repo = repos[0].copy()
rh_repo["published"] = True
rh_repo["registry"] = "registry.access.redhat.com"
rh_repo["repository"] = proxymap(image_name)
repos.append(rh_repo)

return repos


def main(): # pragma: no cover
"""Main func"""

Expand All @@ -369,11 +370,11 @@ def main(): # pragma: no cover
parsed_data = prepare_parsed_data(args)

# First check if it exists at all
image = image_already_exists(args, args.architecture_digest, repository=None)
if image:
# Then, check if it exists in association with the given repository
image = find_image(args, args.architecture_digest)
if image is not None:
identifier = image["_id"]
if image_already_exists(args, args.architecture_digest, repository=args.name):
# Then, check if it already references the given repository
if repo_in_image(args.name.split("/", 1)[-1], image):
LOGGER.info(
f"Image with given docker_image_digest already exists as {identifier} "
f"and is associated with repository {args.name}. "
Expand Down
14 changes: 7 additions & 7 deletions pyxis/test_create_container_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from unittest.mock import patch, MagicMock

from create_container_image import (
image_already_exists,
find_image,
create_container_image,
add_container_image_repository,
prepare_parsed_data,
Expand All @@ -15,7 +15,7 @@


@patch("create_container_image.pyxis.get")
def test_image_already_exists__image_does_exist(mock_get):
def test_find_image__image_does_exist(mock_get):
# Arrange
mock_rsp = MagicMock()
mock_get.return_value = mock_rsp
Expand All @@ -28,7 +28,7 @@ def test_image_already_exists__image_does_exist(mock_get):
mock_rsp.json.return_value = {"data": [{"_id": 0}]}

# Act
exists = image_already_exists(args, args.architecture_digest, args.name)
exists = find_image(args, args.architecture_digest, args.name)

# Assert
assert exists
Expand All @@ -42,7 +42,7 @@ def test_image_already_exists__image_does_exist(mock_get):


@patch("create_container_image.pyxis.get")
def test_image_already_exists__image_does_not_exist(mock_get):
def test_find_image__image_does_not_exist(mock_get):
# Arrange
mock_rsp = MagicMock()
mock_get.return_value = mock_rsp
Expand All @@ -55,14 +55,14 @@ def test_image_already_exists__image_does_not_exist(mock_get):
mock_rsp.json.return_value = {"data": []}

# Act
exists = image_already_exists(args, digest, name)
exists = find_image(args, digest, name)

# Assert
assert not exists


@patch("create_container_image.pyxis.get")
def test_image_already_exists__image_does_exist_but_no_repo(mock_get):
def test_find_image__image_does_exist_but_no_repo(mock_get):
# Arrange
mock_rsp = MagicMock()
mock_get.return_value = mock_rsp
Expand All @@ -75,7 +75,7 @@ def test_image_already_exists__image_does_exist_but_no_repo(mock_get):
mock_rsp.json.return_value = {"data": [{"_id": 0}]}

# Act
exists = image_already_exists(args, args.architecture_digest, None)
exists = find_image(args, args.architecture_digest, None)

# Assert
assert exists
Expand Down

0 comments on commit 3ce907f

Please sign in to comment.