Merge ~cjwatson/launchpad:oci-registry-client-fix-exceptions into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: e325dbfb0b688c23923c77ece224073bd9077c5b
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:oci-registry-client-fix-exceptions
Merge into: launchpad:master
Diff against target: 224 lines (+148/-16)
2 files modified
lib/lp/oci/model/ociregistryclient.py (+20/-15)
lib/lp/oci/tests/test_ociregistryclient.py (+128/-1)
Reviewer Review Type Date Requested Status
Tom Wardill (community) Approve
Review via email: mp+384957@code.launchpad.net

Commit message

Fix HTTP error handling in OCIRegistryClient

Description of the change

urlfetch raises 4xx/5xx HTTP errors as exceptions, so we need to account for that when deciding whether to raise BlobUploadFailed or ManifestUploadFailed.

To post a comment you must log in.
Revision history for this message
Tom Wardill (twom) :
review: Needs Fixing
e325dbf... by Colin Watson

Fix handling of non-201 success responses

Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Tom Wardill (twom) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py
2index 771d0cf..f65a986 100644
3--- a/lib/lp/oci/model/ociregistryclient.py
4+++ b/lib/lp/oci/model/ociregistryclient.py
5@@ -97,12 +97,14 @@ class OCIRegistryClient:
6 post_location = post_response.headers["Location"]
7 query_parsed = {"digest": digest}
8
9- put_response = http_client.request(
10- post_location,
11- params=query_parsed,
12- data=fileobj,
13- method="PUT")
14-
15+ try:
16+ put_response = http_client.request(
17+ post_location,
18+ params=query_parsed,
19+ data=fileobj,
20+ method="PUT")
21+ except HTTPError as http_error:
22+ put_response = http_error.response
23 if put_response.status_code != 201:
24 msg = "Upload of {} for {} failed".format(
25 digest, push_rule.image_name)
26@@ -256,15 +258,18 @@ class OCIRegistryClient:
27 preloaded_data[section["Config"]])
28
29 # Upload the registry manifest
30- manifest_response = http_client.requestPath(
31- "/manifests/{}".format(tag),
32- json=registry_manifest,
33- headers={
34- "Content-Type":
35- "application/"
36- "vnd.docker.distribution.manifest.v2+json"
37- },
38- method="PUT")
39+ try:
40+ manifest_response = http_client.requestPath(
41+ "/manifests/{}".format(tag),
42+ json=registry_manifest,
43+ headers={
44+ "Content-Type":
45+ "application/"
46+ "vnd.docker.distribution.manifest.v2+json"
47+ },
48+ method="PUT")
49+ except HTTPError as http_error:
50+ manifest_response = http_error.response
51 if manifest_response.status_code != 201:
52 raise ManifestUploadFailed(
53 "Failed to upload manifest for {} in {}".format(
54diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py
55index e188790..879a562 100644
56--- a/lib/lp/oci/tests/test_ociregistryclient.py
57+++ b/lib/lp/oci/tests/test_ociregistryclient.py
58@@ -7,9 +7,11 @@ from __future__ import absolute_import, print_function, unicode_literals
59
60 __metaclass__ = type
61
62+from functools import partial
63 import json
64 import os
65 import tarfile
66+import uuid
67
68 from fixtures import MockPatch
69 from requests.exceptions import (
70@@ -20,13 +22,20 @@ import responses
71 from six import StringIO
72 from tenacity import RetryError
73 from testtools.matchers import (
74+ AfterPreprocessing,
75 Equals,
76 MatchesDict,
77+ MatchesException,
78 MatchesListwise,
79+ Raises,
80 )
81 import transaction
82 from zope.security.proxy import removeSecurityProxy
83
84+from lp.oci.interfaces.ociregistryclient import (
85+ BlobUploadFailed,
86+ ManifestUploadFailed,
87+ )
88 from lp.oci.model.ociregistryclient import (
89 BearerTokenRegistryClient,
90 OCIRegistryAuthenticationError,
91@@ -274,7 +283,7 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
92 self.assertNotIn('Authorization', call.request.headers.keys())
93
94 @responses.activate
95- def test_upload_raises_non_404(self):
96+ def test_upload_check_existing_raises_non_404(self):
97 push_rule = self.build.recipe.push_rules[0]
98 http_client = RegistryHTTPClient(push_rule)
99 blobs_url = "{}/blobs/{}".format(
100@@ -336,6 +345,124 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
101 5, self.client._upload.retry.statistics["attempt_number"])
102 self.assertEqual(5, self.retry_count)
103
104+ @responses.activate
105+ def test_upload_put_blob_raises_error(self):
106+ push_rule = self.build.recipe.push_rules[0]
107+ http_client = RegistryHTTPClient(push_rule)
108+ blobs_url = "{}/blobs/{}".format(
109+ http_client.api_url, "test-digest")
110+ uploads_url = "{}/blobs/uploads/".format(http_client.api_url)
111+ upload_url = "{}/blobs/uploads/{}".format(
112+ http_client.api_url, uuid.uuid4())
113+ put_errors = [
114+ {
115+ "code": "BLOB_UPLOAD_INVALID",
116+ "message": "blob upload invalid",
117+ "detail": [],
118+ },
119+ ]
120+ responses.add("HEAD", blobs_url, status=404)
121+ responses.add("POST", uploads_url, headers={"Location": upload_url})
122+ responses.add(
123+ "PUT", upload_url, status=400, json={"errors": put_errors})
124+ self.assertThat(
125+ partial(
126+ self.client._upload,
127+ "test-digest", push_rule, None, http_client),
128+ Raises(MatchesException(
129+ BlobUploadFailed,
130+ AfterPreprocessing(
131+ str,
132+ Equals(
133+ "Upload of {} for {} failed".format(
134+ "test-digest", push_rule.image_name))))))
135+
136+ @responses.activate
137+ def test_upload_put_blob_raises_non_201_success(self):
138+ push_rule = self.build.recipe.push_rules[0]
139+ http_client = RegistryHTTPClient(push_rule)
140+ blobs_url = "{}/blobs/{}".format(
141+ http_client.api_url, "test-digest")
142+ uploads_url = "{}/blobs/uploads/".format(http_client.api_url)
143+ upload_url = "{}/blobs/uploads/{}".format(
144+ http_client.api_url, uuid.uuid4())
145+ responses.add("HEAD", blobs_url, status=404)
146+ responses.add("POST", uploads_url, headers={"Location": upload_url})
147+ responses.add("PUT", upload_url, status=200)
148+ self.assertThat(
149+ partial(
150+ self.client._upload,
151+ "test-digest", push_rule, None, http_client),
152+ Raises(MatchesException(
153+ BlobUploadFailed,
154+ AfterPreprocessing(
155+ str,
156+ Equals(
157+ "Upload of {} for {} failed".format(
158+ "test-digest", push_rule.image_name))))))
159+
160+ @responses.activate
161+ def test_upload_put_manifest_raises_error(self):
162+ self._makeFiles()
163+ self.useFixture(MockPatch(
164+ "lp.oci.model.ociregistryclient.OCIRegistryClient._upload"))
165+ self.useFixture(MockPatch(
166+ "lp.oci.model.ociregistryclient.OCIRegistryClient._upload_layer"))
167+
168+ push_rule = self.build.recipe.push_rules[0]
169+ responses.add(
170+ "GET", "{}/v2/".format(push_rule.registry_url), status=200)
171+
172+ manifests_url = "{}/v2/{}/manifests/edge".format(
173+ push_rule.registry_credentials.url,
174+ push_rule.image_name)
175+ put_errors = [
176+ {
177+ "code": "MANIFEST_INVALID",
178+ "message": "manifest invalid",
179+ "detail": [],
180+ },
181+ ]
182+ responses.add(
183+ "PUT", manifests_url, status=400, json={"errors": put_errors})
184+
185+ self.assertThat(
186+ partial(self.client.upload, self.build),
187+ Raises(MatchesException(
188+ ManifestUploadFailed,
189+ AfterPreprocessing(
190+ str,
191+ Equals(
192+ "Failed to upload manifest for {} in {}".format(
193+ self.build.recipe.name, self.build.id))))))
194+
195+ @responses.activate
196+ def test_upload_put_manifest_raises_non_201_success(self):
197+ self._makeFiles()
198+ self.useFixture(MockPatch(
199+ "lp.oci.model.ociregistryclient.OCIRegistryClient._upload"))
200+ self.useFixture(MockPatch(
201+ "lp.oci.model.ociregistryclient.OCIRegistryClient._upload_layer"))
202+
203+ push_rule = self.build.recipe.push_rules[0]
204+ responses.add(
205+ "GET", "{}/v2/".format(push_rule.registry_url), status=200)
206+
207+ manifests_url = "{}/v2/{}/manifests/edge".format(
208+ push_rule.registry_credentials.url,
209+ push_rule.image_name)
210+ responses.add("PUT", manifests_url, status=200)
211+
212+ self.assertThat(
213+ partial(self.client.upload, self.build),
214+ Raises(MatchesException(
215+ ManifestUploadFailed,
216+ AfterPreprocessing(
217+ str,
218+ Equals(
219+ "Failed to upload manifest for {} in {}".format(
220+ self.build.recipe.name, self.build.id))))))
221+
222
223 class TestRegistryHTTPClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
224 TestCaseWithFactory):

Subscribers

People subscribed via source and target branches

to status/vote changes: