77from pathlib import PurePosixPath
88from typing import Any , Optional , Self , cast
99
10- import requests
10+ import httpx
1111from werkzeug .datastructures import WWWAuthenticate
1212
13- from ... errors . user import ImageParseError
13+ from renku_data_services . errors import errors
1414
1515
1616class ManifestTypes (Enum ):
@@ -29,16 +29,18 @@ class ImageRepoDockerAPI:
2929
3030 hostname : str
3131 oauth2_token : Optional [str ] = field (default = None , repr = False )
32+ # NOTE: We need to follow redirects so that we can authenticate with the image repositories properly.
33+ client : httpx .AsyncClient = httpx .AsyncClient (timeout = 10 , follow_redirects = True )
3234
33- def _get_docker_token (self , image : "Image" ) -> Optional [str ]:
35+ async def _get_docker_token (self , image : "Image" ) -> Optional [str ]:
3436 """Get an authorization token from the docker v2 API.
3537
3638 This will return the token provided by the API (or None if no token was found).
3739 """
3840 image_digest_url = f"https://{ self .hostname } /v2/{ image .name } /manifests/{ image .tag } "
3941 try :
40- auth_req = requests . get (image_digest_url , timeout = 10 )
41- except requests . ConnectionError :
42+ auth_req = await self . client . get (image_digest_url )
43+ except httpx . ConnectError :
4244 auth_req = None
4345 if auth_req is None or not (auth_req .status_code == 401 and "Www-Authenticate" in auth_req .headers ):
4446 # the request status code and header are not what is expected
@@ -54,56 +56,55 @@ def _get_docker_token(self, image: "Image") -> Optional[str]:
5456 if self .oauth2_token :
5557 creds = base64 .urlsafe_b64encode (f"oauth2:{ self .oauth2_token } " .encode ()).decode ()
5658 headers ["Authorization" ] = f"Basic { creds } "
57- token_req = requests . get (realm , params = params , headers = headers , timeout = 10 )
59+ token_req = await self . client . get (realm , params = params , headers = headers )
5860 return str (token_req .json ().get ("token" ))
5961
60- def get_image_manifest (self , image : "Image" ) -> Optional [dict [str , Any ]]:
62+ async def get_image_manifest (self , image : "Image" ) -> Optional [dict [str , Any ]]:
6163 """Query the docker API to get the manifest of an image."""
6264 if image .hostname != self .hostname :
63- raise ImageParseError (
64- f"The image hostname { image .hostname } does not match " f"the image repository { self .hostname } "
65+ raise errors . ValidationError (
66+ message = f"The image hostname { image .hostname } does not match " f"the image repository { self .hostname } "
6567 )
66- token = self ._get_docker_token (image )
68+ token = await self ._get_docker_token (image )
6769 image_digest_url = f"https://{ image .hostname } /v2/{ image .name } /manifests/{ image .tag } "
6870 headers = {"Accept" : ManifestTypes .docker_v2 .value }
6971 if token :
7072 headers ["Authorization" ] = f"Bearer { token } "
71- res = requests . get (image_digest_url , headers = headers , timeout = 10 )
73+ res = await self . client . get (image_digest_url , headers = headers )
7274 if res .status_code != 200 :
7375 headers ["Accept" ] = ManifestTypes .oci_v1 .value
74- res = requests . get (image_digest_url , headers = headers , timeout = 10 )
76+ res = await self . client . get (image_digest_url , headers = headers )
7577 if res .status_code != 200 :
7678 return None
7779 return cast (dict [str , Any ], res .json ())
7880
79- def image_exists (self , image : "Image" ) -> bool :
81+ async def image_exists (self , image : "Image" ) -> bool :
8082 """Check the docker repo API if the image exists."""
81- return self .get_image_manifest (image ) is not None
83+ return await self .get_image_manifest (image ) is not None
8284
83- def get_image_config (self , image : "Image" ) -> Optional [dict [str , Any ]]:
85+ async def get_image_config (self , image : "Image" ) -> Optional [dict [str , Any ]]:
8486 """Query the docker API to get the configuration of an image."""
85- manifest = self .get_image_manifest (image )
87+ manifest = await self .get_image_manifest (image )
8688 if manifest is None :
8789 return None
8890 config_digest = manifest .get ("config" , {}).get ("digest" )
8991 if config_digest is None :
9092 return None
91- token = self ._get_docker_token (image )
92- res = requests .get (
93+ token = await self ._get_docker_token (image )
94+ res = await self . client .get (
9395 f"https://{ image .hostname } /v2/{ image .name } /blobs/{ config_digest } " ,
9496 headers = {
9597 "Accept" : "application/json" ,
9698 "Authorization" : f"Bearer { token } " ,
9799 },
98- timeout = 10 ,
99100 )
100101 if res .status_code != 200 :
101102 return None
102103 return cast (dict [str , Any ], res .json ())
103104
104- def image_workdir (self , image : "Image" ) -> Optional [PurePosixPath ]:
105+ async def image_workdir (self , image : "Image" ) -> Optional [PurePosixPath ]:
105106 """Query the docker API to get the workdir of an image."""
106- config = self .get_image_config (image )
107+ config = await self .get_image_config (image )
107108 if config is None :
108109 return None
109110 nested_config = config .get ("config" , {})
@@ -204,9 +205,9 @@ def build_re(*parts: str) -> re.Pattern:
204205 if len (matches ) == 1 :
205206 return cls (matches [0 ]["hostname" ], matches [0 ]["image" ], matches [0 ]["tag" ])
206207 elif len (matches ) > 1 :
207- raise ImageParseError ( f"Cannot parse the image { path } , too many interpretations { matches } " )
208+ raise errors . ValidationError ( message = f"Cannot parse the image { path } , too many interpretations { matches } " )
208209 else :
209- raise ImageParseError ( f"Cannot parse the image { path } " )
210+ raise errors . ValidationError ( message = f"Cannot parse the image { path } " )
210211
211212 def repo_api (self ) -> ImageRepoDockerAPI :
212213 """Get the docker API from the image."""
0 commit comments