Skip to content

Commit

Permalink
endpoints/v2: use proxy class to fetch manifests
Browse files Browse the repository at this point in the history
also refactor proxy code to make it more seamless to use
  • Loading branch information
flavianmissi committed Jan 14, 2022
1 parent dfabd96 commit 795f53e
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 61 deletions.
72 changes: 26 additions & 46 deletions endpoints/v2/manifest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import json

from functools import wraps

Expand Down Expand Up @@ -50,6 +51,7 @@
from util.bytes import Bytes
from util.names import VALID_TAG_PATTERN
from util.registry.replication import queue_replication_batch
from proxy import Proxy


logger = logging.getLogger(__name__)
Expand All @@ -59,49 +61,6 @@
MANIFEST_TAGNAME_ROUTE = BASE_MANIFEST_ROUTE.format(VALID_TAG_PATTERN)


def _proxy_upstream(config, namespace_name, repo_name, manifest_ref):
session = requests.Session()

# use anonymous auth for now.
auth_url = config["auth"].rstrip("/") + f"?service=registry.docker.io&scope=repository:{namespace_name}/{repo_name}:pull"
token = session.get(auth_url).json()["token"]

url = config["registry"].rstrip("/") + f"/v2/{namespace_name}/{repo_name}/manifests/{manifest_ref}"
headers = {
"Authorization": f"Bearer {token}",
}
accept = request.headers.get("Accept", None)
if accept is not None:
headers["Accept"] = accept

# check if manifest exists in upstream, this won't count towards quota
resp = session.head(url, headers=headers)
if resp.status_code != 200:
# TODO: make it clear to the client that this error is a 404
# from the upstream registry.
return Response(
resp.text,
status=resp.status_code,
)

resp = session.get(url, headers=headers)
if resp.status_code != 200:
# TODO: inspect response and return appropriate error to client
return Response(
resp.text,
status=resp.status_code,
)

return Response(
resp.text,
status=200,
headers={
"Content-Type": resp.headers["Content-Type"],
"Docker-Content-Digest": resp.headers["Docker-Content-Digest"],
},
)


@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=["GET"])
@disallow_for_account_recovery_mode
@parse_repository_name()
Expand All @@ -120,8 +79,19 @@ def fetch_manifest_by_tagname(namespace_name, repo_name, manifest_ref):
"auth": "https://auth.docker.io/token",
"repository": "postgres",
}

if namespace_name == PULL_THRU_CONFIG["namespace"]:
return _proxy_upstream(PULL_THRU_CONFIG, namespace_name, repo_name, manifest_ref)
proxy = Proxy(PULL_THRU_CONFIG["registry"], f"{namespace_name}/{repo_name}")
media_type = request.headers.get("Accept", None)
resp = proxy.get_manifest(manifest_ref, media_type)
return Response(
resp["content"],
status=resp["status"],
headers={
"Content-Type": resp["headers"]["Content-Type"],
"Docker-Content-Digest": resp["headers"]["Docker-Content-Digest"],
},
)

repository_ref = registry_model.lookup_repository(namespace_name, repo_name)
if repository_ref is None:
Expand Down Expand Up @@ -185,11 +155,21 @@ def fetch_manifest_by_digest(namespace_name, repo_name, manifest_ref):
# hard code pull-thru proxy config for proof of concept
PULL_THRU_CONFIG = {
"namespace": "library",
"registry": "https://registry.hub.docker.com",
"registry": "https://registry-1.docker.io",
"auth": "https://auth.docker.io/token",
}
if namespace_name == PULL_THRU_CONFIG["namespace"]:
return _proxy_upstream(PULL_THRU_CONFIG, namespace_name, repo_name, manifest_ref)
proxy = Proxy(PULL_THRU_CONFIG["registry"], f"{namespace_name}/{repo_name}")
media_type = request.headers.get("Accept", None)
resp = proxy.get_manifest(manifest_ref, media_type)
return Response(
resp["content"],
status=resp["status"],
headers={
"Content-Type": resp.headers["Content-Type"],
"Docker-Content-Digest": resp.headers["Docker-Content-Digest"],
},
)

repository_ref = registry_model.lookup_repository(namespace_name, repo_name)
if repository_ref is None:
Expand Down
11 changes: 6 additions & 5 deletions proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
WWW_AUTHENTICATE_REGEX = re.compile(r'(\w+)[=] ?"?([^",]+)"?')



def parse_www_auth(value: str) -> dict[str, str]:
"""
Parses WWW-Authenticate parameters and returns a dict of key=val.
Expand Down Expand Up @@ -46,9 +45,11 @@ def get_manifest(self, image_ref: str, media_type: str | None = None) -> dict[st
url,
headers=headers,
)
if not resp.ok:
raise Exception(resp.json())
return resp.json()
return {
"content": resp.text,
"status": resp.status_code,
"headers": resp.headers,
}

def _authorize(self, auth: tuple[str, str] | None = None):
resp = self.session.get(f"{self.base_url}/v2/")
Expand All @@ -63,7 +64,7 @@ def _authorize(self, auth: tuple[str, str] | None = None):
realm = www_auth.get("realm")

scope = f"repository:{self._repo}:pull"
auth_url = f"{realm}?service={service}&scope={scope}"
auth_url = f"{realm}?service={service}&scope={scope}"
basic_auth = None
if auth is not None:
basic_auth = requests.auth.HTTPBasicAuth(auth[0], auth[1])
Expand Down
21 changes: 11 additions & 10 deletions test/test_proxy.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
import json

from httmock import urlmatch, response, HTTMock

Expand Down Expand Up @@ -124,25 +125,25 @@ def test_auth_with_user_creds_set_session_token(self):
def test_get_manifest(self):
with HTTMock(docker_registry_mock):
proxy = Proxy("registry-1.docker.io", "library/postgres")
manifest = proxy.get_manifest(
resp = proxy.get_manifest(
image_ref="14", media_type="application/vnd.docker.distribution.manifest.v2+json"
)
self.assertEqual(
list(manifest.keys()), ["schemaVersion", "mediaType", "config", "layers"]
)
manifest = json.loads(resp["content"])
self.assertEqual(
list(manifest.keys()), ["schemaVersion", "mediaType", "config", "layers"]
)

def test_get_manifest_404(self):
with HTTMock(docker_registry_mock):
proxy = Proxy("registry-1.docker.io", "library/postgres")
with self.assertRaises(Exception) as cm:
manifest = proxy.get_manifest(
image_ref="666",
media_type="application/vnd.docker.distribution.manifest.v2+json",
)
resp = proxy.get_manifest(
image_ref="666",
media_type="application/vnd.docker.distribution.manifest.v2+json",
)
unknown_manifest = {
"code": "MANIFEST_UNKNOWN",
"message": "manifest unknown",
"detail": "unknown tag=666",
}
error = cm.exception.args[0].get("errors", [{}])[0]
error = json.loads(resp["content"])["errors"][0]
self.assertEqual(unknown_manifest, error)

0 comments on commit 795f53e

Please sign in to comment.