Merge ~twom/launchpad:oci-policy-git-branch-picker-format into launchpad:master

Proposed by Tom Wardill
Status: Merged
Approved by: Tom Wardill
Approved revision: 7c308ffce077ec9836c727e7d85f49eb6d5b3746
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~twom/launchpad:oci-policy-git-branch-picker-format
Merge into: launchpad:master
Diff against target: 359 lines (+133/-31)
7 files modified
lib/lp/app/validators/validation.py (+25/-0)
lib/lp/code/browser/widgets/gitref.py (+13/-1)
lib/lp/code/browser/widgets/tests/test_gitrefwidget.py (+26/-0)
lib/lp/oci/browser/ocirecipe.py (+13/-0)
lib/lp/oci/browser/tests/test_ocirecipe.py (+51/-9)
lib/lp/oci/model/ocirecipe.py (+2/-20)
lib/lp/testing/factory.py (+3/-1)
Reviewer Review Type Date Requested Status
Thiago F. Pappacena (community) Approve
Review via email: mp+397175@code.launchpad.net

Commit message

Add validator to OCIRecipeAdd and OCIRecipeEdit views

Description of the change

Ensure that any new OCIRecipes, and any that are updated, use the branch format of `2.0-20.04`.

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

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/app/validators/validation.py b/lib/lp/app/validators/validation.py
2index e38af13..e9ef8b1 100644
3--- a/lib/lp/app/validators/validation.py
4+++ b/lib/lp/app/validators/validation.py
5@@ -9,8 +9,11 @@ __all__ = [
6 'can_be_nominated_for_series',
7 'valid_cve_sequence',
8 'validate_new_team_email',
9+ 'validate_oci_branch_name',
10 ]
11
12+import re
13+
14 from zope.component import getUtility
15
16 from lp import _
17@@ -79,3 +82,25 @@ def validate_new_team_email(email):
18 _validate_email(email)
19 _check_email_availability(email)
20 return True
21+
22+
23+def validate_oci_branch_name(branch_name):
24+ """Check that a git ref name matches appversion-ubuntuversion."""
25+ split = branch_name.split('-')
26+ # if we've not got at least two components
27+ if len(split) < 2:
28+ return False
29+ app_version = split[0:-1]
30+ ubuntu_version = split[-1]
31+ # 20.04 format
32+ ubuntu_match = re.match("\d{2}\.\d{2}", ubuntu_version)
33+ if not ubuntu_match:
34+ return False
35+ # disallow risks in app version number
36+ for risk in ["stable", "candidate", "beta", "edge"]:
37+ if risk in app_version:
38+ return False
39+ # no '/' as they're a delimiter
40+ if '/' in app_version:
41+ return False
42+ return True
43diff --git a/lib/lp/code/browser/widgets/gitref.py b/lib/lp/code/browser/widgets/gitref.py
44index cbe203d..64f27c5 100644
45--- a/lib/lp/code/browser/widgets/gitref.py
46+++ b/lib/lp/code/browser/widgets/gitref.py
47@@ -107,6 +107,8 @@ class GitRefWidget(BrowserWidget, InputWidget):
48 # If True, only allow reference paths to be branches (refs/heads/*).
49 require_branch = False
50
51+ branch_validator = None
52+
53 def setUpSubWidgets(self):
54 if self._widgets_set_up:
55 return
56@@ -126,6 +128,9 @@ class GitRefWidget(BrowserWidget, InputWidget):
57 self, field.__name__, field, IInputWidget, prefix=self.name)
58 self._widgets_set_up = True
59
60+ def setBranchFormatValidator(self, branch_validator):
61+ self.branch_validator = branch_validator
62+
63 def setRenderedValue(self, value, with_path=True):
64 """See `IWidget`."""
65 self.setUpSubWidgets()
66@@ -205,9 +210,16 @@ class GitRefWidget(BrowserWidget, InputWidget):
67 ref = None
68 if not ref and (repository or self.context.required):
69 raise WidgetInputError(
70+ self.name, self.label,
71+ LaunchpadValidationError(
72+ "Please enter a Git branch path."))
73+ if self.branch_validator and ref is not None:
74+ valid, message = self.branch_validator(ref)
75+ if not valid:
76+ raise WidgetInputError(
77 self.name, self.label,
78 LaunchpadValidationError(
79- "Please enter a Git branch path."))
80+ message))
81 return ref
82
83 def error(self):
84diff --git a/lib/lp/code/browser/widgets/tests/test_gitrefwidget.py b/lib/lp/code/browser/widgets/tests/test_gitrefwidget.py
85index 98e223a..9b8e76c 100644
86--- a/lib/lp/code/browser/widgets/tests/test_gitrefwidget.py
87+++ b/lib/lp/code/browser/widgets/tests/test_gitrefwidget.py
88@@ -274,6 +274,32 @@ class TestGitRefWidget(WithScenarios, TestCaseWithFactory):
89 self.widget.request = LaunchpadTestRequest(form=form)
90 self.assertEqual(ref, self.widget.getInputValue())
91
92+ def test_getInputValue_with_branch_validator_valid(self):
93+ def validator(ref):
94+ return True, ""
95+ [ref] = self.factory.makeGitRefs()
96+ form = {
97+ "field.git_ref.repository": ref.repository.unique_name,
98+ "field.git_ref.path": ref.path,
99+ }
100+ self.widget.setBranchFormatValidator(validator)
101+ self.widget.request = LaunchpadTestRequest(form=form)
102+ self.assertEqual(ref, self.widget.getInputValue())
103+
104+ def test_getInputValue_with_branch_validator_invalid(self):
105+ def validator(ref):
106+ return False, "Not in correct format"
107+ [ref] = self.factory.makeGitRefs()
108+ form = {
109+ "field.git_ref.repository": ref.repository.unique_name,
110+ "field.git_ref.path": ref.path,
111+ }
112+ self.widget.setBranchFormatValidator(validator)
113+ self.widget.request = LaunchpadTestRequest(form=form)
114+ self.assertGetInputValueError(
115+ form,
116+ "Not in correct format")
117+
118 def test_call(self):
119 # The __call__ method sets up the widgets.
120 markup = self.widget()
121diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
122index abaff05..4f4253a 100644
123--- a/lib/lp/oci/browser/ocirecipe.py
124+++ b/lib/lp/oci/browser/ocirecipe.py
125@@ -54,6 +54,7 @@ from lp.app.browser.tales import (
126 GitRepositoryFormatterAPI,
127 )
128 from lp.app.errors import UnexpectedFormData
129+from lp.app.validators.validation import validate_oci_branch_name
130 from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
131 from lp.buildmaster.interfaces.processor import IProcessorSet
132 from lp.code.browser.widgets.gitref import GitRefWidget
133@@ -754,6 +755,16 @@ class OCIRecipeFormMixin:
134 return True
135 return False
136
137+ def _branch_format_validator(self, ref):
138+ result = validate_oci_branch_name(ref.name)
139+ if result:
140+ return result, ""
141+ message = (
142+ "Branch does not match format "
143+ "'applicationversion-ubuntuversion', eg. 'v1.0-20.04'"
144+ )
145+ return False, message
146+
147
148 class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
149 OCIRecipeFormMixin):
150@@ -805,6 +816,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
151 path = self.context.getDefaultGitRepositoryPath(self.user)
152 widget = self.widgets["git_ref"]
153 widget.setUpSubWidgets()
154+ widget.setBranchFormatValidator(self._branch_format_validator)
155 widget.repository_widget.setRenderedValue(path)
156 if widget.error():
157 # Do not override more important git_ref errors.
158@@ -950,6 +962,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
159 oci_proj_url = canonical_url(oci_proj)
160 widget = self.widgets["git_ref"]
161 widget.setUpSubWidgets()
162+ widget.setBranchFormatValidator(self._branch_format_validator)
163 if widget.error():
164 # Do not override more important git_ref errors.
165 return
166diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
167index 22634c1..970c49c 100644
168--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
169+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
170@@ -190,7 +190,8 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
171 def test_create_new_recipe(self):
172 oci_project = self.factory.makeOCIProject()
173 oci_project_display = oci_project.display_name
174- [git_ref] = self.factory.makeGitRefs()
175+ [git_ref] = self.factory.makeGitRefs(
176+ paths=['/refs/heads/v2.0-20.04'])
177 source_display = git_ref.display_name
178 browser = self.getViewBrowser(
179 oci_project, view_name="+new-recipe", user=self.person)
180@@ -227,9 +228,24 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
181 "Official recipe:\nNo",
182 MatchesTagText(content, "official-recipe"))
183
184+ def test_create_new_recipe_invalid_format(self):
185+ oci_project = self.factory.makeOCIProject()
186+ [git_ref] = self.factory.makeGitRefs(
187+ paths=['/refs/heads/invalid'])
188+ browser = self.getViewBrowser(
189+ oci_project, view_name="+new-recipe", user=self.person)
190+ browser.getControl(name="field.name").value = "recipe-name"
191+ browser.getControl("Description").value = "Recipe description"
192+ browser.getControl(name="field.git_ref.repository").value = (
193+ git_ref.repository.identity)
194+ browser.getControl(name="field.git_ref.path").value = git_ref.path
195+ browser.getControl("Create OCI recipe").click()
196+ self.assertIn("Branch does not match format", browser.contents)
197+
198 def test_create_new_recipe_with_build_args(self):
199 oci_project = self.factory.makeOCIProject()
200- [git_ref] = self.factory.makeGitRefs()
201+ [git_ref] = self.factory.makeGitRefs(
202+ paths=['/refs/heads/v2.0-20.04'])
203 browser = self.getViewBrowser(
204 oci_project, view_name="+new-recipe", user=self.person)
205 browser.getControl(name="field.name").value = "recipe-name"
206@@ -289,7 +305,8 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
207 def test_create_new_recipe_processors(self):
208 self.setUpDistroSeries()
209 oci_project = self.factory.makeOCIProject(pillar=self.distribution)
210- [git_ref] = self.factory.makeGitRefs()
211+ [git_ref] = self.factory.makeGitRefs(
212+ paths=['/refs/heads/v2.0-20.04'])
213 browser = self.getViewBrowser(
214 oci_project, view_name="+new-recipe", user=self.person)
215 processors = browser.getControl(name="field.processors")
216@@ -358,7 +375,8 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
217 distribution = self.factory.makeDistribution(
218 oci_project_admin=self.person)
219 oci_project = self.factory.makeOCIProject(pillar=distribution)
220- [git_ref] = self.factory.makeGitRefs()
221+ [git_ref] = self.factory.makeGitRefs(
222+ paths=['/refs/heads/v2.0-20.04'])
223 browser = self.getViewBrowser(
224 oci_project, view_name="+new-recipe", user=self.person)
225 browser.getControl(name="field.name").value = "recipe-name"
226@@ -381,11 +399,13 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
227
228 # do it once
229 oci_project = self.factory.makeOCIProject(pillar=distribution)
230- [git_ref] = self.factory.makeGitRefs()
231+ [git_ref] = self.factory.makeGitRefs(
232+ paths=['/refs/heads/v2.0-20.04'])
233
234 # and then do it again
235 oci_project2 = self.factory.makeOCIProject(pillar=distribution)
236- [git_ref2] = self.factory.makeGitRefs()
237+ [git_ref2] = self.factory.makeGitRefs(
238+ paths=['/refs/heads/v3.0-20.04'])
239 browser = self.getViewBrowser(
240 oci_project, view_name="+new-recipe", user=self.person)
241 browser.getControl(name="field.name").value = "recipe-name"
242@@ -429,7 +449,8 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
243 distribution = self.factory.makeDistribution(
244 oci_project_admin=distro_owner)
245 oci_project = self.factory.makeOCIProject(pillar=distribution)
246- [git_ref] = self.factory.makeGitRefs()
247+ [git_ref] = self.factory.makeGitRefs(
248+ paths=['/refs/heads/v2.0-20.04'])
249 browser = self.getViewBrowser(
250 oci_project, view_name="+new-recipe", user=self.person)
251 browser.getControl(name="field.name").value = "recipe-name"
252@@ -448,7 +469,8 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
253
254 def test_create_recipe_doesnt_override_gitref_errors(self):
255 oci_project = self.factory.makeOCIProject()
256- [git_ref] = self.factory.makeGitRefs()
257+ [git_ref] = self.factory.makeGitRefs(
258+ paths=['/refs/heads/v2.0-20.04'])
259 browser = self.getViewBrowser(
260 oci_project, view_name="+new-recipe", user=self.person)
261 browser.getControl(name="field.name").value = "recipe-name"
262@@ -561,7 +583,8 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
263 oci_project=oci_project, git_ref=old_git_ref)
264 self.factory.makeTeam(
265 name="new-team", displayname="New Team", members=[self.person])
266- [new_git_ref] = self.factory.makeGitRefs()
267+ [new_git_ref] = self.factory.makeGitRefs(
268+ paths=['/refs/heads/v2.0-20.04'])
269 self.factory.makeOCIPushRule(recipe=recipe)
270
271 browser = self.getViewBrowser(recipe, user=self.person)
272@@ -596,6 +619,25 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
273 "Build schedule:\nBuilt daily\nEdit OCI recipe\n",
274 MatchesTagText(content, "build-schedule"))
275
276+ def test_edit_recipe_invalid_branch(self):
277+ oci_project = self.factory.makeOCIProject()
278+ repository = self.factory.makeGitRepository()
279+ [old_git_ref] = self.factory.makeGitRefs(repository=repository)
280+ recipe = self.factory.makeOCIRecipe(
281+ registrant=self.person, owner=self.person,
282+ oci_project=oci_project, git_ref=old_git_ref)
283+ self.factory.makeTeam(
284+ name="new-team", displayname="New Team", members=[self.person])
285+ [new_git_ref] = self.factory.makeGitRefs(
286+ repository=repository, paths=['/refs/heads/invalid'])
287+ self.factory.makeOCIPushRule(recipe=recipe)
288+
289+ browser = self.getViewBrowser(recipe, user=self.person)
290+ browser.getLink("Edit OCI recipe").click()
291+ browser.getControl(name="field.git_ref.path").value = new_git_ref.path
292+ browser.getControl("Update OCI recipe").click()
293+ self.assertIn("Branch does not match format", browser.contents)
294+
295 def test_edit_recipe_sets_date_last_modified(self):
296 # Editing an OCI recipe sets the date_last_modified property.
297 date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
298diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
299index fd14762..41493ee 100644
300--- a/lib/lp/oci/model/ocirecipe.py
301+++ b/lib/lp/oci/model/ocirecipe.py
302@@ -12,7 +12,6 @@ __all__ = [
303 'OCIRecipeSet',
304 ]
305
306-import re
307
308 from lazr.lifecycle.event import ObjectCreatedEvent
309 import pytz
310@@ -47,6 +46,7 @@ from zope.security.proxy import (
311
312 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
313 from lp.app.interfaces.security import IAuthorization
314+from lp.app.validators.validation import validate_oci_branch_name
315 from lp.buildmaster.enums import BuildStatus
316 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
317 from lp.buildmaster.model.buildfarmjob import BuildFarmJob
318@@ -201,25 +201,7 @@ class OCIRecipe(Storm, WebhookTargetMixin):
319
320 @property
321 def is_valid_branch_format(self):
322- name = self.git_ref.name
323- split = name.split('-')
324- # if we've not got at least two components
325- if len(split) < 2:
326- return False
327- app_version = split[0:-1]
328- ubuntu_version = split[-1]
329- # 20.04 format
330- ubuntu_match = re.match("\d{2}\.\d{2}", ubuntu_version)
331- if not ubuntu_match:
332- return False
333- # disallow risks in app version number
334- for risk in ["stable", "candidate", "beta", "edge"]:
335- if risk in app_version:
336- return False
337- # no '/' as they're a delimiter
338- if '/' in app_version:
339- return False
340- return True
341+ return validate_oci_branch_name(self.git_ref.name)
342
343 @property
344 def build_args(self):
345diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
346index 0bd8f83..df269c0 100644
347--- a/lib/lp/testing/factory.py
348+++ b/lib/lp/testing/factory.py
349@@ -4937,7 +4937,9 @@ class BareLaunchpadObjectFactory(ObjectFactory):
350 if oci_project is None:
351 oci_project = self.makeOCIProject()
352 if git_ref is None:
353- [git_ref] = self.makeGitRefs()
354+ component = self.getUniqueUnicode()
355+ paths = [u'refs/heads/{}-20.04'.format(component)]
356+ [git_ref] = self.makeGitRefs(paths=paths)
357 if build_file is None:
358 build_file = self.getUniqueUnicode(u"build_file_for")
359 if build_path is None:

Subscribers

People subscribed via source and target branches

to status/vote changes: