Merge ~twom/launchpad:oci-policy-git-branch-picker-format into launchpad:master
- Git
- lp:~twom/launchpad
- oci-policy-git-branch-picker-format
- Merge into 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) |
Related bugs: |
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.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/app/validators/validation.py b/lib/lp/app/validators/validation.py |
2 | index 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 |
43 | diff --git a/lib/lp/code/browser/widgets/gitref.py b/lib/lp/code/browser/widgets/gitref.py |
44 | index 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): |
84 | diff --git a/lib/lp/code/browser/widgets/tests/test_gitrefwidget.py b/lib/lp/code/browser/widgets/tests/test_gitrefwidget.py |
85 | index 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() |
121 | diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py |
122 | index 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 |
166 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py |
167 | index 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) |
298 | diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py |
299 | index 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): |
345 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
346 | index 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: |
LGTM