Merge ~pappacena/launchpad:oci-unify-request-builds into launchpad:master
- Git
- lp:~pappacena/launchpad
- oci-unify-request-builds
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged |
---|---|
Approved by: | Thiago F. Pappacena |
Approved revision: | 5c5893c9e2f1dfc0ff08a0bbfd06d5646da1af12 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pappacena/launchpad:oci-unify-request-builds |
Merge into: | launchpad:master |
Diff against target: |
401 lines (+123/-57) 7 files modified
lib/lp/oci/browser/ocirecipe.py (+9/-30) lib/lp/oci/browser/tests/test_ocirecipe.py (+31/-13) lib/lp/oci/interfaces/ocirecipe.py (+14/-2) lib/lp/oci/interfaces/ocirecipejob.py (+15/-2) lib/lp/oci/model/ocirecipe.py (+17/-7) lib/lp/oci/model/ocirecipejob.py (+28/-3) lib/lp/oci/templates/ocirecipe-index.pt (+9/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+391645@code.launchpad.net |
Commit message
Use IOCIRecipeReque
Description of the change
To post a comment you must log in.
- d560b56... by Thiago F. Pappacena
-
Merge branch 'master' into oci-unify-
request- builds
- d00a405... by Thiago F. Pappacena
-
Refactoring template display of pending OCI build requests
- 85294fe... by Thiago F. Pappacena
-
Refactoring OCIRecipe.
requestBuilds to receive a list of architectures - 7d63a1d... by Thiago F. Pappacena
-
Refactoring the way we get pending OCI build jobs
- ecb5a47... by Thiago F. Pappacena
-
Adding back requestBuild to OCIRecipe interface
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
Pushed the requested changes.
- 5c5893c... by Thiago F. Pappacena
-
Merge branch 'master' into oci-unify-
request- builds
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
1 | diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py |
2 | index cf11bfb..460b108 100644 |
3 | --- a/lib/lp/oci/browser/ocirecipe.py |
4 | +++ b/lib/lp/oci/browser/ocirecipe.py |
5 | @@ -64,7 +64,6 @@ from lp.oci.interfaces.ocirecipe import ( |
6 | NoSuchOCIRecipe, |
7 | OCI_RECIPE_ALLOW_CREATE, |
8 | OCI_RECIPE_WEBHOOKS_FEATURE_FLAG, |
9 | - OCIRecipeBuildAlreadyPending, |
10 | OCIRecipeFeatureDisabled, |
11 | ) |
12 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet |
13 | @@ -74,7 +73,6 @@ from lp.oci.interfaces.ociregistrycredentials import ( |
14 | user_can_edit_credentials_for_owner, |
15 | ) |
16 | from lp.services.features import getFeatureFlag |
17 | -from lp.services.helpers import english_list |
18 | from lp.services.propertycache import cachedproperty |
19 | from lp.services.webapp import ( |
20 | canonical_url, |
21 | @@ -644,36 +642,17 @@ class OCIRecipeRequestBuildsView(LaunchpadFormView): |
22 | 'distro_arch_series', |
23 | 'You need to select at least one architecture.') |
24 | |
25 | - def requestBuilds(self, data): |
26 | - """User action for requesting a number of builds. |
27 | - |
28 | - We raise exceptions for most errors, but if there's already a |
29 | - pending build for a particular architecture, we simply record that |
30 | - so that other builds can be queued and a message displayed to the |
31 | - caller. |
32 | - """ |
33 | - informational = {} |
34 | - builds = [] |
35 | - already_pending = [] |
36 | - for arch in data['distro_arch_series']: |
37 | - try: |
38 | - build = self.context.requestBuild(self.user, arch) |
39 | - builds.append(build) |
40 | - except OCIRecipeBuildAlreadyPending: |
41 | - already_pending.append(arch) |
42 | - if already_pending: |
43 | - informational['already_pending'] = ( |
44 | - "An identical build is already pending for %s." % |
45 | - english_list(arch.architecturetag for arch in already_pending)) |
46 | - return builds, informational |
47 | - |
48 | @action('Request builds', name='request') |
49 | def request_action(self, action, data): |
50 | - builds, informational = self.requestBuilds(data) |
51 | - already_pending = informational.get('already_pending') |
52 | - notification_text = new_builds_notification_text( |
53 | - builds, already_pending) |
54 | - self.request.response.addNotification(notification_text) |
55 | + if data.get('distro_arch_series'): |
56 | + architectures = [ |
57 | + arch.architecturetag for arch in data['distro_arch_series']] |
58 | + else: |
59 | + architectures = None |
60 | + self.context.requestBuilds(self.user, architectures) |
61 | + self.request.response.addNotification( |
62 | + "Your builds were scheduled and should start soon. " |
63 | + "Refresh this page for details.") |
64 | self.next_url = self.cancel_url |
65 | |
66 | |
67 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py |
68 | index 72f9c83..82422f1 100644 |
69 | --- a/lib/lp/oci/browser/tests/test_ocirecipe.py |
70 | +++ b/lib/lp/oci/browser/tests/test_ocirecipe.py |
71 | @@ -50,18 +50,22 @@ from lp.oci.interfaces.ocirecipe import ( |
72 | IOCIRecipeSet, |
73 | OCI_RECIPE_ALLOW_CREATE, |
74 | ) |
75 | +from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource |
76 | from lp.oci.interfaces.ociregistrycredentials import ( |
77 | IOCIRegistryCredentialsSet, |
78 | ) |
79 | from lp.oci.tests.helpers import OCIConfigHelperMixin |
80 | from lp.registry.interfaces.person import IPersonSet |
81 | +from lp.services.config import config |
82 | from lp.services.database.constants import UTC_NOW |
83 | from lp.services.database.interfaces import IStore |
84 | from lp.services.features.testing import FeatureFixture |
85 | +from lp.services.job.runner import JobRunner |
86 | from lp.services.propertycache import get_property_cache |
87 | from lp.services.webapp import canonical_url |
88 | from lp.services.webapp.servers import LaunchpadTestRequest |
89 | from lp.testing import ( |
90 | + admin_logged_in, |
91 | anonymous_logged_in, |
92 | BrowserTestCase, |
93 | login, |
94 | @@ -71,6 +75,7 @@ from lp.testing import ( |
95 | TestCaseWithFactory, |
96 | time_counter, |
97 | ) |
98 | +from lp.testing.dbuser import dbuser |
99 | from lp.testing.layers import ( |
100 | DatabaseFunctionalLayer, |
101 | LaunchpadFunctionalLayer, |
102 | @@ -938,6 +943,30 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView): |
103 | self.assertRaises( |
104 | Unauthorized, self.getViewBrowser, self.recipe, "+request-builds") |
105 | |
106 | + def runRequestBuildJobs(self): |
107 | + with admin_logged_in(): |
108 | + jobs = getUtility(IOCIRecipeRequestBuildsJobSource).iterReady() |
109 | + with dbuser(config.IOCIRecipeRequestBuildsJobSource.dbuser): |
110 | + JobRunner(jobs).runAll() |
111 | + |
112 | + def test_pending_build_requests_not_shown_if_absent(self): |
113 | + self.recipe.requestBuilds(self.recipe.owner) |
114 | + browser = self.getViewBrowser(self.recipe, user=self.person) |
115 | + content = extract_text(find_main_content(browser.contents)) |
116 | + self.assertIn( |
117 | + "You have 1 pending build request. " |
118 | + "The builds should start automatically soon.", |
119 | + content.replace("\n", " ")) |
120 | + |
121 | + # Run the build request, so we don't have any pending one. The |
122 | + # message should be gone then. |
123 | + self.runRequestBuildJobs() |
124 | + browser = self.getViewBrowser(self.recipe, user=self.person) |
125 | + content = extract_text(find_main_content(browser.contents)) |
126 | + self.assertNotIn( |
127 | + "The builds should start automatically soon.", |
128 | + content.replace("\n", " ")) |
129 | + |
130 | def test_request_builds_action(self): |
131 | # Requesting a build creates pending builds. |
132 | browser = self.getViewBrowser( |
133 | @@ -946,6 +975,8 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView): |
134 | self.assertTrue(browser.getControl("i386").selected) |
135 | browser.getControl("Request builds").click() |
136 | |
137 | + self.runRequestBuildJobs() |
138 | + |
139 | login_person(self.person) |
140 | builds = self.recipe.pending_builds |
141 | self.assertContentEqual( |
142 | @@ -954,19 +985,6 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView): |
143 | self.assertContentEqual( |
144 | [2510], set(build.buildqueue_record.lastscore for build in builds)) |
145 | |
146 | - def test_request_builds_rejects_duplicate(self): |
147 | - # A duplicate build request causes a notification. |
148 | - self.recipe.requestBuild(self.person, self.distroseries["amd64"]) |
149 | - browser = self.getViewBrowser( |
150 | - self.recipe, "+request-builds", user=self.person) |
151 | - self.assertTrue(browser.getControl("amd64").selected) |
152 | - self.assertTrue(browser.getControl("i386").selected) |
153 | - browser.getControl("Request builds").click() |
154 | - main_text = extract_text(find_main_content(browser.contents)) |
155 | - self.assertIn("1 new build has been queued.", main_text) |
156 | - self.assertIn( |
157 | - "An identical build is already pending for amd64.", main_text) |
158 | - |
159 | def test_request_builds_no_architectures(self): |
160 | # Selecting no architectures causes a validation failure. |
161 | browser = self.getViewBrowser( |
162 | diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py |
163 | index 718cdd3..0bd3cd2 100644 |
164 | --- a/lib/lp/oci/interfaces/ocirecipe.py |
165 | +++ b/lib/lp/oci/interfaces/ocirecipe.py |
166 | @@ -54,6 +54,7 @@ from zope.schema import ( |
167 | Dict, |
168 | Int, |
169 | List, |
170 | + Set, |
171 | Text, |
172 | TextLine, |
173 | ) |
174 | @@ -173,6 +174,10 @@ class IOCIRecipeBuildRequest(Interface): |
175 | value_type=Reference(schema=Interface), |
176 | required=True, readonly=True))) |
177 | |
178 | + architectures = Set( |
179 | + title=_("If set, limit builds to these architecture tags."), |
180 | + value_type=TextLine(), required=False, readonly=True) |
181 | + |
182 | |
183 | class IOCIRecipeView(Interface): |
184 | """`IOCIRecipe` attributes that require launchpad.View permission.""" |
185 | @@ -202,6 +207,9 @@ class IOCIRecipeView(Interface): |
186 | "The architectures that are available to be enabled or disabled for " |
187 | "this OCI recipe.") |
188 | |
189 | + pending_build_requests = Attribute( |
190 | + "The list of build requests that didn't trigger builds yet.") |
191 | + |
192 | # This should only be set by using IOCIProject.setOfficialRecipe |
193 | official = Bool( |
194 | title=_("OCI project official"), |
195 | @@ -265,6 +273,10 @@ class IOCIRecipeView(Interface): |
196 | def requestBuild(requester, architecture): |
197 | """Request that the OCI recipe is built. |
198 | |
199 | + Please, note that this method does not associate the created build |
200 | + with an OCIRecipeBuildRequest. So, prefer using the |
201 | + OCIRecipe.requestBuilds (plural). |
202 | + |
203 | :param requester: The person requesting the build. |
204 | :param architecture: The architecture to build for. |
205 | :return: `IOCIRecipeBuild`. |
206 | @@ -273,7 +285,7 @@ class IOCIRecipeView(Interface): |
207 | @call_with(requester=REQUEST_USER) |
208 | @export_factory_operation(IOCIRecipeBuildRequest, []) |
209 | @operation_for_version("devel") |
210 | - def requestBuilds(requester): |
211 | + def requestBuilds(requester, architectures=None): |
212 | """Request that the OCI recipe is built for all available |
213 | architectures. |
214 | |
215 | @@ -281,7 +293,7 @@ class IOCIRecipeView(Interface): |
216 | :return: A `IOCIRecipeBuildRequest` instance. |
217 | """ |
218 | |
219 | - def requestBuildsFromJob(requester): |
220 | + def requestBuildsFromJob(requester, architectures=None): |
221 | """Synchronous part of requesting builds, that should be called as a |
222 | Celery task. |
223 | |
224 | diff --git a/lib/lp/oci/interfaces/ocirecipejob.py b/lib/lp/oci/interfaces/ocirecipejob.py |
225 | index 17d9fd5..104a2ff 100644 |
226 | --- a/lib/lp/oci/interfaces/ocirecipejob.py |
227 | +++ b/lib/lp/oci/interfaces/ocirecipejob.py |
228 | @@ -81,13 +81,26 @@ class IOCIRecipeRequestBuildsJob(IRunnableJob): |
229 | |
230 | class IOCIRecipeRequestBuildsJobSource(IJobSource): |
231 | |
232 | - def create(oci_recipe, requester): |
233 | + def create(oci_recipe, requester, architectures=None): |
234 | """Request builds of an OCI Recipe. |
235 | |
236 | :param oci_recipe: The OCI recipe to build. |
237 | :param requester: The person requesting the builds. |
238 | + :param architectures: Build only for this list of |
239 | + architectures, if they are available for the recipe. If |
240 | + None, build for all available architectures. |
241 | """ |
242 | |
243 | - def getByOCIRecipeAndID(self, oci_recipe, job_id): |
244 | + def getByOCIRecipeAndID(recipe, job_id): |
245 | """Retrieve the build job by OCI recipe and the given job ID. |
246 | """ |
247 | + |
248 | + def findByOCIRecipe(recipe, statuses=None, job_ids=None): |
249 | + """Find jobs for an OCI recipe. |
250 | + |
251 | + :param oci_recipe: An OCI recipe to search for. |
252 | + :param statuses: An optional iterable of `JobStatus`es to search for. |
253 | + :param job_ids: An optional iterable of job IDs to search for. |
254 | + :return: A sequence of `OCIRecipeRequestBuildsJob`s with the specified |
255 | + OCI recipe. |
256 | + """ |
257 | diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py |
258 | index 3ca817c..15fa5be 100644 |
259 | --- a/lib/lp/oci/model/ocirecipe.py |
260 | +++ b/lib/lp/oci/model/ocirecipe.py |
261 | @@ -421,32 +421,42 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
262 | def getBuildRequest(self, job_id): |
263 | return OCIRecipeBuildRequest(self, job_id) |
264 | |
265 | - def requestBuildsFromJob(self, requester, build_request=None): |
266 | + def requestBuildsFromJob(self, requester, build_request=None, |
267 | + architectures=None): |
268 | self._checkRequestBuild(requester) |
269 | - distro_arch_series_to_build = set(self.getAllowedArchitectures()) |
270 | + distro_arch_series = set(self.getAllowedArchitectures()) |
271 | |
272 | builds = [] |
273 | - for das in distro_arch_series_to_build: |
274 | + for das in distro_arch_series: |
275 | + if (architectures is not None |
276 | + and das.architecturetag not in architectures): |
277 | + continue |
278 | try: |
279 | builds.append(self.requestBuild( |
280 | requester, das, build_request=build_request)) |
281 | except OCIRecipeBuildAlreadyPending: |
282 | pass |
283 | |
284 | - # If we have distro_arch_series_to_build, but they all failed to due |
285 | + # If we have distro_arch_series, but they all failed to due |
286 | # to pending builds, we fail the job. |
287 | - if len(distro_arch_series_to_build) > 0 and len(builds) == 0: |
288 | + if len(distro_arch_series) > 0 and len(builds) == 0: |
289 | raise OCIRecipeBuildAlreadyPending |
290 | |
291 | return builds |
292 | |
293 | - def requestBuilds(self, requester): |
294 | + def requestBuilds(self, requester, architectures=None): |
295 | self._checkRequestBuild(requester) |
296 | job = getUtility(IOCIRecipeRequestBuildsJobSource).create( |
297 | - self, requester) |
298 | + self, requester, architectures) |
299 | return self.getBuildRequest(job.job_id) |
300 | |
301 | @property |
302 | + def pending_build_requests(self): |
303 | + util = getUtility(IOCIRecipeRequestBuildsJobSource) |
304 | + return util.findByOCIRecipe( |
305 | + self, statuses=(JobStatus.WAITING, JobStatus.RUNNING)) |
306 | + |
307 | + @property |
308 | def push_rules(self): |
309 | rules = IStore(self).find( |
310 | OCIPushRule, |
311 | diff --git a/lib/lp/oci/model/ocirecipejob.py b/lib/lp/oci/model/ocirecipejob.py |
312 | index 095181e..15eac4f 100644 |
313 | --- a/lib/lp/oci/model/ocirecipejob.py |
314 | +++ b/lib/lp/oci/model/ocirecipejob.py |
315 | @@ -17,6 +17,7 @@ from lazr.enum import ( |
316 | ) |
317 | import six |
318 | from storm.databases.postgres import JSON |
319 | +from storm.expr import Desc |
320 | from storm.properties import Int |
321 | from storm.references import Reference |
322 | from storm.store import EmptyResultSet |
323 | @@ -162,9 +163,13 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived): |
324 | config = config.IOCIRecipeRequestBuildsJobSource |
325 | |
326 | @classmethod |
327 | - def create(cls, recipe, requester): |
328 | + def create(cls, recipe, requester, architectures=None): |
329 | """See `OCIRecipeRequestBuildsJob`.""" |
330 | - metadata = {"requester": requester.id} |
331 | + metadata = { |
332 | + "requester": requester.id, |
333 | + "architectures": ( |
334 | + list(architectures) if architectures is not None else None), |
335 | + } |
336 | recipe_job = OCIRecipeJob(recipe, cls.class_job_type, metadata) |
337 | job = cls(recipe_job) |
338 | job.celeryRunOnCommit() |
339 | @@ -180,6 +185,20 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived): |
340 | raise NotFoundError("Could not find job ID %s" % job_id) |
341 | return cls(job) |
342 | |
343 | + @classmethod |
344 | + def findByOCIRecipe(cls, recipe, statuses=None, job_ids=None): |
345 | + conditions = [ |
346 | + OCIRecipeJob.recipe == recipe, |
347 | + OCIRecipeJob.job_type == cls.class_job_type] |
348 | + if statuses is not None: |
349 | + conditions.append(Job._status.is_in(statuses)) |
350 | + if job_ids is not None: |
351 | + conditions.append(OCIRecipeJob.job_id.is_in(job_ids)) |
352 | + return IStore(OCIRecipeJob).find( |
353 | + (OCIRecipeJob, Job), |
354 | + OCIRecipeJob.job_id == Job.id, |
355 | + *conditions).order_by(Desc(OCIRecipeJob.job_id)) |
356 | + |
357 | def getOperationDescription(self): |
358 | return "requesting builds of %s" % self.recipe |
359 | |
360 | @@ -234,6 +253,11 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived): |
361 | """See `OCIRecipeRequestBuildsJob`.""" |
362 | self.metadata["builds"] = [build.id for build in builds] |
363 | |
364 | + @property |
365 | + def architectures(self): |
366 | + architectures = self.metadata["architectures"] |
367 | + return set(architectures) if architectures is not None else None |
368 | + |
369 | def run(self): |
370 | """See `IRunnableJob`.""" |
371 | requester = self.requester |
372 | @@ -243,7 +267,8 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived): |
373 | return |
374 | try: |
375 | self.builds = self.recipe.requestBuildsFromJob( |
376 | - requester, build_request=self.build_request) |
377 | + requester, build_request=self.build_request, |
378 | + architectures=self.architectures) |
379 | self.error_message = None |
380 | except self.retry_error_types: |
381 | raise |
382 | diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt |
383 | index 5c3ca56..9a9aefc 100644 |
384 | --- a/lib/lp/oci/templates/ocirecipe-index.pt |
385 | +++ b/lib/lp/oci/templates/ocirecipe-index.pt |
386 | @@ -84,6 +84,15 @@ |
387 | </div> |
388 | |
389 | <h2>Latest builds</h2> |
390 | + <div tal:define="count context/pending_build_requests/count|nothing; |
391 | + plural string: build requests; |
392 | + singular string: build request;" |
393 | + tal:condition="count"> |
394 | + You have <span tal:replace="count" /> pending |
395 | + <tal:plural |
396 | + metal:use-macro="context/@@+base-layout-macros/plural-message"/>. |
397 | + The builds should start automatically soon. |
398 | + </div> |
399 | <table id="latest-builds-listing" class="listing" |
400 | style="margin-bottom: 1em;"> |
401 | <thead> |
I'd like the internal API to be shaped a bit differently in some places, but this is along the right lines.