mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
Add a dangerzone-image
CLI script
It contains utilities to interact with OCI registries, like getting the list of published tags and getting the content of a manifest. It does so via the use of the Docker Registry API v2 [0]. The script has been added to the `dev_scripts`, and is also installed on the system under `dangerzone-image`. [0] https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
This commit is contained in:
parent
83be5fb151
commit
e1bdb75435
6 changed files with 185 additions and 0 deletions
3
dangerzone/updater/__init__.py
Normal file
3
dangerzone/updater/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
42
dangerzone/updater/cli.py
Normal file
42
dangerzone/updater/cli.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
from . import attestations, errors, log, registry, signatures
|
||||
|
||||
DEFAULT_REPOSITORY = "freedomofpress/dangerzone"
|
||||
DEFAULT_BRANCH = "main"
|
||||
DEFAULT_IMAGE_NAME = "ghcr.io/freedomofpress/dangerzone/dangerzone"
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option("--debug", is_flag=True)
|
||||
def main(debug: bool) -> None:
|
||||
if debug:
|
||||
click.echo("Debug mode enabled")
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.INFO
|
||||
logging.basicConfig(level=level)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("image")
|
||||
def list_remote_tags(image: str) -> None:
|
||||
"""List the tags available for a given image."""
|
||||
click.echo(f"Existing tags for {image}")
|
||||
for tag in registry.list_tags(image):
|
||||
click.echo(tag)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("image")
|
||||
def get_manifest(image: str) -> None:
|
||||
"""Retrieves a remote manifest for a given image and displays it."""
|
||||
click.echo(registry.get_manifest(image).content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
10
dangerzone/updater/errors.py
Normal file
10
dangerzone/updater/errors.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
class UpdaterError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ImageNotFound(UpdaterError):
|
||||
pass
|
||||
|
||||
|
||||
class RegistryError(UpdaterError):
|
||||
pass
|
116
dangerzone/updater/registry.py
Normal file
116
dangerzone/updater/registry.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
import re
|
||||
from collections import namedtuple
|
||||
from hashlib import sha256
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
from . import errors, log
|
||||
|
||||
__all__ = [
|
||||
"get_manifest_digest",
|
||||
"list_tags",
|
||||
"get_manifest",
|
||||
"parse_image_location",
|
||||
]
|
||||
|
||||
SIGSTORE_BUNDLE = "application/vnd.dev.sigstore.bundle.v0.3+json"
|
||||
IMAGE_INDEX_MEDIA_TYPE = "application/vnd.oci.image.index.v1+json"
|
||||
ACCEPT_MANIFESTS_HEADER = ",".join(
|
||||
[
|
||||
"application/vnd.docker.distribution.manifest.v1+json",
|
||||
"application/vnd.docker.distribution.manifest.v1+prettyjws",
|
||||
"application/vnd.docker.distribution.manifest.v2+json",
|
||||
"application/vnd.oci.image.manifest.v1+json",
|
||||
"application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
IMAGE_INDEX_MEDIA_TYPE,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Image = namedtuple("Image", ["registry", "namespace", "image_name", "tag", "digest"])
|
||||
|
||||
|
||||
def parse_image_location(input_string: str) -> Image:
|
||||
"""Parses container image location into an Image namedtuple"""
|
||||
pattern = (
|
||||
r"^"
|
||||
r"(?P<registry>[a-zA-Z0-9.-]+)/"
|
||||
r"(?P<namespace>[a-zA-Z0-9-]+)/"
|
||||
r"(?P<image_name>[^:@]+)"
|
||||
r"(?::(?P<tag>[a-zA-Z0-9.-]+))?"
|
||||
r"(?:@(?P<digest>sha256:[a-zA-Z0-9]+))?"
|
||||
r"$"
|
||||
)
|
||||
match = re.match(pattern, input_string)
|
||||
if not match:
|
||||
raise ValueError("Malformed image location")
|
||||
return Image(
|
||||
registry=match.group("registry"),
|
||||
namespace=match.group("namespace"),
|
||||
image_name=match.group("image_name"),
|
||||
tag=match.group("tag") or "latest",
|
||||
digest=match.group("digest"),
|
||||
)
|
||||
|
||||
|
||||
def _get_auth_header(image: Image) -> Dict[str, str]:
|
||||
auth_url = f"https://{image.registry}/token"
|
||||
response = requests.get(
|
||||
auth_url,
|
||||
params={
|
||||
"service": f"{image.registry}",
|
||||
"scope": f"repository:{image.namespace}/{image.image_name}:pull",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
token = response.json()["token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _url(image: Image) -> str:
|
||||
return f"https://{image.registry}/v2/{image.namespace}/{image.image_name}"
|
||||
|
||||
|
||||
def list_tags(image_str: str) -> list:
|
||||
image = parse_image_location(image_str)
|
||||
url = f"{_url(image)}/tags/list"
|
||||
response = requests.get(url, headers=_get_auth_header(image))
|
||||
response.raise_for_status()
|
||||
tags = response.json().get("tags", [])
|
||||
return tags
|
||||
|
||||
|
||||
def get_manifest(image_str: str) -> requests.Response:
|
||||
"""Get manifest information for a specific tag"""
|
||||
image = parse_image_location(image_str)
|
||||
manifest_url = f"{_url(image)}/manifests/{image.tag}"
|
||||
headers = {
|
||||
"Accept": ACCEPT_MANIFESTS_HEADER,
|
||||
}
|
||||
headers.update(_get_auth_header(image))
|
||||
|
||||
response = requests.get(manifest_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
|
||||
def list_manifests(image_str: str) -> list:
|
||||
return get_manifest(image_str).json().get("manifests")
|
||||
|
||||
|
||||
def get_blob(image: Image, digest: str) -> requests.Response:
|
||||
response = requests.get(
|
||||
f"{_url(image)}/blobs/{digest}", headers=_get_auth_header(image)
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
|
||||
def get_manifest_digest(
|
||||
image_str: str, tag_manifest_content: Optional[bytes] = None
|
||||
) -> str:
|
||||
if not tag_manifest_content:
|
||||
tag_manifest_content = get_manifest(image_str).content
|
||||
|
||||
return sha256(tag_manifest_content).hexdigest()
|
13
dev_scripts/dangerzone-image
Executable file
13
dev_scripts/dangerzone-image
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Load dangerzone module and resources from the source code tree
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.dangerzone_dev = True
|
||||
|
||||
from dangerzone.updater import cli
|
||||
|
||||
cli.main()
|
|
@ -27,6 +27,7 @@ packaging = "*"
|
|||
[tool.poetry.scripts]
|
||||
dangerzone = 'dangerzone:main'
|
||||
dangerzone-cli = 'dangerzone:main'
|
||||
dangerzone-image = "dangerzone.updater.cli:main"
|
||||
|
||||
# Dependencies required for packaging the code on various platforms.
|
||||
[tool.poetry.group.package.dependencies]
|
||||
|
|
Loading…
Reference in a new issue