Merge ~twom/launchpad:oci-fix-multi-arch-uploads-vanishing into launchpad:master

Proposed by Tom Wardill
Status: Merged
Approved by: Tom Wardill
Approved revision: 86d8f8a38fe692cf71279f1bb9522e0f7d2818fc
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~twom/launchpad:oci-fix-multi-arch-uploads-vanishing
Merge into: launchpad:master
Diff against target: 157 lines (+98/-9)
2 files modified
lib/lp/oci/model/ociregistryclient.py (+10/-8)
lib/lp/oci/tests/test_ociregistryclient.py (+88/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+405376@code.launchpad.net

Commit message

Upload single-arch to manifest digest, rather than final tag

lp: #1929693

Description of the change

Previously, we were uploading each build as they finished to the tag from the push rule, then uploading the multi-arch manifest as a final step.
If there was latency in the builds for different architectures finishing, this caused flapping over whether an image for an architecture that wasn't built yet existed.

Instead, upload the single-arch manifest to it's 'sha256:xxx' location, then refer to it when we updated the multi-arch manifest.

This ensures that an image will always exist for an architecture that has been built.

To post a comment you must log in.
86d8f8a... by Tom Wardill

Test for uploading to a sha tag

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py
index 98b925e..9672875 100644
--- a/lib/lp/oci/model/ociregistryclient.py
+++ b/lib/lp/oci/model/ociregistryclient.py
@@ -296,6 +296,8 @@ class OCIRegistryClient:
296 # Specifically the Schema 2 manifest.296 # Specifically the Schema 2 manifest.
297 digest = None297 digest = None
298 data = json.dumps(registry_manifest).encode("UTF-8")298 data = json.dumps(registry_manifest).encode("UTF-8")
299 if tag is None:
300 tag = "sha256:{}".format(hashlib.sha256(data).hexdigest())
299 size = len(data)301 size = len(data)
300 content_type = registry_manifest.get(302 content_type = registry_manifest.get(
301 "mediaType",303 "mediaType",
@@ -323,7 +325,8 @@ class OCIRegistryClient:
323325
324 @classmethod326 @classmethod
325 def _upload_to_push_rule(327 def _upload_to_push_rule(
326 cls, push_rule, build, manifest, digests, preloaded_data, tag):328 cls, push_rule, build, manifest, digests, preloaded_data,
329 tag=None):
327 http_client = RegistryHTTPClient.getInstance(push_rule)330 http_client = RegistryHTTPClient.getInstance(push_rule)
328331
329 for section in manifest:332 for section in manifest:
@@ -390,13 +393,12 @@ class OCIRegistryClient:
390 exceptions = []393 exceptions = []
391 try:394 try:
392 for push_rule in build.recipe.push_rules:395 for push_rule in build.recipe.push_rules:
393 for tag in cls._calculateTags(build.recipe):396 try:
394 try:397 cls._upload_to_push_rule(
395 cls._upload_to_push_rule(398 push_rule, build, manifest, digests,
396 push_rule, build, manifest, digests,399 preloaded_data, tag=None)
397 preloaded_data, tag)400 except Exception as e:
398 except Exception as e:401 exceptions.append(e)
399 exceptions.append(e)
400 if len(exceptions) == 1:402 if len(exceptions) == 1:
401 raise exceptions[0]403 raise exceptions[0]
402 elif len(exceptions) > 1:404 elif len(exceptions) > 1:
diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py
index 286bdce..90f4db4 100644
--- a/lib/lp/oci/tests/test_ociregistryclient.py
+++ b/lib/lp/oci/tests/test_ociregistryclient.py
@@ -23,7 +23,6 @@ from requests.exceptions import (
23 HTTPError,23 HTTPError,
24 )24 )
25import responses25import responses
26from tenacity import RetryError
27from testtools.matchers import (26from testtools.matchers import (
28 AfterPreprocessing,27 AfterPreprocessing,
29 ContainsDict,28 ContainsDict,
@@ -216,6 +215,9 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
216215
217 self.client.upload(self.build)216 self.client.upload(self.build)
218217
218 # We should have uploaded to the digest, not the tag
219 self.assertIn('sha256:', responses.calls[1].request.url)
220 self.assertNotIn('edge', responses.calls[1].request.url)
219 request = json.loads(responses.calls[1].request.body.decode("UTF-8"))221 request = json.loads(responses.calls[1].request.body.decode("UTF-8"))
220222
221 self.assertThat(request, MatchesDict({223 self.assertThat(request, MatchesDict({
@@ -1006,6 +1008,91 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
1006 HTTPError, self.client.uploadManifestList,1008 HTTPError, self.client.uploadManifestList,
1007 build_request, [self.build])1009 build_request, [self.build])
10081010
1011 @responses.activate
1012 def test_multi_arch_manifest_with_existing_architectures(self):
1013 """Ensure that an existing arch release does not vanish
1014 while waiting for a new upload."""
1015 current_manifest = {
1016 "schemaVersion": 2,
1017 "mediaType": "application/"
1018 "vnd.docker.distribution.manifest.list.v2+json",
1019 "manifests": [{
1020 "platform": {"os": "linux", "architecture": "386"},
1021 "mediaType": "application/"
1022 "vnd.docker.distribution.manifest.v2+json",
1023 "digest": "initial-386-digest",
1024 "size": 110
1025 }, {
1026 "platform": {"os": "linux", "architecture": "amd64"},
1027 "mediaType": "application/"
1028 "vnd.docker.distribution.manifest.v2+json",
1029 "digest": "initial-amd64-digest",
1030 "size": 220
1031 }]
1032 }
1033
1034 recipe = self.factory.makeOCIRecipe(git_ref=self.git_ref)
1035 distroseries = self.factory.makeDistroSeries(
1036 distribution=recipe.distribution, status=SeriesStatus.CURRENT)
1037 for architecturetag, processor_name in (
1038 ("amd64", "amd64"),
1039 ("i386", "386"),
1040 ):
1041 self.factory.makeDistroArchSeries(
1042 distroseries=distroseries, architecturetag=architecturetag,
1043 processor=getUtility(IProcessorSet).getByName(processor_name))
1044 build1 = self.factory.makeOCIRecipeBuild(
1045 recipe=recipe, distro_arch_series=distroseries["amd64"])
1046 build2 = self.factory.makeOCIRecipeBuild(
1047 recipe=recipe, distro_arch_series=distroseries["i386"])
1048
1049 job = mock.Mock()
1050 job.builds = [build1, build2]
1051 job.uploaded_manifests = {
1052 build1.id: {"digest": "new-build1-digest", "size": 1111},
1053 build2.id: {"digest": "new-build2-digest", "size": 2222},
1054 }
1055 job_source = mock.Mock()
1056 job_source.getByOCIRecipeAndID.return_value = job
1057 self.useFixture(
1058 ZopeUtilityFixture(job_source, IOCIRecipeRequestBuildsJobSource))
1059 build_request = OCIRecipeBuildRequest(recipe, -1)
1060
1061 push_rule = self.factory.makeOCIPushRule(recipe=recipe)
1062 responses.add(
1063 "GET", "{}/v2/{}/manifests/v1.0-20.04_edge".format(
1064 push_rule.registry_url, push_rule.image_name),
1065 json=current_manifest,
1066 status=200)
1067 self.addManifestResponses(push_rule, status_code=201)
1068
1069 responses.add(
1070 "GET", "{}/v2/".format(push_rule.registry_url), status=200)
1071 self.addManifestResponses(push_rule, status_code=201)
1072
1073 self.client.uploadManifestList(build_request, [build1])
1074 self.assertEqual(3, len(responses.calls))
1075
1076 # Check that we have the old manifest for 386,
1077 # but the new one for amd64
1078 self.assertEqual({
1079 "schemaVersion": 2,
1080 "mediaType": "application/"
1081 "vnd.docker.distribution.manifest.list.v2+json",
1082 "manifests": [
1083 {
1084 "platform": {"os": "linux", "architecture": "386"},
1085 "mediaType": "application"
1086 "/vnd.docker.distribution.manifest.v2+json",
1087 "digest": "initial-386-digest", "size": 110},
1088 {
1089 "platform": {"os": "linux", "architecture": "amd64"},
1090 "mediaType": "application"
1091 "/vnd.docker.distribution.manifest.v2+json",
1092 "digest": "new-build1-digest", "size": 1111
1093 }]
1094 }, json.loads(responses.calls[2].request.body.decode("UTF-8")))
1095
10091096
1010class TestRegistryHTTPClient(OCIConfigHelperMixin, SpyProxyCallsMixin,1097class TestRegistryHTTPClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
1011 TestCaseWithFactory):1098 TestCaseWithFactory):

Subscribers

People subscribed via source and target branches

to status/vote changes: