Merge ~twom/launchpad:oci-policy-use-distribution-credentials-in-upload into launchpad:master
- Git
- lp:~twom/launchpad
- oci-policy-use-distribution-credentials-in-upload
- Merge into master
Proposed by
Tom Wardill
Status: | Superseded |
---|---|
Proposed branch: | ~twom/launchpad:oci-policy-use-distribution-credentials-in-upload |
Merge into: | launchpad:master |
Prerequisite: | ~twom/launchpad:db-oci-policy-use-distribution-credentials-in-upload |
Diff against target: |
1117 lines (+670/-38) (has conflicts) 18 files modified
database/schema/patch-2210-24-0.sql (+11/-0) lib/lp/oci/browser/ocirecipe.py (+31/-1) lib/lp/oci/browser/tests/test_ocirecipe.py (+45/-0) lib/lp/oci/configure.zcml (+12/-0) lib/lp/oci/interfaces/ocirecipe.py (+12/-0) lib/lp/oci/model/ocipushrule.py (+22/-0) lib/lp/oci/model/ocirecipe.py (+35/-6) lib/lp/oci/templates/ocirecipe-index.pt (+40/-27) lib/lp/oci/templates/ocirecipe-new.pt (+5/-0) lib/lp/oci/tests/test_ocirecipe.py (+53/-0) lib/lp/oci/tests/test_ociregistryclient.py (+52/-0) lib/lp/registry/browser/configure.zcml (+1/-1) lib/lp/registry/browser/distribution.py (+115/-2) lib/lp/registry/browser/tests/test_distribution_views.py (+122/-1) lib/lp/registry/configure.zcml (+1/-0) lib/lp/registry/interfaces/distribution.py (+8/-0) lib/lp/registry/model/distribution.py (+7/-0) lib/lp/registry/templates/distribution-edit.pt (+98/-0) Conflict in lib/lp/oci/tests/test_ociregistryclient.py |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+395983@code.launchpad.net |
This proposal has been superseded by a proposal from 2021-01-08.
Commit message
Use distribution credentials in OCI upload
Description of the change
To post a comment you must log in.
Unmerged commits
- e2dd813... by Tom Wardill
-
Upload with distribution credentials
- 20adcdd... by Tom Wardill
-
Add UI for image_name on OCIRecipe
- bb203e9... by Tom Wardill
-
Remove push rules from templates if using distribution credentials
- fa131bc... by Tom Wardill
-
Add image name to recipe
- 067424b... by Tom Wardill
-
Add OCIRecipe.
image_name DB patch - a29743d... by Tom Wardill
-
Add UI for OCI credentials on edit Distribution
- 2ff17d5... by Tom Wardill
-
Add DB field to model and interface for Distribution
- 7679793... by Tom Wardill
-
Add DB patch
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/database/schema/patch-2210-24-0.sql b/database/schema/patch-2210-24-0.sql |
2 | new file mode 100644 |
3 | index 0000000..833fa88 |
4 | --- /dev/null |
5 | +++ b/database/schema/patch-2210-24-0.sql |
6 | @@ -0,0 +1,11 @@ |
7 | +-- Copyright 2021 Canonical Ltd. This software is licensed under the |
8 | +-- GNU Affero General Public License version 3 (see the file LICENSE). |
9 | + |
10 | +SET client_min_messages=ERROR; |
11 | + |
12 | +ALTER TABLE Distribution |
13 | + ADD COLUMN oci_credentials INTEGER REFERENCES OCIRegistryCredentials; |
14 | + |
15 | +COMMENT ON COLUMN Distribution.oci_credentials IS 'Credentials and URL to use for uploading all OCI Images in this distribution to a registry.'; |
16 | + |
17 | +INSERT INTO LaunchpadDatabaseRevision VALUES (2210, 24, 0); |
18 | diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py |
19 | index f18779d..1e04c97 100644 |
20 | --- a/lib/lp/oci/browser/ocirecipe.py |
21 | +++ b/lib/lp/oci/browser/ocirecipe.py |
22 | @@ -4,6 +4,8 @@ |
23 | """OCI recipe views.""" |
24 | |
25 | from __future__ import absolute_import, print_function, unicode_literals |
26 | +import ipdb |
27 | + |
28 | |
29 | __metaclass__ = type |
30 | __all__ = [ |
31 | @@ -739,6 +741,15 @@ class OCIRecipeFormMixin: |
32 | build_args[k] = v |
33 | data['build_args'] = build_args |
34 | |
35 | + @property |
36 | + def use_distribution_credentials(self): |
37 | + if hasattr(self.context, 'oci_project'): |
38 | + project = self.context.oci_project |
39 | + else: |
40 | + project = self.context |
41 | + distro = project.distribution |
42 | + return bool(distro and distro.oci_registry_credentials) |
43 | + |
44 | |
45 | class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, |
46 | OCIRecipeFormMixin): |
47 | @@ -772,6 +783,14 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, |
48 | "The architectures that this OCI recipe builds for. Some " |
49 | "architectures are restricted and may only be enabled or " |
50 | "disabled by administrators.") |
51 | + if self.use_distribution_credentials: |
52 | + self.form_fields += FormFields(TextLine( |
53 | + __name__='image_name', |
54 | + title=u"Image name", |
55 | + description=( |
56 | + "Name to use for registry upload. " |
57 | + "Defaults to the name of the recipe."), |
58 | + required=False, readonly=False)) |
59 | |
60 | def setUpWidgets(self): |
61 | """See `LaunchpadFormView`.""" |
62 | @@ -816,7 +835,9 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, |
63 | oci_project=self.context, git_ref=data["git_ref"], |
64 | build_file=data["build_file"], description=data["description"], |
65 | build_daily=data["build_daily"], build_args=data["build_args"], |
66 | - build_path=data["build_path"], processors=data["processors"]) |
67 | + build_path=data["build_path"], processors=data["processors"], |
68 | + # image_name is only available if using distribution credentials. |
69 | + image_name=data.get("image_name")) |
70 | self.next_url = canonical_url(recipe) |
71 | |
72 | |
73 | @@ -888,6 +909,14 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin, |
74 | "The architectures that this OCI recipe builds for. Some " |
75 | "architectures are restricted and may only be enabled or " |
76 | "disabled by administrators.") |
77 | + if self.use_distribution_credentials: |
78 | + self.form_fields += FormFields(TextLine( |
79 | + __name__='image_name', |
80 | + title=u"Image name", |
81 | + description=( |
82 | + "Name to use for registry upload. " |
83 | + "Defaults to the name of the recipe."), |
84 | + required=False, readonly=False)) |
85 | |
86 | def validate(self, data): |
87 | """See `LaunchpadFormView`.""" |
88 | @@ -924,6 +953,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin, |
89 | data["processors"].append(processor) |
90 | self.validateBuildArgs(data) |
91 | |
92 | + |
93 | class OCIRecipeDeleteView(BaseOCIRecipeEditView): |
94 | """View for deleting OCI recipes.""" |
95 | |
96 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py |
97 | index e22665d..ce20835 100644 |
98 | --- a/lib/lp/oci/browser/tests/test_ocirecipe.py |
99 | +++ b/lib/lp/oci/browser/tests/test_ocirecipe.py |
100 | @@ -6,6 +6,9 @@ |
101 | |
102 | from __future__ import absolute_import, print_function, unicode_literals |
103 | |
104 | +from lp.soyuz.browser.archive import DistributionArchiveURL |
105 | + |
106 | + |
107 | __metaclass__ = type |
108 | |
109 | from datetime import ( |
110 | @@ -243,6 +246,29 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView): |
111 | "Build-time\nARG variables:\nVAR1=10\nVAR2=20", |
112 | MatchesTagText(content, "build-args")) |
113 | |
114 | + def test_create_new_recipe_with_image_name(self): |
115 | + oci_project = self.factory.makeOCIProject() |
116 | + credentials = self.factory.makeOCIRegistryCredentials() |
117 | + with person_logged_in(oci_project.distribution.owner): |
118 | + oci_project.distribution.oci_registry_credentials = credentials |
119 | + [git_ref] = self.factory.makeGitRefs() |
120 | + browser = self.getViewBrowser( |
121 | + oci_project, view_name="+new-recipe", user=self.person) |
122 | + browser.getControl(name="field.name").value = "recipe-name" |
123 | + browser.getControl("Description").value = "Recipe description" |
124 | + browser.getControl(name="field.git_ref.repository").value = ( |
125 | + git_ref.repository.identity) |
126 | + browser.getControl(name="field.git_ref.path").value = git_ref.path |
127 | + |
128 | + image_name = self.factory.getUniqueUnicode() |
129 | + browser.getControl(name="field.image_name").value = image_name |
130 | + browser.getControl("Create OCI recipe").click() |
131 | + |
132 | + content = find_main_content(browser.contents) |
133 | + self.assertThat( |
134 | + "Registry image name\n{}".format(image_name), |
135 | + MatchesTagText(content, "image-name")) |
136 | + |
137 | def test_create_new_recipe_users_teams_as_owner_options(self): |
138 | # Teams that the user is in are options for the OCI recipe owner. |
139 | self.factory.makeTeam( |
140 | @@ -539,6 +565,25 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView): |
141 | IStore(recipe).reload(recipe) |
142 | self.assertEqual({"VAR1": "xxx", "VAR2": "uu"}, recipe.build_args) |
143 | |
144 | + def test_edit_image_name(self): |
145 | + self.setUpDistroSeries() |
146 | + credentials = self.factory.makeOCIRegistryCredentials() |
147 | + with person_logged_in(self.distribution.owner): |
148 | + self.distribution.oci_registry_credentials = credentials |
149 | + oci_project = self.factory.makeOCIProject(pillar=self.distribution) |
150 | + recipe = self.factory.makeOCIRecipe( |
151 | + registrant=self.person, owner=self.person, oci_project=oci_project) |
152 | + browser = self.getViewBrowser( |
153 | + recipe, view_name="+edit", user=recipe.owner) |
154 | + image_name = self.factory.getUniqueUnicode() |
155 | + field = browser.getControl(name="field.image_name") |
156 | + field.value = image_name |
157 | + browser.getControl("Update OCI recipe").click() |
158 | + content = find_main_content(browser.contents) |
159 | + self.assertThat( |
160 | + "Registry image name\n{}".format(image_name), |
161 | + MatchesTagText(content, "image-name")) |
162 | + |
163 | def test_edit_with_invisible_processor(self): |
164 | # It's possible for existing recipes to have an enabled processor |
165 | # that's no longer usable with the current distroseries, which will |
166 | diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml |
167 | index 1f7a6c7..86c235b 100644 |
168 | --- a/lib/lp/oci/configure.zcml |
169 | +++ b/lib/lp/oci/configure.zcml |
170 | @@ -155,6 +155,18 @@ |
171 | interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleSet"/> |
172 | </securedutility> |
173 | |
174 | + <!-- OCIDistributionPushRule --> |
175 | + <class class="lp.oci.model.ocipushrule.OCIDistributionPushRule"> |
176 | + <require |
177 | + permission="launchpad.View" |
178 | + interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleView |
179 | + lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" /> |
180 | + <require |
181 | + permission="launchpad.Edit" |
182 | + interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleEdit" |
183 | + set_schema="lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" /> |
184 | + </class> |
185 | + |
186 | <!-- OCI related jobs --> |
187 | <securedutility |
188 | component="lp.oci.model.ocirecipebuildjob.OCIRegistryUploadJob" |
189 | diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py |
190 | index 131c4d3..51cf370 100644 |
191 | --- a/lib/lp/oci/interfaces/ocirecipe.py |
192 | +++ b/lib/lp/oci/interfaces/ocirecipe.py |
193 | @@ -280,6 +280,11 @@ class IOCIRecipeView(Interface): |
194 | "Whether everything is set up to allow uploading builds of " |
195 | "this OCI recipe to a registry.")) |
196 | |
197 | + use_distribution_credentials = Bool( |
198 | + title=_("Use Distribution credentials"), required=True, readonly=True, |
199 | + description=_("Use the credentials on a Distribution for " |
200 | + "registry upload")) |
201 | + |
202 | def requestBuild(requester, architecture): |
203 | """Request that the OCI recipe is built. |
204 | |
205 | @@ -434,6 +439,13 @@ class IOCIRecipeEditableAttributes(IHasOwner): |
206 | description=_("If True, this recipe should be built daily."), |
207 | readonly=False)) |
208 | |
209 | + image_name = exported(TextLine( |
210 | + title=_("Image name"), |
211 | + description=_("Image name to use on upload to registry. " |
212 | + "Defaults to recipe name if not set."), |
213 | + required=False, |
214 | + readonly=False)) |
215 | + |
216 | |
217 | class IOCIRecipeAdminAttributes(Interface): |
218 | """`IOCIRecipe` attributes that can be edited by admins. |
219 | diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py |
220 | index 24d5e27..f7736e6 100644 |
221 | --- a/lib/lp/oci/model/ocipushrule.py |
222 | +++ b/lib/lp/oci/model/ocipushrule.py |
223 | @@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals |
224 | |
225 | __metaclass__ = type |
226 | __all__ = [ |
227 | + 'OCIDistributionPushRule', |
228 | 'OCIPushRule', |
229 | 'OCIPushRuleSet', |
230 | ] |
231 | @@ -72,6 +73,27 @@ class OCIPushRule(Storm): |
232 | IStore(OCIPushRule).remove(self) |
233 | |
234 | |
235 | +@implementer(IOCIPushRule) |
236 | +class OCIDistributionPushRule: |
237 | + """A none-database instance that is synthesised from data elsewhere.""" |
238 | + |
239 | + registry_credentials = None |
240 | + |
241 | + def __init__(self, recipe, registry_credentials, image_name): |
242 | + self.id = -1 |
243 | + self.recipe = recipe |
244 | + self.registry_credentials = registry_credentials |
245 | + self.image_name = image_name |
246 | + |
247 | + @property |
248 | + def registry_url(self): |
249 | + return self.registry_credentials.url |
250 | + |
251 | + @property |
252 | + def username(self): |
253 | + return self.registry_credentials.username |
254 | + |
255 | + |
256 | @implementer(IOCIPushRuleSet) |
257 | class OCIPushRuleSet: |
258 | |
259 | diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py |
260 | index ab7f655..34fd348 100644 |
261 | --- a/lib/lp/oci/model/ocirecipe.py |
262 | +++ b/lib/lp/oci/model/ocirecipe.py |
263 | @@ -74,7 +74,10 @@ from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource |
264 | from lp.oci.interfaces.ociregistrycredentials import ( |
265 | IOCIRegistryCredentialsSet, |
266 | ) |
267 | -from lp.oci.model.ocipushrule import OCIPushRule |
268 | +from lp.oci.model.ocipushrule import ( |
269 | + OCIDistributionPushRule, |
270 | + OCIPushRule, |
271 | + ) |
272 | from lp.oci.model.ocirecipebuild import OCIRecipeBuild |
273 | from lp.oci.model.ocirecipejob import OCIRecipeJob |
274 | from lp.registry.interfaces.distribution import IDistributionSet |
275 | @@ -160,10 +163,13 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
276 | |
277 | build_daily = Bool(name="build_daily", default=False) |
278 | |
279 | + _image_name = Unicode(name="image_name", allow_none=True) |
280 | + |
281 | def __init__(self, name, registrant, owner, oci_project, git_ref, |
282 | description=None, official=False, require_virtualized=True, |
283 | build_file=None, build_daily=False, date_created=DEFAULT, |
284 | - allow_internet=True, build_args=None, build_path=None): |
285 | + allow_internet=True, build_args=None, build_path=None, |
286 | + image_name=None): |
287 | if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE): |
288 | raise OCIRecipeFeatureDisabled() |
289 | super(OCIRecipe, self).__init__() |
290 | @@ -182,6 +188,7 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
291 | self.allow_internet = allow_internet |
292 | self.build_args = build_args or {} |
293 | self.build_path = build_path |
294 | + self.image_name = image_name |
295 | |
296 | def __repr__(self): |
297 | return "<OCIRecipe ~%s/%s/+oci/%s/+recipe/%s>" % ( |
298 | @@ -463,10 +470,18 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
299 | |
300 | @property |
301 | def push_rules(self): |
302 | + # if we're in a distribution that has credentials set at that level |
303 | + # create a push rule using those credentials |
304 | + if self.use_distribution_credentials: |
305 | + push_rule = OCIDistributionPushRule( |
306 | + self, |
307 | + self.oci_project.distribution.oci_registry_credentials, |
308 | + self.image_name) |
309 | + return [push_rule] |
310 | rules = IStore(self).find( |
311 | OCIPushRule, |
312 | OCIPushRule.recipe == self.id) |
313 | - return rules |
314 | + return list(rules) |
315 | |
316 | @property |
317 | def _pending_states(self): |
318 | @@ -527,7 +542,20 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
319 | |
320 | @property |
321 | def can_upload_to_registry(self): |
322 | - return not self.push_rules.is_empty() |
323 | + return bool(self.push_rules) |
324 | + |
325 | + @property |
326 | + def use_distribution_credentials(self): |
327 | + distribution = self.oci_project.distribution |
328 | + return distribution and distribution.oci_registry_credentials |
329 | + |
330 | + @property |
331 | + def image_name(self): |
332 | + return self._image_name or self.name |
333 | + |
334 | + @image_name.setter |
335 | + def image_name(self, value): |
336 | + self._image_name = value |
337 | |
338 | def newPushRule(self, registrant, registry_url, image_name, credentials, |
339 | credentials_owner=None): |
340 | @@ -569,7 +597,8 @@ class OCIRecipeSet: |
341 | def new(self, name, registrant, owner, oci_project, git_ref, build_file, |
342 | description=None, official=False, require_virtualized=True, |
343 | build_daily=False, processors=None, date_created=DEFAULT, |
344 | - allow_internet=True, build_args=None, build_path=None): |
345 | + allow_internet=True, build_args=None, build_path=None, |
346 | + image_name=None): |
347 | """See `IOCIRecipeSet`.""" |
348 | if not registrant.inTeam(owner): |
349 | if owner.is_team: |
350 | @@ -594,7 +623,7 @@ class OCIRecipeSet: |
351 | oci_recipe = OCIRecipe( |
352 | name, registrant, owner, oci_project, git_ref, description, |
353 | official, require_virtualized, build_file, build_daily, |
354 | - date_created, allow_internet, build_args, build_path) |
355 | + date_created, allow_internet, build_args, build_path, image_name) |
356 | store.add(oci_recipe) |
357 | |
358 | if processors is None: |
359 | diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt |
360 | index 9a9aefc..662c742 100644 |
361 | --- a/lib/lp/oci/templates/ocirecipe-index.pt |
362 | +++ b/lib/lp/oci/templates/ocirecipe-index.pt |
363 | @@ -81,6 +81,12 @@ |
364 | <pre tal:content="build_args" /> |
365 | </dd> |
366 | </dl> |
367 | + <dl id="image-name" tal:condition="context/use_distribution_credentials"> |
368 | + <dt>Registry image name</dt> |
369 | + <dd> |
370 | + <span tal:content="context/image_name" /> |
371 | + </dd> |
372 | + </dl> |
373 | </div> |
374 | |
375 | <h2>Latest builds</h2> |
376 | @@ -144,35 +150,42 @@ |
377 | </div> |
378 | |
379 | |
380 | - <h2>Recipe push rules</h2> |
381 | - <table id="push-rules-listing" tal:condition="view/has_push_rules" class="listing" |
382 | - style="margin-bottom: 1em; "> |
383 | - <thead> |
384 | - <tr> |
385 | - <th>Registry URL</th> |
386 | - <th>Username</th> |
387 | - <th>Image Name</th> |
388 | - </tr> |
389 | - </thead> |
390 | - <tbody> |
391 | - <tal:recipe-push-rules repeat="item view/push_rules"> |
392 | - <tr tal:define="rule item; |
393 | - show_credentials rule/registry_credentials/required:launchpad.View" |
394 | - tal:attributes="id string:rule-${rule/id}"> |
395 | - <td tal:content="python: rule.registry_credentials.url if show_credentials else ''"/> |
396 | - <td tal:content="python: rule.registry_credentials.username if show_credentials else ''"/> |
397 | - <td tal:content="rule/image_name"/> |
398 | + <div tal:condition=context/use_distribution_credentials> |
399 | + <h3>Registry upload</h3> |
400 | + <p>This recipe will use the registry credentials set by the parent distribution</p> |
401 | + </div> |
402 | + |
403 | + <div tal:condition="not: context/use_distribution_credentials"> |
404 | + <h2>Recipe push rules</h2> |
405 | + <table id="push-rules-listing" tal:condition="view/has_push_rules" class="listing" |
406 | + style="margin-bottom: 1em; "> |
407 | + <thead> |
408 | + <tr> |
409 | + <th>Registry URL</th> |
410 | + <th>Username</th> |
411 | + <th>Image Name</th> |
412 | </tr> |
413 | - </tal:recipe-push-rules> |
414 | - </tbody> |
415 | - </table> |
416 | - <p tal:condition="not: view/has_push_rules"> |
417 | - This OCI recipe has no push rules defined yet. |
418 | - </p> |
419 | + </thead> |
420 | + <tbody> |
421 | + <tal:recipe-push-rules repeat="item view/push_rules"> |
422 | + <tr tal:define="rule item; |
423 | + show_credentials rule/registry_credentials/required:launchpad.View" |
424 | + tal:attributes="id string:rule-${rule/id}"> |
425 | + <td tal:content="python: rule.registry_credentials.url if show_credentials else ''"/> |
426 | + <td tal:content="python: rule.registry_credentials.username if show_credentials else ''"/> |
427 | + <td tal:content="rule/image_name"/> |
428 | + </tr> |
429 | + </tal:recipe-push-rules> |
430 | + </tbody> |
431 | + </table> |
432 | + <p tal:condition="not: view/has_push_rules"> |
433 | + This OCI recipe has no push rules defined yet. |
434 | + </p> |
435 | |
436 | - <div tal:define="link context/menu:context/edit_push_rules" |
437 | - tal:condition="link/enabled"> |
438 | - <tal:edit-push-rules replace="structure link/fmt:link"/> |
439 | + <div tal:define="link context/menu:context/edit_push_rules" |
440 | + tal:condition="link/enabled"> |
441 | + <tal:edit-push-rules replace="structure link/fmt:link"/> |
442 | + </div> |
443 | </div> |
444 | |
445 | </div> |
446 | diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt |
447 | index 1cae71e..00d9b08 100644 |
448 | --- a/lib/lp/oci/templates/ocirecipe-new.pt |
449 | +++ b/lib/lp/oci/templates/ocirecipe-new.pt |
450 | @@ -41,6 +41,11 @@ |
451 | <tal:widget define="widget nocall:view/widgets/processors"> |
452 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
453 | </tal:widget> |
454 | + <span tal:condition="view/use_distribution_credentials"> |
455 | + <tal:widget define="widget nocall:view/widgets/image_name" > |
456 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
457 | + </tal:widget> |
458 | + </span> |
459 | </table> |
460 | </metal:formbody> |
461 | </div> |
462 | diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py |
463 | index a0b1c59..699954a 100644 |
464 | --- a/lib/lp/oci/tests/test_ocirecipe.py |
465 | +++ b/lib/lp/oci/tests/test_ocirecipe.py |
466 | @@ -749,6 +749,36 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory): |
467 | "VAR3": "A string", |
468 | }, recipe.build_args) |
469 | |
470 | + def test_use_distribution_credentials_set(self): |
471 | + distribution = self.factory.makeDistribution() |
472 | + credentials = self.factory.makeOCIRegistryCredentials() |
473 | + with person_logged_in(distribution.owner): |
474 | + distribution.oci_registry_credentials = credentials |
475 | + project = self.factory.makeOCIProject(pillar=distribution) |
476 | + recipe = self.factory.makeOCIRecipe(oci_project=project) |
477 | + self.assertTrue(recipe.use_distribution_credentials) |
478 | + |
479 | + def test_use_distribution_credentials_not_set(self): |
480 | + distribution = self.factory.makeDistribution() |
481 | + project = self.factory.makeOCIProject(pillar=distribution) |
482 | + recipe = self.factory.makeOCIRecipe(oci_project=project) |
483 | + self.assertFalse(recipe.use_distribution_credentials) |
484 | + |
485 | + def test_image_name_set(self): |
486 | + distribution = self.factory.makeDistribution() |
487 | + project = self.factory.makeOCIProject(pillar=distribution) |
488 | + recipe = self.factory.makeOCIRecipe(oci_project=project) |
489 | + image_name = self.factory.getUniqueUnicode() |
490 | + with person_logged_in(recipe.owner): |
491 | + recipe.image_name = image_name |
492 | + self.assertEqual(image_name, removeSecurityProxy(recipe)._image_name) |
493 | + |
494 | + def test_image_name_not_set(self): |
495 | + distribution = self.factory.makeDistribution() |
496 | + project = self.factory.makeOCIProject(pillar=distribution) |
497 | + recipe = self.factory.makeOCIRecipe(oci_project=project) |
498 | + self.assertEqual(recipe.name, recipe.image_name) |
499 | + |
500 | |
501 | class TestOCIRecipeProcessors(TestCaseWithFactory): |
502 | |
503 | @@ -1284,6 +1314,29 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory): |
504 | self.assertEqual( |
505 | image_name, push_rules["entries"][0]["image_name"]) |
506 | |
507 | + def test_api_set_image_name(self): |
508 | + """Can you set and retrieve the image name via the API?""" |
509 | + self.setConfig() |
510 | + |
511 | + image_name = self.factory.getUniqueUnicode() |
512 | + |
513 | + with person_logged_in(self.person): |
514 | + oci_project = self.factory.makeOCIProject( |
515 | + registrant=self.person) |
516 | + recipe = self.factory.makeOCIRecipe( |
517 | + oci_project=oci_project, owner=self.person, |
518 | + registrant=self.person) |
519 | + url = api_url(recipe) |
520 | + |
521 | + resp = self.webservice.patch( |
522 | + url, 'application/json', |
523 | + json.dumps({'image_name': image_name})) |
524 | + |
525 | + self.assertEqual(209, resp.status, resp.body) |
526 | + |
527 | + ws_project = self.load_from_api(url) |
528 | + self.assertEqual(image_name, ws_project['image_name']) |
529 | + |
530 | |
531 | class TestOCIRecipeAsyncWebservice(TestCaseWithFactory): |
532 | layer = LaunchpadFunctionalLayer |
533 | diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py |
534 | index ecfaee3..403662d 100644 |
535 | --- a/lib/lp/oci/tests/test_ociregistryclient.py |
536 | +++ b/lib/lp/oci/tests/test_ociregistryclient.py |
537 | @@ -69,6 +69,7 @@ from lp.services.compat import mock |
538 | from lp.services.features.testing import FeatureFixture |
539 | from lp.testing import ( |
540 | admin_logged_in, |
541 | + person_logged_in, |
542 | TestCaseWithFactory, |
543 | ) |
544 | from lp.testing.fixture import ZopeUtilityFixture |
545 | @@ -226,6 +227,7 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin, |
546 | })) |
547 | |
548 | @responses.activate |
549 | +<<<<<<< lib/lp/oci/tests/test_ociregistryclient.py |
550 | def test_upload_ignores_superseded_builds(self): |
551 | self.build.updateStatus(BuildStatus.FULLYBUILT) |
552 | recipe = self.build.recipe |
553 | @@ -249,6 +251,56 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin, |
554 | OCIRecipeBuildRegistryUploadStatus.SUPERSEDED, |
555 | self.build.registry_upload_status) |
556 | self.assertEqual(0, len(responses.calls)) |
557 | +======= |
558 | + def test_upload_with_distribution_credentials(self): |
559 | + self._makeFiles() |
560 | + self.useFixture(MockPatch( |
561 | + "lp.oci.model.ociregistryclient.OCIRegistryClient._upload")) |
562 | + self.useFixture(MockPatch( |
563 | + "lp.oci.model.ociregistryclient.OCIRegistryClient._upload_layer", |
564 | + return_value=999)) |
565 | + credentials = self.factory.makeOCIRegistryCredentials() |
566 | + image_name = self.factory.getUniqueUnicode() |
567 | + self.build.recipe.image_name = image_name |
568 | + distro = self.build.recipe.oci_project.distribution |
569 | + with person_logged_in(distro.owner): |
570 | + distro.oci_registry_credentials = credentials |
571 | + # we have distribution credentials, we should have a 'push rule' |
572 | + push_rule = self.build.recipe.push_rules[0] |
573 | + responses.add("GET", "%s/v2/" % push_rule.registry_url, status=200) |
574 | + self.addManifestResponses(push_rule) |
575 | + |
576 | + self.client.upload(self.build) |
577 | + |
578 | + request = json.loads(responses.calls[1].request.body) |
579 | + |
580 | + self.assertThat(request, MatchesDict({ |
581 | + "layers": MatchesListwise([ |
582 | + MatchesDict({ |
583 | + "mediaType": Equals( |
584 | + "application/vnd.docker.image.rootfs.diff.tar.gzip"), |
585 | + "digest": Equals("diff_id_1"), |
586 | + "size": Equals(999)}), |
587 | + MatchesDict({ |
588 | + "mediaType": Equals( |
589 | + "application/vnd.docker.image.rootfs.diff.tar.gzip"), |
590 | + "digest": Equals("diff_id_2"), |
591 | + "size": Equals(999)}) |
592 | + ]), |
593 | + "schemaVersion": Equals(2), |
594 | + "config": MatchesDict({ |
595 | + "mediaType": Equals( |
596 | + "application/vnd.docker.container.image.v1+json"), |
597 | + "digest": Equals( |
598 | + "sha256:33b69b4b6e106f9fc7a8b93409" |
599 | + "36c85cf7f84b2d017e7b55bee6ab214761f6ab"), |
600 | + "size": Equals(52) |
601 | + }), |
602 | + "mediaType": Equals( |
603 | + "application/vnd.docker.distribution.manifest.v2+json") |
604 | + })) |
605 | + |
606 | +>>>>>>> lib/lp/oci/tests/test_ociregistryclient.py |
607 | |
608 | @responses.activate |
609 | def test_upload_formats_credentials(self): |
610 | diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml |
611 | index 7c97dc5..6be995f 100644 |
612 | --- a/lib/lp/registry/browser/configure.zcml |
613 | +++ b/lib/lp/registry/browser/configure.zcml |
614 | @@ -2275,7 +2275,7 @@ |
615 | for="lp.registry.interfaces.distribution.IDistribution" |
616 | class="lp.registry.browser.distribution.DistributionEditView" |
617 | permission="launchpad.Edit" |
618 | - template="../../app/templates/generic-edit.pt" |
619 | + template="../templates/distribution-edit.pt" |
620 | /> |
621 | <browser:page |
622 | name="+admin" |
623 | diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py |
624 | index c499689..25950ae 100644 |
625 | --- a/lib/lp/registry/browser/distribution.py |
626 | +++ b/lib/lp/registry/browser/distribution.py |
627 | @@ -49,10 +49,15 @@ from zope.formlib.boolwidgets import CheckBoxWidget |
628 | from zope.formlib.widget import CustomWidgetFactory |
629 | from zope.interface import implementer |
630 | from zope.lifecycleevent import ObjectCreatedEvent |
631 | -from zope.schema import Bool |
632 | +from zope.schema import ( |
633 | + Bool, |
634 | + Password, |
635 | + TextLine, |
636 | + ) |
637 | from zope.security.checker import canWrite |
638 | from zope.security.interfaces import Unauthorized |
639 | |
640 | +from lp import _ |
641 | from lp.answers.browser.faqtarget import FAQTargetNavigationMixin |
642 | from lp.answers.browser.questiontarget import QuestionTargetTraversalMixin |
643 | from lp.app.browser.launchpadform import ( |
644 | @@ -80,6 +85,9 @@ from lp.bugs.browser.structuralsubscription import ( |
645 | ) |
646 | from lp.buildmaster.interfaces.processor import IProcessorSet |
647 | from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin |
648 | +from lp.oci.interfaces.ociregistrycredentials import ( |
649 | + IOCIRegistryCredentialsSet, |
650 | + ) |
651 | from lp.registry.browser import ( |
652 | add_subscribe_link, |
653 | RegistryEditFormView, |
654 | @@ -1019,6 +1027,101 @@ class DistributionEditView(RegistryEditFormView, |
655 | """See `LaunchpadFormView`.""" |
656 | return 'Change %s details' % self.context.displayname |
657 | |
658 | + def createOCICredentials(self): |
659 | + return form.Fields( |
660 | + TextLine( |
661 | + __name__='oci_credentials_url', |
662 | + title=u"Registry URL", |
663 | + description=( |
664 | + u"URL for the OCI registry to upload images to." |
665 | + ), |
666 | + required=False), |
667 | + TextLine( |
668 | + __name__='oci_credentials_region', |
669 | + title=u"OCI registry region", |
670 | + description=u"Region for the OCI Registry.", |
671 | + required=False), |
672 | + TextLine( |
673 | + __name__='oci_credentials_username', |
674 | + title=u"OCI registry username", |
675 | + description=u"Username for the OCI Registry.", |
676 | + required=False), |
677 | + Password( |
678 | + __name__='oci_credentials_password', |
679 | + title=u"OCI registry password", |
680 | + description=u"Password for the OCI Registry.", |
681 | + required=False), |
682 | + Password( |
683 | + __name__='oci_credentials_confirm_password', |
684 | + title=u"Confirm password", |
685 | + required=False), |
686 | + Bool( |
687 | + __name__='oci_credentials_delete', |
688 | + title=u"Delete", |
689 | + description=u"Delete these credentials.", |
690 | + required=False,) |
691 | + ) |
692 | + |
693 | + def changeOCICredentials(self, data): |
694 | + delete = data.pop("oci_credentials_delete", None) |
695 | + if delete and self.context.oci_registry_credentials: |
696 | + credentials = self.context.oci_registry_credentials |
697 | + self.context.oci_registry_credentials = None |
698 | + credentials.destroySelf() |
699 | + return |
700 | + |
701 | + url = data.pop("oci_credentials_url", None) |
702 | + username = data.pop("oci_credentials_username", None) |
703 | + region = data.pop("oci_credentials_region", None) |
704 | + # validated against confirm password in validateOCICredentials |
705 | + password = data.pop("oci_credentials_password", None) |
706 | + if "oci_credentials_confirm_password" in data: |
707 | + del(data["oci_credentials_confirm_password"]) |
708 | + |
709 | + # If we're not deleting, but don't have a url, then don't do anything |
710 | + if not url: |
711 | + return |
712 | + |
713 | + current_credentials = self.context.oci_registry_credentials |
714 | + if current_credentials: |
715 | + current_credentials.url = url |
716 | + current_credentials.setCredentials({ |
717 | + "username": username, |
718 | + "password": password, |
719 | + "region": region}) |
720 | + return |
721 | + credentials = getUtility(IOCIRegistryCredentialsSet).new( |
722 | + self.context.owner, |
723 | + self.context.owner, |
724 | + url, |
725 | + {"username": username, |
726 | + "password": password, |
727 | + "region": region}) |
728 | + self.context.oci_registry_credentials = credentials |
729 | + |
730 | + def validateOCICredentials(self, data): |
731 | + # if we're deleting credentials, we don't need to validate |
732 | + if data.get("oci_credentials_delete"): |
733 | + return |
734 | + url = data.get("oci_credentials_url") |
735 | + username = data.get("oci_credentials_username") |
736 | + if username and not url: |
737 | + self.setFieldError( |
738 | + 'oci_credentials_url', |
739 | + _("A URL is required if a username is present.")) |
740 | + password = data.get("oci_credentials_password") |
741 | + confirm_password = data.get("oci_credentials_confirm_password") |
742 | + if password != confirm_password: |
743 | + self.setFieldError( |
744 | + "oci_credentials_password", |
745 | + _("Passwords must match.")) |
746 | + existing_credentials = self.context.oci_registry_credentials |
747 | + if existing_credentials and not url: |
748 | + self.setFieldError( |
749 | + "oci_credentials_url", |
750 | + _("URL must be specified. " |
751 | + "Delete credentials to unset URL.")) |
752 | + |
753 | def setUpFields(self): |
754 | """See `LaunchpadFormView`.""" |
755 | RegistryEditFormView.setUpFields(self) |
756 | @@ -1027,14 +1130,22 @@ class DistributionEditView(RegistryEditFormView, |
757 | getUtility(IProcessorSet).getAll(), |
758 | u"The architectures on which the distribution's main archive can " |
759 | u"build.") |
760 | + self.form_fields += self.createOCICredentials() |
761 | |
762 | @property |
763 | def initial_values(self): |
764 | - return { |
765 | + data = { |
766 | 'require_virtualized': |
767 | self.context.main_archive.require_virtualized, |
768 | 'processors': self.context.main_archive.processors, |
769 | } |
770 | + # Do OCI initial values |
771 | + oci_credentials = self.context.oci_registry_credentials |
772 | + if oci_credentials: |
773 | + data["oci_credentials_url"] = oci_credentials.url |
774 | + data["oci_credentials_username"] = oci_credentials.username |
775 | + data["oci_credentials_region"] = oci_credentials.region |
776 | + return data |
777 | |
778 | def validate(self, data): |
779 | """Constrain bug expiration to Launchpad Bugs tracker.""" |
780 | @@ -1044,6 +1155,7 @@ class DistributionEditView(RegistryEditFormView, |
781 | official_malone = data.get('official_malone', False) |
782 | if not official_malone: |
783 | data['enable_bug_expiration'] = False |
784 | + self.validateOCICredentials(data) |
785 | |
786 | def change_archive_fields(self, data): |
787 | # Update context.main_archive. |
788 | @@ -1063,6 +1175,7 @@ class DistributionEditView(RegistryEditFormView, |
789 | @action("Change", name='change') |
790 | def change_action(self, action, data): |
791 | self.change_archive_fields(data) |
792 | + self.changeOCICredentials(data) |
793 | self.updateContextFromData(data) |
794 | |
795 | |
796 | diff --git a/lib/lp/registry/browser/tests/test_distribution_views.py b/lib/lp/registry/browser/tests/test_distribution_views.py |
797 | index fde5a8a..195560d 100644 |
798 | --- a/lib/lp/registry/browser/tests/test_distribution_views.py |
799 | +++ b/lib/lp/registry/browser/tests/test_distribution_views.py |
800 | @@ -9,6 +9,7 @@ from zope.component import getUtility |
801 | |
802 | from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet |
803 | from lp.buildmaster.interfaces.processor import IProcessorSet |
804 | +from lp.oci.tests.helpers import OCIConfigHelperMixin |
805 | from lp.registry.browser.distribution import DistributionPublisherConfigView |
806 | from lp.registry.enums import DistributionDefaultTraversalPolicy |
807 | from lp.registry.interfaces.distribution import IDistributionSet |
808 | @@ -183,7 +184,7 @@ class TestDistroAddView(TestCaseWithFactory): |
809 | self.assertContentEqual([], distribution.main_archive.processors) |
810 | |
811 | |
812 | -class TestDistroEditView(TestCaseWithFactory): |
813 | +class TestDistroEditView(OCIConfigHelperMixin, TestCaseWithFactory): |
814 | """Test the +edit page for a distribution.""" |
815 | |
816 | layer = DatabaseFunctionalLayer |
817 | @@ -193,6 +194,7 @@ class TestDistroEditView(TestCaseWithFactory): |
818 | self.admin = login_celebrity('admin') |
819 | self.distribution = self.factory.makeDistribution() |
820 | self.all_processors = getUtility(IProcessorSet).getAll() |
821 | + self.setConfig() |
822 | |
823 | def test_edit_distro_init_value_require_virtualized(self): |
824 | view = create_initialized_view( |
825 | @@ -260,6 +262,125 @@ class TestDistroEditView(TestCaseWithFactory): |
826 | method="POST", form=edit_form) |
827 | self.assertEqual(self.distribution.package_derivatives_email, email) |
828 | |
829 | + def test_oci_validation_username_no_url(self): |
830 | + edit_form = self.getDefaultEditDict() |
831 | + edit_form["field.oci_credentials_username"] = "username" |
832 | + |
833 | + view = create_initialized_view( |
834 | + self.distribution, '+edit', principal=self.admin, |
835 | + method='POST', form=edit_form) |
836 | + self.assertEqual( |
837 | + "A URL is required if a username is present.", |
838 | + view.getFieldError("oci_credentials_url")) |
839 | + |
840 | + def test_oci_validation_different_passwords(self): |
841 | + edit_form = self.getDefaultEditDict() |
842 | + edit_form["field.oci_credentials_password"] = "password1" |
843 | + edit_form["field.oci_credentials_confirm_password"] = "password2" |
844 | + view = create_initialized_view( |
845 | + self.distribution, '+edit', principal=self.admin, |
846 | + method='POST', form=edit_form) |
847 | + self.assertEqual( |
848 | + "Passwords must match.", |
849 | + view.getFieldError("oci_credentials_password")) |
850 | + |
851 | + def test_oci_validation_url_unset(self): |
852 | + edit_form = self.getDefaultEditDict() |
853 | + edit_form["field.oci_credentials_url"] = "" |
854 | + |
855 | + credentials = self.factory.makeOCIRegistryCredentials( |
856 | + registrant=self.distribution.owner, |
857 | + owner=self.distribution.owner) |
858 | + self.distribution.oci_registry_credentials = credentials |
859 | + |
860 | + view = create_initialized_view( |
861 | + self.distribution, '+edit', principal=self.admin, |
862 | + method='POST', form=edit_form) |
863 | + self.assertEqual( |
864 | + "URL must be specified. Delete credentials to unset URL.", |
865 | + view.getFieldError("oci_credentials_url")) |
866 | + |
867 | + def test_oci_create_credentials_url_only(self): |
868 | + edit_form = self.getDefaultEditDict() |
869 | + registry_url = self.factory.getUniqueURL() |
870 | + edit_form["field.oci_credentials_url"] = registry_url |
871 | + |
872 | + create_initialized_view( |
873 | + self.distribution, '+edit', principal=self.admin, |
874 | + method='POST', form=edit_form) |
875 | + self.assertEqual( |
876 | + registry_url, self.distribution.oci_registry_credentials.url) |
877 | + |
878 | + def test_oci_create_credentials(self): |
879 | + edit_form = self.getDefaultEditDict() |
880 | + registry_url = self.factory.getUniqueURL() |
881 | + username = self.factory.getUniqueUnicode() |
882 | + password = self.factory.getUniqueUnicode() |
883 | + edit_form["field.oci_credentials_url"] = registry_url |
884 | + edit_form["field.oci_credentials_username"] = username |
885 | + edit_form["field.oci_credentials_password"] = password |
886 | + edit_form["field.oci_credentials_confirm_password"] = password |
887 | + |
888 | + create_initialized_view( |
889 | + self.distribution, '+edit', principal=self.admin, |
890 | + method='POST', form=edit_form) |
891 | + self.assertEqual( |
892 | + username, self.distribution.oci_registry_credentials.username) |
893 | + |
894 | + def test_oci_create_credentials_change_url(self): |
895 | + edit_form = self.getDefaultEditDict() |
896 | + credentials = self.factory.makeOCIRegistryCredentials( |
897 | + registrant=self.distribution.owner, |
898 | + owner=self.distribution.owner) |
899 | + self.distribution.oci_registry_credentials = credentials |
900 | + registry_url = self.factory.getUniqueURL() |
901 | + edit_form["field.oci_credentials_url"] = registry_url |
902 | + |
903 | + create_initialized_view( |
904 | + self.distribution, '+edit', principal=self.admin, |
905 | + method='POST', form=edit_form) |
906 | + self.assertEqual( |
907 | + registry_url, self.distribution.oci_registry_credentials.url) |
908 | + # This should have mutated, not created new credentials records |
909 | + self.assertEqual( |
910 | + credentials.id, self.distribution.oci_registry_credentials.id) |
911 | + |
912 | + def test_oci_create_credentials_change_password(self): |
913 | + edit_form = self.getDefaultEditDict() |
914 | + credentials = self.factory.makeOCIRegistryCredentials( |
915 | + registrant=self.distribution.owner, |
916 | + owner=self.distribution.owner) |
917 | + self.distribution.oci_registry_credentials = credentials |
918 | + password = self.factory.getUniqueUnicode() |
919 | + edit_form["field.oci_credentials_url"] = credentials.url |
920 | + edit_form["field.oci_credentials_username"] = credentials.username |
921 | + edit_form["field.oci_credentials_password"] = password |
922 | + edit_form["field.oci_credentials_confirm_password"] = password |
923 | + |
924 | + create_initialized_view( |
925 | + self.distribution, '+edit', principal=self.admin, |
926 | + method='POST', form=edit_form) |
927 | + distro_credentials = self.distribution.oci_registry_credentials |
928 | + unencrypted_credentials = distro_credentials.getCredentials() |
929 | + self.assertEqual( |
930 | + password, unencrypted_credentials["password"]) |
931 | + # This should not have changed |
932 | + self.assertEqual( |
933 | + distro_credentials.url, credentials.url) |
934 | + |
935 | + def test_oci_delete_credentials(self): |
936 | + edit_form = self.getDefaultEditDict() |
937 | + credentials = self.factory.makeOCIRegistryCredentials( |
938 | + registrant=self.distribution.owner, |
939 | + owner=self.distribution.owner) |
940 | + self.distribution.oci_registry_credentials = credentials |
941 | + edit_form['field.oci_credentials_delete'] = 'on' |
942 | + |
943 | + create_initialized_view( |
944 | + self.distribution, '+edit', principal=self.admin, |
945 | + method='POST', form=edit_form) |
946 | + self.assertIsNone(self.distribution.oci_registry_credentials) |
947 | + |
948 | |
949 | class TestDistributionAdminView(TestCaseWithFactory): |
950 | """Test the +admin page for a distribution.""" |
951 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml |
952 | index 7c2dad8..7a7a396 100644 |
953 | --- a/lib/lp/registry/configure.zcml |
954 | +++ b/lib/lp/registry/configure.zcml |
955 | @@ -1833,6 +1833,7 @@ |
956 | mirror_admin |
957 | mugshot |
958 | oci_project_admin |
959 | + oci_registry_credentials |
960 | official_answers |
961 | official_blueprints |
962 | official_malone |
963 | diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py |
964 | index 8c23b90..6d13dc5 100644 |
965 | --- a/lib/lp/registry/interfaces/distribution.py |
966 | +++ b/lib/lp/registry/interfaces/distribution.py |
967 | @@ -73,6 +73,7 @@ from lp.bugs.interfaces.bugtarget import ( |
968 | from lp.bugs.interfaces.structuralsubscription import ( |
969 | IStructuralSubscriptionTarget, |
970 | ) |
971 | +from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials |
972 | from lp.registry.enums import ( |
973 | DistributionDefaultTraversalPolicy, |
974 | VCSType, |
975 | @@ -719,6 +720,13 @@ class IDistributionPublic( |
976 | def newOCIProject(registrant, name, description=None): |
977 | """Create an `IOCIProject` for this distro.""" |
978 | |
979 | + oci_registry_credentials = Reference( |
980 | + IOCIRegistryCredentials, |
981 | + title=_("OCI registry credentials"), |
982 | + description=_("Credentials and URL to use for uploading all OCI " |
983 | + "Images in this distribution to a registry."), |
984 | + required=False, readonly=True) |
985 | + |
986 | |
987 | @exported_as_webservice_entry(as_of="beta") |
988 | class IDistribution( |
989 | diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py |
990 | index a3d4062..cceef00 100644 |
991 | --- a/lib/lp/registry/model/distribution.py |
992 | +++ b/lib/lp/registry/model/distribution.py |
993 | @@ -33,6 +33,10 @@ from storm.expr import ( |
994 | SQL, |
995 | ) |
996 | from storm.info import ClassAlias |
997 | +from storm.locals import ( |
998 | + Int, |
999 | + Reference, |
1000 | + ) |
1001 | from storm.store import Store |
1002 | from zope.component import getUtility |
1003 | from zope.interface import implementer |
1004 | @@ -267,6 +271,9 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements, |
1005 | enum=DistributionDefaultTraversalPolicy, notNull=False, |
1006 | default=DistributionDefaultTraversalPolicy.SERIES) |
1007 | redirect_default_traversal = BoolCol(notNull=False, default=False) |
1008 | + oci_registry_credentialsID = Int(name='oci_credentials', allow_none=True) |
1009 | + oci_registry_credentials = Reference( |
1010 | + oci_registry_credentialsID, "OCIRegistryCredentials.id") |
1011 | |
1012 | def __repr__(self): |
1013 | display_name = self.display_name.encode('ASCII', 'backslashreplace') |
1014 | diff --git a/lib/lp/registry/templates/distribution-edit.pt b/lib/lp/registry/templates/distribution-edit.pt |
1015 | new file mode 100644 |
1016 | index 0000000..a8562b9 |
1017 | --- /dev/null |
1018 | +++ b/lib/lp/registry/templates/distribution-edit.pt |
1019 | @@ -0,0 +1,98 @@ |
1020 | +<html |
1021 | + xmlns="http://www.w3.org/1999/xhtml" |
1022 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
1023 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
1024 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
1025 | + metal:use-macro="view/macro:page/main_side" |
1026 | + i18n:domain="launchpad" |
1027 | +> |
1028 | +<body> |
1029 | + |
1030 | +<tal:main metal:fill-slot="main"> |
1031 | + |
1032 | + <div metal:use-macro="context/@@launchpad_form/form"> |
1033 | + <metal:formbody fill-slot="widgets"> |
1034 | + <table class="form"> |
1035 | + <tal:widget define="widget nocall:view/widgets/display_name"> |
1036 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1037 | + </tal:widget> |
1038 | + <tal:widget define="widget nocall:view/widgets/summary"> |
1039 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1040 | + </tal:widget> |
1041 | + <tal:widget define="widget nocall:view/widgets/description"> |
1042 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1043 | + </tal:widget> |
1044 | + <tal:widget define="widget nocall:view/widgets/bug_reporting_guidelines"> |
1045 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1046 | + </tal:widget> |
1047 | + <tal:widget define="widget nocall:view/widgets/bug_reported_acknowledgement"> |
1048 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1049 | + </tal:widget> |
1050 | + <tal:widget define="widget nocall:view/widgets/package_derivatives_email"> |
1051 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1052 | + </tal:widget> |
1053 | + <tal:widget define="widget nocall:view/widgets/icon"> |
1054 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1055 | + </tal:widget> |
1056 | + <tal:widget define="widget nocall:view/widgets/logo"> |
1057 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1058 | + </tal:widget> |
1059 | + <tal:widget define="widget nocall:view/widgets/mugshot"> |
1060 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1061 | + </tal:widget> |
1062 | + <tal:widget define="widget nocall:view/widgets/official_malone"> |
1063 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1064 | + </tal:widget> |
1065 | + <tal:widget define="widget nocall:view/widgets/enable_bug_expiration"> |
1066 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1067 | + </tal:widget> |
1068 | + <tal:widget define="widget nocall:view/widgets/blueprints_usage"> |
1069 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1070 | + </tal:widget> |
1071 | + <tal:widget define="widget nocall:view/widgets/translations_usage"> |
1072 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1073 | + </tal:widget> |
1074 | + <tal:widget define="widget nocall:view/widgets/answers_usage"> |
1075 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1076 | + </tal:widget> |
1077 | + <tal:widget define="widget nocall:view/widgets/translation_focus"> |
1078 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1079 | + </tal:widget> |
1080 | + <tal:widget define="widget nocall:view/widgets/default_traversal_policy"> |
1081 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1082 | + </tal:widget> |
1083 | + <tal:widget define="widget nocall:view/widgets/redirect_default_traversal"> |
1084 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1085 | + </tal:widget> |
1086 | + |
1087 | + <tr> |
1088 | + <td><label>OCI registry credentials</label></td> |
1089 | + <tr> |
1090 | + <tr> |
1091 | + <td tal:define="widget nocall:view/widgets/oci_credentials_url"> |
1092 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1093 | + </td> |
1094 | + <td tal:define="widget nocall:view/widgets/oci_credentials_region"> |
1095 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1096 | + </td> |
1097 | + <td tal:define="widget nocall:view/widgets/oci_credentials_username"> |
1098 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1099 | + </td> |
1100 | + <td tal:define="widget nocall:view/widgets/oci_credentials_password"> |
1101 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1102 | + </td> |
1103 | + <td tal:define="widget nocall:view/widgets/oci_credentials_confirm_password"> |
1104 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1105 | + </td> |
1106 | + <td tal:define="widget nocall:view/widgets/oci_credentials_delete"> |
1107 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1108 | + </td> |
1109 | + </tr> |
1110 | + </table> |
1111 | + </metal:formbody> |
1112 | + </div> |
1113 | + |
1114 | +</tal:main> |
1115 | + |
1116 | +</body> |
1117 | +</html> |