Merge ~twom/launchpad:oci-add-registry-status-to-recipe-page into launchpad:master
- Git
- lp:~twom/launchpad
- oci-add-registry-status-to-recipe-page
- Merge into master
Proposed by
Tom Wardill
Status: | Merged |
---|---|
Approved by: | Tom Wardill |
Approved revision: | 6e92e27b324b7ef525f5c94ce934b287152fbec7 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~twom/launchpad:oci-add-registry-status-to-recipe-page |
Merge into: | launchpad:master |
Diff against target: |
745 lines (+425/-76) 7 files modified
lib/lp/oci/browser/ocirecipe.py (+52/-1) lib/lp/oci/browser/tests/test_ocirecipe.py (+120/-38) lib/lp/oci/interfaces/ocirecipe.py (+31/-0) lib/lp/oci/interfaces/ocirecipejob.py (+3/-0) lib/lp/oci/model/ocirecipe.py (+61/-0) lib/lp/oci/model/ocirecipejob.py (+65/-3) lib/lp/oci/templates/ocirecipe-index.pt (+93/-34) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Review via email: mp+401144@code.launchpad.net |
Commit message
Add registry upload status to OCIRecipe:+index
Description of the change
The status of the registry upload for an OCIRecipeBuild was hidden on the build page. The upload could fail, while the build succeeds, leading the +index to confusingly say that everything had succeeded.
Rebuild +index to have a table based on OCIRequestBuildJob, listing the individual builds for a request and their upload status.
Some recipes are old enough to have builds that do not use the async requestBuilds, so munge those into something that looks like a request with a single build purely for display purposes.
This is based on the BuildSetStatus enum, with some later enhancements for the data to be aware of a set of Registry Uploads, as well as Builds.
To post a comment you must log in.
Revision history for this message
Tom Wardill (twom) wrote : | # |
Revision history for this message
Ioana Lasc (ilasc) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py |
2 | index 21dad88..f55bf1d 100644 |
3 | --- a/lib/lp/oci/browser/ocirecipe.py |
4 | +++ b/lib/lp/oci/browser/ocirecipe.py |
5 | @@ -43,6 +43,7 @@ from zope.schema import ( |
6 | ValidationError, |
7 | ) |
8 | from zope.security.interfaces import Unauthorized |
9 | +from zope.security.proxy import removeSecurityProxy |
10 | |
11 | from lp.app.browser.launchpadform import ( |
12 | action, |
13 | @@ -75,7 +76,11 @@ from lp.oci.interfaces.ocirecipe import ( |
14 | OCI_RECIPE_WEBHOOKS_FEATURE_FLAG, |
15 | OCIRecipeFeatureDisabled, |
16 | ) |
17 | -from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet |
18 | +from lp.oci.interfaces.ocirecipebuild import ( |
19 | + IOCIRecipeBuildSet, |
20 | + OCIRecipeBuildRegistryUploadStatus, |
21 | + ) |
22 | +from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource |
23 | from lp.oci.interfaces.ociregistrycredentials import ( |
24 | IOCIRegistryCredentialsSet, |
25 | OCIRegistryCredentialsAlreadyExist, |
26 | @@ -101,6 +106,7 @@ from lp.services.webapp.breadcrumb import NameBreadcrumb |
27 | from lp.services.webhooks.browser import WebhookTargetNavigationMixin |
28 | from lp.soyuz.browser.archive import EnableProcessorsMixin |
29 | from lp.soyuz.browser.build import get_build_by_id_str |
30 | +from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus |
31 | |
32 | |
33 | class OCIRecipeNavigation(WebhookTargetNavigationMixin, Navigation): |
34 | @@ -308,6 +314,51 @@ class OCIRecipeView(LaunchpadView): |
35 | distro = oci_project.distribution |
36 | return bool(distro and distro.oci_registry_credentials) |
37 | |
38 | + def getImageForStatus(self, status): |
39 | + image_map = { |
40 | + BuildSetStatus.NEEDSBUILD: '/@@/build-needed', |
41 | + BuildSetStatus.FULLYBUILT_PENDING: '/@@/build-success-publishing', |
42 | + BuildSetStatus.FAILEDTOBUILD: '/@@/no', |
43 | + BuildSetStatus.BUILDING: '/@@/processing', |
44 | + } |
45 | + return image_map.get( |
46 | + status, '/@@/yes') |
47 | + |
48 | + def _convertBuildJobToStatus(self, build_job): |
49 | + recipe_set = getUtility(IOCIRecipeSet) |
50 | + unscheduled_upload = OCIRecipeBuildRegistryUploadStatus.UNSCHEDULED |
51 | + upload_status = build_job.registry_upload_status |
52 | + # This is just a dict, but zope wraps it as RecipeSet is secured |
53 | + status = removeSecurityProxy( |
54 | + recipe_set.getStatusSummaryForBuilds([build_job])) |
55 | + # Add the registry job status |
56 | + status["upload_scheduled"] = upload_status != unscheduled_upload |
57 | + status["upload"] = upload_status |
58 | + status["date"] = build_job.date |
59 | + status["date_estimated"] = build_job.estimate |
60 | + return { |
61 | + "builds": [build_job], |
62 | + "job_id": "build{}".format(build_job.id), |
63 | + "date_created": build_job.date_created, |
64 | + "date_finished": build_job.date_finished, |
65 | + "build_status": status |
66 | + } |
67 | + |
68 | + def build_requests(self): |
69 | + req_util = getUtility(IOCIRecipeRequestBuildsJobSource) |
70 | + build_requests = list(req_util.findByOCIRecipe(self.context)[:10]) |
71 | + |
72 | + # It's possible that some recipes have builds that are older |
73 | + # than the introduction of the async requestBuilds. |
74 | + # In that case, convert the single build to a fake 'request build job' |
75 | + # that has a single attached build. |
76 | + if len(build_requests) < 10: |
77 | + recipe = self.context |
78 | + no_request_builds = recipe.completed_builds_without_build_request |
79 | + for build in no_request_builds[:10 - len(build_requests)]: |
80 | + build_requests.append(self._convertBuildJobToStatus(build)) |
81 | + return build_requests[:10] |
82 | + |
83 | |
84 | def builds_for_recipe(recipe): |
85 | """A list of interesting builds. |
86 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py |
87 | index 7049d9c..54d4b10 100644 |
88 | --- a/lib/lp/oci/browser/tests/test_ocirecipe.py |
89 | +++ b/lib/lp/oci/browser/tests/test_ocirecipe.py |
90 | @@ -1255,25 +1255,53 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): |
91 | "OCI recipe breadcrumb", "li", |
92 | text=re.compile(r"\srecipe-name\s"))))) |
93 | |
94 | + def makeRecipe(self, processor_names, **kwargs): |
95 | + recipe = self.factory.makeOCIRecipe(**kwargs) |
96 | + processors_list = [] |
97 | + distroseries = self.factory.makeDistroSeries( |
98 | + distribution=recipe.oci_project.distribution) |
99 | + for proc_name in processor_names: |
100 | + proc = getUtility(IProcessorSet).getByName(proc_name) |
101 | + distro = self.factory.makeDistroArchSeries( |
102 | + distroseries=distroseries, architecturetag=proc_name, |
103 | + processor=proc) |
104 | + distro.addOrUpdateChroot(self.factory.makeLibraryFileAlias()) |
105 | + processors_list.append(proc) |
106 | + recipe.setProcessors(processors_list) |
107 | + return recipe |
108 | + |
109 | def test_index(self): |
110 | oci_project = self.factory.makeOCIProject( |
111 | pillar=self.distroseries.distribution) |
112 | - oci_project_name = oci_project.name |
113 | oci_project_display = oci_project.display_name |
114 | [ref] = self.factory.makeGitRefs( |
115 | owner=self.person, target=self.person, name="recipe-repository", |
116 | paths=["refs/heads/master"]) |
117 | - recipe = self.makeOCIRecipe( |
118 | - oci_project=oci_project, git_ref=ref, build_file="Dockerfile") |
119 | + recipe = self.makeRecipe( |
120 | + processor_names=["amd64", "386"], |
121 | + build_file="Dockerfile", git_ref=ref, |
122 | + oci_project=oci_project, registrant=self.person, owner=self.person) |
123 | + build_request = recipe.requestBuilds(self.person) |
124 | + builds = recipe.requestBuildsFromJob(self.person, build_request) |
125 | + job = removeSecurityProxy(build_request).job |
126 | + removeSecurityProxy(job).builds = builds |
127 | + |
128 | + for build in builds: |
129 | + removeSecurityProxy(build).updateStatus( |
130 | + BuildStatus.BUILDING, builder=None, |
131 | + date_started=build.date_created) |
132 | + removeSecurityProxy(build).updateStatus( |
133 | + BuildStatus.FULLYBUILT, builder=None, |
134 | + date_finished=build.date_started + timedelta(minutes=30)) |
135 | + |
136 | + # We also need to account for builds that don't have a build_request |
137 | build = self.makeBuild( |
138 | recipe=recipe, status=BuildStatus.FULLYBUILT, |
139 | duration=timedelta(minutes=30)) |
140 | |
141 | - browser = self.getViewBrowser(build.recipe) |
142 | + browser = self.getViewBrowser(build_request.recipe) |
143 | login_person(self.person) |
144 | self.assertTextMatchesExpressionIgnoreWhitespace("""\ |
145 | - %s OCI project |
146 | - recipe-name |
147 | .* |
148 | OCI recipe information |
149 | Owner: Test Person |
150 | @@ -1285,9 +1313,33 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): |
151 | Official recipe: |
152 | No |
153 | Latest builds |
154 | - Status When complete Architecture |
155 | - Successfully built 30 minutes ago 386 |
156 | - """ % (oci_project_name, oci_project_display, recipe.build_path), |
157 | + Build status |
158 | + Upload status |
159 | + When requested |
160 | + When complete |
161 | + All builds were built successfully. |
162 | + No registry upload requested. |
163 | + a moment ago |
164 | + in 29 minutes |
165 | + amd64 |
166 | + Successfully built |
167 | + 386 |
168 | + Successfully built |
169 | + amd64 |
170 | + in 29 minutes |
171 | + 386 |
172 | + in 29 minutes |
173 | + All builds were built successfully. |
174 | + No registry upload requested. |
175 | + 1 hour ago |
176 | + 30 minutes ago |
177 | + 386 |
178 | + Successfully built |
179 | + 386 |
180 | + 30 minutes ago |
181 | + Recipe push rules |
182 | + This OCI recipe has no push rules defined yet. |
183 | + """ % (oci_project_display, recipe.build_path), |
184 | extract_text(find_main_content(browser.contents))) |
185 | |
186 | # Check portlet on side menu. |
187 | @@ -1346,8 +1398,18 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): |
188 | Official recipe: |
189 | No |
190 | Latest builds |
191 | - Status When complete Architecture |
192 | - Successfully built 30 minutes ago 386 |
193 | + Build status |
194 | + Upload status |
195 | + When requested |
196 | + When complete |
197 | + All builds were built successfully. |
198 | + No registry upload requested. |
199 | + 1 hour ago |
200 | + 30 minutes ago |
201 | + 386 |
202 | + Successfully built |
203 | + 386 |
204 | + 30 minutes ago |
205 | """ % (oci_project_name, oci_project_display, build_path), |
206 | self.getMainText(build.recipe)) |
207 | |
208 | @@ -1389,8 +1451,18 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): |
209 | Official recipe: |
210 | No |
211 | Latest builds |
212 | - Status When complete Architecture |
213 | - Successfully built 30 minutes ago 386 |
214 | + Build status |
215 | + Upload status |
216 | + When requested |
217 | + When complete |
218 | + All builds were built successfully. |
219 | + No registry upload requested. |
220 | + 1 hour ago |
221 | + 30 minutes ago |
222 | + 386 |
223 | + Successfully built |
224 | + 386 |
225 | + 30 minutes ago |
226 | """ % (oci_project_name, oci_project_display, build_path), |
227 | main_text) |
228 | |
229 | @@ -1401,8 +1473,20 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): |
230 | build.setLog(self.factory.makeLibraryFileAlias()) |
231 | self.assertTextMatchesExpressionIgnoreWhitespace(r"""\ |
232 | Latest builds |
233 | - Status When complete Architecture |
234 | - Successfully built 30 minutes ago buildlog \(.*\) 386 |
235 | + Build status |
236 | + Upload status |
237 | + When requested |
238 | + When complete |
239 | + All builds were built successfully. |
240 | + No registry upload requested. |
241 | + 1 hour ago |
242 | + 30 minutes ago |
243 | + 386 |
244 | + buildlog |
245 | + \(.*\) |
246 | + Successfully built |
247 | + 386 |
248 | + 30 minutes ago |
249 | """, self.getMainText(build.recipe)) |
250 | |
251 | def test_index_no_builds(self): |
252 | @@ -1414,13 +1498,29 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): |
253 | |
254 | def test_index_pending_build(self): |
255 | # A pending build is listed as such. |
256 | - build = self.makeBuild() |
257 | - build.queueBuild() |
258 | + oci_project = self.factory.makeOCIProject( |
259 | + pillar=self.distroseries.distribution) |
260 | + [ref] = self.factory.makeGitRefs( |
261 | + owner=self.person, target=self.person, name="recipe-repository", |
262 | + paths=["refs/heads/master"]) |
263 | + recipe = self.makeRecipe( |
264 | + processor_names=["amd64", "386"], |
265 | + build_file="Dockerfile", git_ref=ref, |
266 | + oci_project=oci_project, registrant=self.person, owner=self.person) |
267 | + build_request = recipe.requestBuilds(self.person) |
268 | + builds = recipe.requestBuildsFromJob(self.person, build_request) |
269 | + job = removeSecurityProxy(build_request).job |
270 | + removeSecurityProxy(job).builds = builds |
271 | self.assertTextMatchesExpressionIgnoreWhitespace(r"""\ |
272 | Latest builds |
273 | - Status When complete Architecture |
274 | - Needs building in .* \(estimated\) 386 |
275 | - """, self.getMainText(build.recipe)) |
276 | + Build status |
277 | + Upload status |
278 | + When requested |
279 | + When complete |
280 | + There are some builds waiting to be built. |
281 | + a moment ago |
282 | + in .* \(estimated\) |
283 | + """, self.getMainText(recipe)) |
284 | |
285 | def test_index_request_builds_link(self): |
286 | # Recipe owners get a link to allow requesting builds. |
287 | @@ -1531,24 +1631,6 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView): |
288 | with dbuser(config.IOCIRecipeRequestBuildsJobSource.dbuser): |
289 | JobRunner(jobs).runAll() |
290 | |
291 | - def test_pending_build_requests_not_shown_if_absent(self): |
292 | - self.recipe.requestBuilds(self.recipe.owner) |
293 | - browser = self.getViewBrowser(self.recipe, user=self.person) |
294 | - content = extract_text(find_main_content(browser.contents)) |
295 | - self.assertIn( |
296 | - "You have 1 pending build request. " |
297 | - "The builds should start automatically soon.", |
298 | - content.replace("\n", " ")) |
299 | - |
300 | - # Run the build request, so we don't have any pending one. The |
301 | - # message should be gone then. |
302 | - self.runRequestBuildJobs() |
303 | - browser = self.getViewBrowser(self.recipe, user=self.person) |
304 | - content = extract_text(find_main_content(browser.contents)) |
305 | - self.assertNotIn( |
306 | - "The builds should start automatically soon.", |
307 | - content.replace("\n", " ")) |
308 | - |
309 | def test_request_builds_action(self): |
310 | # Requesting a build creates pending builds. |
311 | browser = self.getViewBrowser( |
312 | diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py |
313 | index ae682ed..1cd81dd 100644 |
314 | --- a/lib/lp/oci/interfaces/ocirecipe.py |
315 | +++ b/lib/lp/oci/interfaces/ocirecipe.py |
316 | @@ -280,6 +280,15 @@ class IOCIRecipeView(Interface): |
317 | # Really IOCIRecipeBuild, patched in _schema_circular_imports. |
318 | value_type=Reference(schema=Interface), readonly=True) |
319 | |
320 | + completed_builds_without_build_request = CollectionField( |
321 | + title=_("Completed builds of this OCI recipe."), |
322 | + description=_( |
323 | + "Completed builds of this OCI recipe, sorted in descending " |
324 | + "order of finishing that do no have a corresponding " |
325 | + "build request"), |
326 | + # Really IOCIRecipeBuild, patched in _schema_circular_imports. |
327 | + value_type=Reference(schema=Interface), readonly=True) |
328 | + |
329 | pending_builds = CollectionField( |
330 | title=_("Pending builds of this OCI recipe."), |
331 | description=_( |
332 | @@ -586,3 +595,25 @@ class IOCIRecipeSet(Interface): |
333 | After this, any OCI recipes that previously used this repository |
334 | will have no source and so cannot dispatch new builds. |
335 | """ |
336 | + |
337 | + def getStatusSummaryForBuilds(builds): |
338 | + """Return a summary of the build status for the given builds. |
339 | + |
340 | + The returned summary includes a status, a description of |
341 | + that status and the builds related to the status. |
342 | + |
343 | + :param builds: A list of build records. |
344 | + :type builds: ``list`` |
345 | + :return: A dict consisting of the build status summary for the |
346 | + given builds. For example: |
347 | + { |
348 | + 'status': BuildSetStatus.FULLYBUILT, |
349 | + 'builds': [build1, build2] |
350 | + } |
351 | + or, an example where there are currently some builds building: |
352 | + { |
353 | + 'status': BuildSetStatus.BUILDING, |
354 | + 'builds':[build3] |
355 | + } |
356 | + :rtype: ``dict``. |
357 | + """ |
358 | diff --git a/lib/lp/oci/interfaces/ocirecipejob.py b/lib/lp/oci/interfaces/ocirecipejob.py |
359 | index 2b59faf..5fac142 100644 |
360 | --- a/lib/lp/oci/interfaces/ocirecipejob.py |
361 | +++ b/lib/lp/oci/interfaces/ocirecipejob.py |
362 | @@ -90,6 +90,9 @@ class IOCIRecipeRequestBuildsJob(IRunnableJob): |
363 | BuildRequest. |
364 | """ |
365 | |
366 | + def build_status(): |
367 | + """Return the status of the builds and the upload to a registry.""" |
368 | + |
369 | |
370 | class IOCIRecipeRequestBuildsJobSource(IJobSource): |
371 | |
372 | diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py |
373 | index a56fa7d..3dcb917 100644 |
374 | --- a/lib/lp/oci/model/ocirecipe.py |
375 | +++ b/lib/lp/oci/model/ocirecipe.py |
376 | @@ -5,6 +5,9 @@ |
377 | |
378 | from __future__ import absolute_import, print_function, unicode_literals |
379 | |
380 | +from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus |
381 | + |
382 | + |
383 | __metaclass__ = type |
384 | __all__ = [ |
385 | 'get_ocirecipe_privacy_filter', |
386 | @@ -732,6 +735,20 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
387 | return self._getBuilds(filter_term, order_by) |
388 | |
389 | @property |
390 | + def completed_builds_without_build_request(self): |
391 | + """See `IOCIRecipe`.""" |
392 | + filter_term = ( |
393 | + Not(OCIRecipeBuild.status.is_in(self._pending_states)), |
394 | + OCIRecipeBuild.build_request_id == None) |
395 | + order_by = ( |
396 | + NullsLast(Desc(Greatest( |
397 | + OCIRecipeBuild.date_started, |
398 | + OCIRecipeBuild.date_finished))), |
399 | + Desc(OCIRecipeBuild.id)) |
400 | + return self._getBuilds(filter_term, order_by) |
401 | + |
402 | + |
403 | + @property |
404 | def pending_builds(self): |
405 | """See `IOCIRecipe`.""" |
406 | filter_term = (OCIRecipeBuild.status.is_in(self._pending_states)) |
407 | @@ -932,6 +949,50 @@ class OCIRecipeSet: |
408 | if git_ref is not None: |
409 | recipe.git_ref = git_ref |
410 | |
411 | + def getStatusSummaryForBuilds(self, builds): |
412 | + # Create a small helper function to collect the builds for a given |
413 | + # list of build states: |
414 | + def collect_builds(*states): |
415 | + wanted = [] |
416 | + for state in states: |
417 | + candidates = [build for build in builds |
418 | + if build.status == state] |
419 | + wanted.extend(candidates) |
420 | + return wanted |
421 | + failed = collect_builds(BuildStatus.FAILEDTOBUILD, |
422 | + BuildStatus.MANUALDEPWAIT, |
423 | + BuildStatus.CHROOTWAIT, |
424 | + BuildStatus.FAILEDTOUPLOAD) |
425 | + needsbuild = collect_builds(BuildStatus.NEEDSBUILD) |
426 | + building = collect_builds(BuildStatus.BUILDING, |
427 | + BuildStatus.UPLOADING) |
428 | + successful = collect_builds(BuildStatus.FULLYBUILT) |
429 | + |
430 | + # Note: the BuildStatus DBItems are used here to summarize the |
431 | + # status of a set of builds:s |
432 | + if len(building) != 0: |
433 | + return { |
434 | + 'status': BuildSetStatus.BUILDING, |
435 | + 'builds': building, |
436 | + } |
437 | + # If there are no builds, this is a 'pending build request' |
438 | + # and needs building |
439 | + elif len(needsbuild) != 0 or len(builds) == 0: |
440 | + return { |
441 | + 'status': BuildSetStatus.NEEDSBUILD, |
442 | + 'builds': needsbuild, |
443 | + } |
444 | + elif len(failed) != 0: |
445 | + return { |
446 | + 'status': BuildSetStatus.FAILEDTOBUILD, |
447 | + 'builds': failed, |
448 | + } |
449 | + else: |
450 | + return { |
451 | + 'status': BuildSetStatus.FULLYBUILT, |
452 | + 'builds': successful, |
453 | + } |
454 | + |
455 | |
456 | @implementer(IOCIRecipeBuildRequest) |
457 | class OCIRecipeBuildRequest: |
458 | diff --git a/lib/lp/oci/model/ocirecipejob.py b/lib/lp/oci/model/ocirecipejob.py |
459 | index 3320cff..ac885d7 100644 |
460 | --- a/lib/lp/oci/model/ocirecipejob.py |
461 | +++ b/lib/lp/oci/model/ocirecipejob.py |
462 | @@ -4,6 +4,9 @@ |
463 | """A build job for OCI Recipe.""" |
464 | |
465 | from __future__ import absolute_import, print_function, unicode_literals |
466 | +from lp.buildmaster.model.processor import Processor |
467 | +from lp.oci.interfaces.ocirecipe import IOCIRecipeSet |
468 | + |
469 | |
470 | __metaclass__ = type |
471 | __all__ = [ |
472 | @@ -27,8 +30,10 @@ from zope.interface import ( |
473 | implementer, |
474 | provider, |
475 | ) |
476 | +from zope.security.proxy import removeSecurityProxy |
477 | |
478 | from lp.app.errors import NotFoundError |
479 | +from lp.oci.interfaces.ocirecipebuild import OCIRecipeBuildRegistryUploadStatus |
480 | from lp.oci.interfaces.ocirecipejob import ( |
481 | IOCIRecipeJob, |
482 | IOCIRecipeRequestBuildsJob, |
483 | @@ -37,6 +42,8 @@ from lp.oci.interfaces.ocirecipejob import ( |
484 | from lp.oci.model.ocirecipebuild import OCIRecipeBuild |
485 | from lp.registry.interfaces.person import IPersonSet |
486 | from lp.services.config import config |
487 | +from lp.services.database.bulk import load_related |
488 | +from lp.services.database.decoratedresultset import DecoratedResultSet |
489 | from lp.services.database.enumcol import EnumCol |
490 | from lp.services.database.interfaces import ( |
491 | IMasterStore, |
492 | @@ -51,6 +58,7 @@ from lp.services.job.runner import BaseRunnableJob |
493 | from lp.services.mail.sendmail import format_address_for_person |
494 | from lp.services.propertycache import cachedproperty |
495 | from lp.services.scripts import log |
496 | +from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus |
497 | |
498 | |
499 | class OCIRecipeJobType(DBEnumeratedType): |
500 | @@ -196,11 +204,17 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived): |
501 | conditions.append(Job._status.is_in(statuses)) |
502 | if job_ids is not None: |
503 | conditions.append(OCIRecipeJob.job_id.is_in(job_ids)) |
504 | - return IStore(OCIRecipeJob).find( |
505 | - (OCIRecipeJob, Job), |
506 | + oci_jobs = IStore(OCIRecipeJob).find( |
507 | + OCIRecipeJob, |
508 | OCIRecipeJob.job_id == Job.id, |
509 | *conditions).order_by(Desc(OCIRecipeJob.job_id)) |
510 | |
511 | + def preload_jobs(rows): |
512 | + load_related(Job, rows, ["job_id"]) |
513 | + |
514 | + return DecoratedResultSet( |
515 | + oci_jobs, lambda oci_job: cls(oci_job), pre_iter_hook=preload_jobs) |
516 | + |
517 | def getOperationDescription(self): |
518 | return "requesting builds of %s" % self.recipe |
519 | |
520 | @@ -244,9 +258,13 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived): |
521 | def builds(self): |
522 | """See `OCIRecipeRequestBuildsJob`.""" |
523 | build_ids = self.metadata.get("builds") |
524 | + # Sort this by architecture/processor name, so it's consistent |
525 | + # when displayed |
526 | if build_ids: |
527 | return IStore(OCIRecipeBuild).find( |
528 | - OCIRecipeBuild, OCIRecipeBuild.id.is_in(build_ids)) |
529 | + OCIRecipeBuild, OCIRecipeBuild.id.is_in(build_ids), |
530 | + OCIRecipeBuild.processor_id == Processor.id).order_by( |
531 | + Desc(Processor.name)) |
532 | else: |
533 | return EmptyResultSet() |
534 | |
535 | @@ -270,6 +288,50 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived): |
536 | def addUploadedManifest(self, build_id, manifest_info): |
537 | self.metadata["uploaded_manifests"][int(build_id)] = manifest_info |
538 | |
539 | + def build_status(self): |
540 | + # This just returns a dict, but Zope is really helpful here |
541 | + status = removeSecurityProxy( |
542 | + getUtility(IOCIRecipeSet).getStatusSummaryForBuilds( |
543 | + list(self.builds))) |
544 | + |
545 | + # This has a really long name! |
546 | + statusEnum = OCIRecipeBuildRegistryUploadStatus |
547 | + |
548 | + # Set the pending upload status if either we're not done uploading, |
549 | + # or there was no upload requested in the first place (no push rules) |
550 | + if status['status'] == BuildSetStatus.FULLYBUILT: |
551 | + upload_status = [ |
552 | + (x.registry_upload_status == statusEnum.UPLOADED or |
553 | + x.registry_upload_status == statusEnum.UNSCHEDULED) |
554 | + for x in status['builds']] |
555 | + if not all(upload_status): |
556 | + status['status'] = BuildSetStatus.FULLYBUILT_PENDING |
557 | + |
558 | + # Add a flag for if we're expecting a registry upload |
559 | + status['upload_scheduled'] = any( |
560 | + x.registry_upload_status != statusEnum.UNSCHEDULED |
561 | + for x in status['builds']) |
562 | + |
563 | + # Set the equivalent of BuildSetStatus, but for registry upload |
564 | + # If any of the builds have failed to upload |
565 | + if any(x.registry_upload_status == statusEnum.FAILEDTOUPLOAD |
566 | + for x in status['builds']): |
567 | + status['upload'] = statusEnum.FAILEDTOUPLOAD |
568 | + # If any of the builds are still waiting to upload |
569 | + elif any(x.registry_upload_status == statusEnum.PENDING |
570 | + for x in status['builds']): |
571 | + status['upload'] = statusEnum.PENDING |
572 | + else: |
573 | + status['upload'] = statusEnum.UPLOADED |
574 | + |
575 | + # Get the longest date and whether any of them are estimated |
576 | + # for the summary of the builds |
577 | + dates = [x.date for x in self.builds if x.date] |
578 | + status['date'] = max(dates) if dates else None |
579 | + status['date_estimated'] = any(x.estimate for x in self.builds) |
580 | + |
581 | + return status |
582 | + |
583 | def run(self): |
584 | """See `IRunnableJob`.""" |
585 | requester = self.requester |
586 | diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt |
587 | index 5a1f3c1..569ea43 100644 |
588 | --- a/lib/lp/oci/templates/ocirecipe-index.pt |
589 | +++ b/lib/lp/oci/templates/ocirecipe-index.pt |
590 | @@ -8,6 +8,27 @@ |
591 | > |
592 | |
593 | <body> |
594 | + |
595 | + <div metal:fill-slot="head_epilogue"> |
596 | + <script type="text/javascript"> |
597 | + LPJS.use('node', 'lp.app.widgets.expander', function(Y) { |
598 | + Y.on('domready', function() { |
599 | + var all_expanders = Y.all('.expander-icon'); |
600 | + all_expanders.each(function(icon) { |
601 | + var base_id = icon.get('id').replace('-expander', ''); |
602 | + console.log(base_id); |
603 | + var content_node = Y.one('#' + base_id); |
604 | + var animate_node = content_node.one('ul'); |
605 | + var expander = new Y.lp.app.widgets.expander.Expander( |
606 | + icon, content_node, { animate_node: animate_node }); |
607 | + expander.setUp(); |
608 | + }); |
609 | + }); |
610 | + }); |
611 | + </script> |
612 | +</div> |
613 | + |
614 | + |
615 | <metal:registering fill-slot="registering"> |
616 | Created by |
617 | <tal:registrant replace="structure context/registrant/fmt:link"/> |
618 | @@ -102,55 +123,93 @@ |
619 | </div> |
620 | |
621 | <h2>Latest builds</h2> |
622 | - <div tal:define="count context/pending_build_requests/count|nothing; |
623 | - plural string: build requests; |
624 | - singular string: build request;" |
625 | - tal:condition="count"> |
626 | - You have <span tal:replace="count" /> pending |
627 | - <tal:plural |
628 | - metal:use-macro="context/@@+base-layout-macros/plural-message"/>. |
629 | - The builds should start automatically soon. |
630 | - </div> |
631 | <table id="latest-builds-listing" class="listing" |
632 | style="margin-bottom: 1em;"> |
633 | <thead> |
634 | <tr> |
635 | - <th>Status</th> |
636 | + <th>Build status</th> |
637 | + <th>Upload status</th> |
638 | + <th>When requested</th> |
639 | <th>When complete</th> |
640 | - <th>Architecture</th> |
641 | </tr> |
642 | </thead> |
643 | <tbody> |
644 | - <tal:recipe-builds repeat="item view/builds"> |
645 | - <tr tal:define="build item" |
646 | - tal:attributes="id string:build-${build/id}"> |
647 | - <td tal:attributes="class string:build_status ${build/status/name}"> |
648 | - <span tal:replace="structure build/image:icon"/> |
649 | - <a tal:content="build/status/title" |
650 | - tal:attributes="href build/fmt:url"/> |
651 | + <tal:build-requests repeat="build_request view/build_requests"> |
652 | + <tr tal:define="build_status build_request/build_status"> |
653 | + <td tal:define="status_img python: view.getImageForStatus(build_status['status'])"> |
654 | + <span tal:attributes="id string:request-${build_request/job_id}-expander" class="expander-icon" tal:condition="python: build_status['status'].name is not 'NEEDSBUILD'"> </span> |
655 | + <img tal:attributes="title build_status/status/description; |
656 | + alt build_status/status/description; |
657 | + src status_img" /> |
658 | + <span tal:content="build_status/status/description" /> |
659 | + |
660 | </td> |
661 | - <td class="datebuilt"> |
662 | - <tal:date replace="build/date/fmt:displaydate"/> |
663 | - <tal:estimate condition="build/estimate"> |
664 | + <td> |
665 | + <tal:registry-upload tal:condition="build_status/upload_scheduled"> |
666 | + <span tal:content="build_status/upload/title" /> |
667 | + </tal:registry-upload> |
668 | + <tal:registry-upload tal:condition="not:build_status/upload_scheduled"> |
669 | + <span tal:condition="python: 'FULLYBUILT' in build_status['status'].title">No registry upload requested.</span> |
670 | + <span tal:condition="python: 'FAILEDTOBUILD' in build_status['status'].title">No registry upload requested.</span> |
671 | + </tal:registry-upload> |
672 | + </td> |
673 | + <td> |
674 | + <span tal:content="build_request/date_created/fmt:displaydate" /> |
675 | + </td> |
676 | + <td> |
677 | + <span tal:content="build_status/date/fmt:displaydate" /> |
678 | + <tal:estimate condition="build_status/date_estimated"> |
679 | (estimated) |
680 | </tal:estimate> |
681 | - |
682 | - <tal:build-log define="file build/log" tal:condition="file"> |
683 | - <a class="sprite download" |
684 | - tal:attributes="href build/log_url">buildlog</a> |
685 | - (<span tal:replace="file/content/filesize/fmt:bytes"/>) |
686 | - </tal:build-log> |
687 | </td> |
688 | + </tr> |
689 | + <tr tal:define="build_status build_request/build_status" tal:attributes="id string:request-${build_request/job_id}" tal:condition="python: build_status['status'].name is not 'NEEDSBUILD'"> |
690 | <td> |
691 | - <!-- XXX cjwatson 2020-02-19: This should show a DAS |
692 | - architecture tag rather than a processor name once we can |
693 | - do that. --> |
694 | - <a class="sprite distribution" |
695 | - tal:define="processor build/processor" |
696 | - tal:content="processor/name"/> |
697 | + <ul tal:repeat="build build_request/builds"> |
698 | + <li style="padding-left: 22px;"> |
699 | + <strong> |
700 | + <a class="sprite distribution" |
701 | + tal:define="processor build/processor" |
702 | + tal:content="processor/name" |
703 | + tal:attributes="href build/fmt:url"/> |
704 | + </strong> |
705 | + <span tal:define="file build/log" tal:condition="file"> |
706 | + <a class="sprite download" |
707 | + tal:attributes="href build/log_url">buildlog</a> |
708 | + (<span tal:replace="file/content/filesize/fmt:bytes"/>) |
709 | + </span> |
710 | + <span tal:content="build/status/title" /> |
711 | + </li> |
712 | + </ul> |
713 | </td> |
714 | + <td> |
715 | + <ul tal:condition="build_status/upload_scheduled" tal:repeat="build build_request/builds"> |
716 | + <li style="padding-left: 22px;"> |
717 | + <strong><a class="sprite distribution" |
718 | + tal:define="processor build/processor" |
719 | + tal:content="processor/name"/></strong> |
720 | + <span tal:content="build/registry_upload_status/title" /> |
721 | + </li> |
722 | + </ul> |
723 | + </td> |
724 | + <td> |
725 | + </td> |
726 | + <td> |
727 | + <ul tal:repeat="build build_request/builds"> |
728 | + <li style="padding-left: 22px;"> |
729 | + <strong><a class="sprite distribution" |
730 | + tal:define="processor build/processor" |
731 | + tal:content="processor/name"/></strong> |
732 | + <span tal:content="build/date/fmt:displaydate" /> |
733 | + <tal:estimate condition="build/estimate"> |
734 | + (estimated) |
735 | + </tal:estimate> |
736 | + </li> |
737 | + </ul> |
738 | + </td> |
739 | + |
740 | </tr> |
741 | - </tal:recipe-builds> |
742 | + </tal:build-requests> |
743 | </tbody> |
744 | </table> |
745 | <p tal:condition="not: view/builds"> |
Screenshot: https:/ /people. canonical. com/~tomwardill /registry- upload- status. png