Merge ~twom/launchpad:oci-fix-partial-upload-status-being-wrong into launchpad:master

Proposed by Tom Wardill
Status: Merged
Approved by: Tom Wardill
Approved revision: 1115c346550e92addaeacfaef2231831cee168a2
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~twom/launchpad:oci-fix-partial-upload-status-being-wrong
Merge into: launchpad:master
Diff against target: 333 lines (+172/-34)
7 files modified
lib/lp/oci/browser/ocirecipe.py (+1/-1)
lib/lp/oci/browser/tests/test_ocirecipe.py (+1/-0)
lib/lp/oci/interfaces/ocirecipebuild.py (+39/-0)
lib/lp/oci/model/ocirecipejob.py (+45/-20)
lib/lp/oci/templates/ocirecipe-index.pt (+6/-5)
lib/lp/oci/tests/test_ocirecipebuildjob.py (+0/-8)
lib/lp/oci/tests/test_ocirecipejob.py (+80/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Thiago F. Pappacena (community) Approve
Review via email: mp+401809@code.launchpad.net

Commit message

Use a StatusSet for registry upload

Description of the change

We need to keep track of partial upload status in a set of builds.

Do that using a different enum.

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

LGTM

review: Approve
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/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
2index eb62596..bc978e7 100644
3--- a/lib/lp/oci/browser/ocirecipe.py
4+++ b/lib/lp/oci/browser/ocirecipe.py
5@@ -333,7 +333,7 @@ class OCIRecipeView(LaunchpadView):
6 status = removeSecurityProxy(
7 recipe_set.getStatusSummaryForBuilds([build_job]))
8 # Add the registry job status
9- status["upload_scheduled"] = upload_status != unscheduled_upload
10+ status["upload_requested"] = upload_status != unscheduled_upload
11 status["upload"] = upload_status
12 status["date"] = build_job.date
13 status["date_estimated"] = build_job.estimate
14diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
15index b51097e..a4308ea 100644
16--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
17+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
18@@ -1525,6 +1525,7 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
19 When requested
20 When complete
21 There are some builds waiting to be built.
22+ Waiting for builds to start.
23 a moment ago
24 in .* \(estimated\)
25 """, self.getMainText(recipe))
26diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py
27index b9b64ea..22799e7 100644
28--- a/lib/lp/oci/interfaces/ocirecipebuild.py
29+++ b/lib/lp/oci/interfaces/ocirecipebuild.py
30@@ -13,6 +13,7 @@ __all__ = [
31 'IOCIRecipeBuild',
32 'IOCIRecipeBuildSet',
33 'OCIRecipeBuildRegistryUploadStatus',
34+ 'OCIRecipeBuildSetRegistryUploadStatus',
35 ]
36
37 from lazr.enum import (
38@@ -102,6 +103,44 @@ class OCIRecipeBuildRegistryUploadStatus(EnumeratedType):
39 """)
40
41
42+class OCIRecipeBuildSetRegistryUploadStatus(EnumeratedType):
43+ """OCI build registry upload status type
44+
45+ OCI builds may be uploaded to a registry. This represents the state of
46+ that process.
47+ """
48+
49+ UNSCHEDULED = Item("""
50+ Unscheduled
51+
52+ No upload of these OCI builds to a registry is scheduled.
53+ """)
54+
55+ PENDING = Item("""
56+ Pending
57+
58+ These OCI builds are queued for upload to a registry.
59+ """)
60+
61+ FAILEDTOUPLOAD = Item("""
62+ Failed to upload
63+
64+ The last attempt to upload these OCI builds to a registry failed.
65+ """)
66+
67+ UPLOADED = Item("""
68+ Uploaded
69+
70+ These OCI builds were successfully uploaded to a registry.
71+ """)
72+
73+ PARTIAL = Item("""
74+ Partial
75+
76+ Some OCI builds have uploaded to a registry.
77+ """)
78+
79+
80 class IOCIRecipeBuildView(IPackageBuild):
81 """`IOCIRecipeBuild` attributes that require launchpad.View permission."""
82
83diff --git a/lib/lp/oci/model/ocirecipejob.py b/lib/lp/oci/model/ocirecipejob.py
84index ac885d7..dd57542 100644
85--- a/lib/lp/oci/model/ocirecipejob.py
86+++ b/lib/lp/oci/model/ocirecipejob.py
87@@ -33,7 +33,10 @@ from zope.interface import (
88 from zope.security.proxy import removeSecurityProxy
89
90 from lp.app.errors import NotFoundError
91-from lp.oci.interfaces.ocirecipebuild import OCIRecipeBuildRegistryUploadStatus
92+from lp.oci.interfaces.ocirecipebuild import (
93+ OCIRecipeBuildRegistryUploadStatus,
94+ OCIRecipeBuildSetRegistryUploadStatus,
95+ )
96 from lp.oci.interfaces.ocirecipejob import (
97 IOCIRecipeJob,
98 IOCIRecipeRequestBuildsJob,
99@@ -288,41 +291,63 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
100 def addUploadedManifest(self, build_id, manifest_info):
101 self.metadata["uploaded_manifests"][int(build_id)] = manifest_info
102
103+ @property
104 def build_status(self):
105+ builds = self.builds
106 # This just returns a dict, but Zope is really helpful here
107 status = removeSecurityProxy(
108 getUtility(IOCIRecipeSet).getStatusSummaryForBuilds(
109- list(self.builds)))
110+ list(builds)))
111
112 # This has a really long name!
113- statusEnum = OCIRecipeBuildRegistryUploadStatus
114+ singleStatus = OCIRecipeBuildRegistryUploadStatus
115+ setStatus = OCIRecipeBuildSetRegistryUploadStatus
116
117 # Set the pending upload status if either we're not done uploading,
118 # or there was no upload requested in the first place (no push rules)
119 if status['status'] == BuildSetStatus.FULLYBUILT:
120 upload_status = [
121- (x.registry_upload_status == statusEnum.UPLOADED or
122- x.registry_upload_status == statusEnum.UNSCHEDULED)
123+ (x.registry_upload_status == singleStatus.UPLOADED or
124+ x.registry_upload_status == singleStatus.UNSCHEDULED)
125 for x in status['builds']]
126 if not all(upload_status):
127 status['status'] = BuildSetStatus.FULLYBUILT_PENDING
128
129- # Add a flag for if we're expecting a registry upload
130- status['upload_scheduled'] = any(
131- x.registry_upload_status != statusEnum.UNSCHEDULED
132- for x in status['builds'])
133-
134- # Set the equivalent of BuildSetStatus, but for registry upload
135- # If any of the builds have failed to upload
136- if any(x.registry_upload_status == statusEnum.FAILEDTOUPLOAD
137- for x in status['builds']):
138- status['upload'] = statusEnum.FAILEDTOUPLOAD
139- # If any of the builds are still waiting to upload
140- elif any(x.registry_upload_status == statusEnum.PENDING
141- for x in status['builds']):
142- status['upload'] = statusEnum.PENDING
143+ # Are we expecting an upload to be or to have been attempted?
144+ # This is slightly complicated as the upload depends on the push
145+ # rules at the time of build completion
146+ upload_requested = False
147+ # If there's an upload job for any of the builds, we have
148+ # requested an upload
149+ if any(x.last_registry_upload_job for x in builds):
150+ upload_requested = True
151+ # If all of the builds haven't finished, but the recipe currently
152+ # has push rules specified, then we will attempt an upload
153+ # in the future
154+ if any(not x.date_finished and x.recipe.can_upload_to_registry
155+ for x in builds):
156+ upload_requested = True
157+ status['upload_requested'] = upload_requested
158+
159+ # Convert the set of registry statuses into a single line
160+ # for display
161+ upload_status = [x.registry_upload_status for x in builds]
162+ # Any of the builds failed
163+ if any(x == singleStatus.FAILEDTOUPLOAD for x in upload_status):
164+ status['upload'] = setStatus.FAILEDTOUPLOAD
165+ # All of the builds uploaded
166+ elif all(x == singleStatus.UPLOADED for x in upload_status):
167+ status['upload'] = setStatus.UPLOADED
168+ # All of the builds are yet to attempt an upload
169+ elif all(x == singleStatus.UNSCHEDULED for x in upload_status):
170+ status['upload'] = setStatus.UNSCHEDULED
171+ # Any of the builds have uploaded. Set after 'all of the builds'
172+ # have uploaded.
173+ elif any(x == singleStatus.UPLOADED for x in upload_status):
174+ status['upload'] = setStatus.PARTIAL
175+ # And if it's none of the above, we're waiting
176 else:
177- status['upload'] = statusEnum.UPLOADED
178+ status['upload'] = setStatus.PENDING
179
180 # Get the longest date and whether any of them are estimated
181 # for the summary of the builds
182diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt
183index 569ea43..e33952a 100644
184--- a/lib/lp/oci/templates/ocirecipe-index.pt
185+++ b/lib/lp/oci/templates/ocirecipe-index.pt
186@@ -145,12 +145,13 @@
187
188 </td>
189 <td>
190- <tal:registry-upload tal:condition="build_status/upload_scheduled">
191+
192+ <tal:registry-upload tal:condition="build_status/upload_requested">
193 <span tal:content="build_status/upload/title" />
194 </tal:registry-upload>
195- <tal:registry-upload tal:condition="not:build_status/upload_scheduled">
196- <span tal:condition="python: 'FULLYBUILT' in build_status['status'].title">No registry upload requested.</span>
197- <span tal:condition="python: 'FAILEDTOBUILD' in build_status['status'].title">No registry upload requested.</span>
198+ <tal:registry-upload tal:condition="not:build_status/upload_requested">
199+ <span tal:condition="build_status/status/enumvalue:NEEDSBUILD">Waiting for builds to start.</span>
200+ <span tal:condition="not: build_status/status/enumvalue:NEEDSBUILD">No registry upload requested.</span>
201 </tal:registry-upload>
202 </td>
203 <td>
204@@ -183,7 +184,7 @@
205 </ul>
206 </td>
207 <td>
208- <ul tal:condition="build_status/upload_scheduled" tal:repeat="build build_request/builds">
209+ <ul tal:condition="build_status/upload_requested" tal:repeat="build build_request/builds">
210 <li style="padding-left: 22px;">
211 <strong><a class="sprite distribution"
212 tal:define="processor build/processor"
213diff --git a/lib/lp/oci/tests/test_ocirecipebuildjob.py b/lib/lp/oci/tests/test_ocirecipebuildjob.py
214index 0ff2938..95718bb 100644
215--- a/lib/lp/oci/tests/test_ocirecipebuildjob.py
216+++ b/lib/lp/oci/tests/test_ocirecipebuildjob.py
217@@ -7,14 +7,8 @@ from __future__ import absolute_import, print_function, unicode_literals
218
219 __metaclass__ = type
220
221-from datetime import (
222- datetime,
223- timedelta,
224- )
225 import os
226 import signal
227-import threading
228-import time
229
230 from fixtures import FakeLogger
231 from storm.locals import Store
232@@ -22,7 +16,6 @@ from testtools.matchers import (
233 Equals,
234 MatchesDict,
235 MatchesListwise,
236- MatchesSetwise,
237 MatchesStructure,
238 )
239 import transaction
240@@ -54,7 +47,6 @@ from lp.oci.model.ocirecipebuildjob import (
241 from lp.services.compat import mock
242 from lp.services.config import config
243 from lp.services.database.locking import (
244- AdvisoryLockHeld,
245 LockType,
246 try_advisory_lock,
247 )
248diff --git a/lib/lp/oci/tests/test_ocirecipejob.py b/lib/lp/oci/tests/test_ocirecipejob.py
249new file mode 100644
250index 0000000..962d442
251--- /dev/null
252+++ b/lib/lp/oci/tests/test_ocirecipejob.py
253@@ -0,0 +1,80 @@
254+# Copyright 2021 Canonical Ltd. This software is licensed under the
255+# GNU Affero General Public License version 3 (see the file LICENSE).
256+
257+"""Tests for classes in OCIRecipeJob."""
258+
259+from __future__ import absolute_import, print_function, unicode_literals
260+
261+__metaclass__ = type
262+
263+from zope.component import getUtility
264+from zope.security.proxy import removeSecurityProxy
265+
266+from lp.buildmaster.enums import BuildStatus
267+from lp.buildmaster.interfaces.processor import IProcessorSet
268+from lp.oci.interfaces.ocirecipe import (
269+ OCI_RECIPE_ALLOW_CREATE
270+ )
271+from lp.oci.interfaces.ocirecipebuild import (
272+ OCIRecipeBuildSetRegistryUploadStatus,
273+ )
274+from lp.oci.interfaces.ocirecipebuildjob import IOCIRegistryUploadJobSource
275+from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource
276+from lp.registry.interfaces.series import SeriesStatus
277+from lp.services.features.testing import FeatureFixture
278+from lp.services.job.interfaces.job import JobStatus
279+from lp.testing import (
280+ person_logged_in,
281+ TestCaseWithFactory,
282+ )
283+from lp.testing.layers import DatabaseFunctionalLayer
284+
285+
286+class TestOCIRecipeRequestBuildsJob(TestCaseWithFactory):
287+
288+ layer = DatabaseFunctionalLayer
289+
290+ def setUp(self):
291+ super(TestOCIRecipeRequestBuildsJob, self).setUp()
292+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
293+
294+ def getDistroArchSeries(self, distroseries, proc_name="386",
295+ arch_tag="i386"):
296+ processor = getUtility(IProcessorSet).getByName(proc_name)
297+
298+ das = self.factory.makeDistroArchSeries(
299+ distroseries=distroseries, architecturetag=arch_tag,
300+ processor=processor)
301+ fake_chroot = self.factory.makeLibraryFileAlias(
302+ filename="fake_chroot.tar.gz", db_only=True)
303+ das.addOrUpdateChroot(fake_chroot)
304+ return das
305+
306+ def test_partial_on_failure(self):
307+ ocirecipe = removeSecurityProxy(self.factory.makeOCIRecipe(
308+ require_virtualized=False))
309+ owner = ocirecipe.owner
310+ distro = ocirecipe.oci_project.distribution
311+ series = self.factory.makeDistroSeries(
312+ distribution=distro, status=SeriesStatus.CURRENT)
313+ self.getDistroArchSeries(series, "386", "386")
314+ self.getDistroArchSeries(series, "hppa", "hppa")
315+ job = getUtility(IOCIRecipeRequestBuildsJobSource).create(
316+ ocirecipe, owner)
317+
318+ with person_logged_in(job.requester):
319+ builds = ocirecipe.requestBuildsFromJob(
320+ job.requester, build_request=job.build_request)
321+ removeSecurityProxy(job).builds = builds
322+
323+ builds[0].updateStatus(BuildStatus.FAILEDTOBUILD)
324+ builds[1].updateStatus(BuildStatus.FULLYBUILT)
325+ upload_job = getUtility(IOCIRegistryUploadJobSource).create(builds[1])
326+ removeSecurityProxy(upload_job).job._status = JobStatus.COMPLETED
327+
328+ status = job.build_status
329+
330+ self.assertTrue(status['upload_requested'])
331+ self.assertEqual(
332+ OCIRecipeBuildSetRegistryUploadStatus.PARTIAL,
333+ status['upload'])

Subscribers

People subscribed via source and target branches

to status/vote changes: