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
1diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py
2index 98b925e..9672875 100644
3--- a/lib/lp/oci/model/ociregistryclient.py
4+++ b/lib/lp/oci/model/ociregistryclient.py
5@@ -296,6 +296,8 @@ class OCIRegistryClient:
6 # Specifically the Schema 2 manifest.
7 digest = None
8 data = json.dumps(registry_manifest).encode("UTF-8")
9+ if tag is None:
10+ tag = "sha256:{}".format(hashlib.sha256(data).hexdigest())
11 size = len(data)
12 content_type = registry_manifest.get(
13 "mediaType",
14@@ -323,7 +325,8 @@ class OCIRegistryClient:
15
16 @classmethod
17 def _upload_to_push_rule(
18- cls, push_rule, build, manifest, digests, preloaded_data, tag):
19+ cls, push_rule, build, manifest, digests, preloaded_data,
20+ tag=None):
21 http_client = RegistryHTTPClient.getInstance(push_rule)
22
23 for section in manifest:
24@@ -390,13 +393,12 @@ class OCIRegistryClient:
25 exceptions = []
26 try:
27 for push_rule in build.recipe.push_rules:
28- for tag in cls._calculateTags(build.recipe):
29- try:
30- cls._upload_to_push_rule(
31- push_rule, build, manifest, digests,
32- preloaded_data, tag)
33- except Exception as e:
34- exceptions.append(e)
35+ try:
36+ cls._upload_to_push_rule(
37+ push_rule, build, manifest, digests,
38+ preloaded_data, tag=None)
39+ except Exception as e:
40+ exceptions.append(e)
41 if len(exceptions) == 1:
42 raise exceptions[0]
43 elif len(exceptions) > 1:
44diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py
45index 286bdce..90f4db4 100644
46--- a/lib/lp/oci/tests/test_ociregistryclient.py
47+++ b/lib/lp/oci/tests/test_ociregistryclient.py
48@@ -23,7 +23,6 @@ from requests.exceptions import (
49 HTTPError,
50 )
51 import responses
52-from tenacity import RetryError
53 from testtools.matchers import (
54 AfterPreprocessing,
55 ContainsDict,
56@@ -216,6 +215,9 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
57
58 self.client.upload(self.build)
59
60+ # We should have uploaded to the digest, not the tag
61+ self.assertIn('sha256:', responses.calls[1].request.url)
62+ self.assertNotIn('edge', responses.calls[1].request.url)
63 request = json.loads(responses.calls[1].request.body.decode("UTF-8"))
64
65 self.assertThat(request, MatchesDict({
66@@ -1006,6 +1008,91 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
67 HTTPError, self.client.uploadManifestList,
68 build_request, [self.build])
69
70+ @responses.activate
71+ def test_multi_arch_manifest_with_existing_architectures(self):
72+ """Ensure that an existing arch release does not vanish
73+ while waiting for a new upload."""
74+ current_manifest = {
75+ "schemaVersion": 2,
76+ "mediaType": "application/"
77+ "vnd.docker.distribution.manifest.list.v2+json",
78+ "manifests": [{
79+ "platform": {"os": "linux", "architecture": "386"},
80+ "mediaType": "application/"
81+ "vnd.docker.distribution.manifest.v2+json",
82+ "digest": "initial-386-digest",
83+ "size": 110
84+ }, {
85+ "platform": {"os": "linux", "architecture": "amd64"},
86+ "mediaType": "application/"
87+ "vnd.docker.distribution.manifest.v2+json",
88+ "digest": "initial-amd64-digest",
89+ "size": 220
90+ }]
91+ }
92+
93+ recipe = self.factory.makeOCIRecipe(git_ref=self.git_ref)
94+ distroseries = self.factory.makeDistroSeries(
95+ distribution=recipe.distribution, status=SeriesStatus.CURRENT)
96+ for architecturetag, processor_name in (
97+ ("amd64", "amd64"),
98+ ("i386", "386"),
99+ ):
100+ self.factory.makeDistroArchSeries(
101+ distroseries=distroseries, architecturetag=architecturetag,
102+ processor=getUtility(IProcessorSet).getByName(processor_name))
103+ build1 = self.factory.makeOCIRecipeBuild(
104+ recipe=recipe, distro_arch_series=distroseries["amd64"])
105+ build2 = self.factory.makeOCIRecipeBuild(
106+ recipe=recipe, distro_arch_series=distroseries["i386"])
107+
108+ job = mock.Mock()
109+ job.builds = [build1, build2]
110+ job.uploaded_manifests = {
111+ build1.id: {"digest": "new-build1-digest", "size": 1111},
112+ build2.id: {"digest": "new-build2-digest", "size": 2222},
113+ }
114+ job_source = mock.Mock()
115+ job_source.getByOCIRecipeAndID.return_value = job
116+ self.useFixture(
117+ ZopeUtilityFixture(job_source, IOCIRecipeRequestBuildsJobSource))
118+ build_request = OCIRecipeBuildRequest(recipe, -1)
119+
120+ push_rule = self.factory.makeOCIPushRule(recipe=recipe)
121+ responses.add(
122+ "GET", "{}/v2/{}/manifests/v1.0-20.04_edge".format(
123+ push_rule.registry_url, push_rule.image_name),
124+ json=current_manifest,
125+ status=200)
126+ self.addManifestResponses(push_rule, status_code=201)
127+
128+ responses.add(
129+ "GET", "{}/v2/".format(push_rule.registry_url), status=200)
130+ self.addManifestResponses(push_rule, status_code=201)
131+
132+ self.client.uploadManifestList(build_request, [build1])
133+ self.assertEqual(3, len(responses.calls))
134+
135+ # Check that we have the old manifest for 386,
136+ # but the new one for amd64
137+ self.assertEqual({
138+ "schemaVersion": 2,
139+ "mediaType": "application/"
140+ "vnd.docker.distribution.manifest.list.v2+json",
141+ "manifests": [
142+ {
143+ "platform": {"os": "linux", "architecture": "386"},
144+ "mediaType": "application"
145+ "/vnd.docker.distribution.manifest.v2+json",
146+ "digest": "initial-386-digest", "size": 110},
147+ {
148+ "platform": {"os": "linux", "architecture": "amd64"},
149+ "mediaType": "application"
150+ "/vnd.docker.distribution.manifest.v2+json",
151+ "digest": "new-build1-digest", "size": 1111
152+ }]
153+ }, json.loads(responses.calls[2].request.body.decode("UTF-8")))
154+
155
156 class TestRegistryHTTPClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
157 TestCaseWithFactory):

Subscribers

People subscribed via source and target branches

to status/vote changes: