Merge ~twom/launchpad:oci-policy-tags-hang-off-branches into launchpad:master
- Git
- lp:~twom/launchpad
- oci-policy-tags-hang-off-branches
- Merge into master
Status: | Superseded |
---|---|
Proposed branch: | ~twom/launchpad:oci-policy-tags-hang-off-branches |
Merge into: | launchpad:master |
Prerequisite: | ~twom/launchpad:oci-policy-use-distribution-credentials-in-upload |
Diff against target: |
1325 lines (+118/-684) 18 files modified
dev/null (+0/-98) lib/lp/oci/browser/ocirecipe.py (+1/-28) lib/lp/oci/browser/tests/test_ocirecipe.py (+0/-42) lib/lp/oci/configure.zcml (+0/-12) lib/lp/oci/interfaces/ocirecipe.py (+4/-11) lib/lp/oci/model/ocipushrule.py (+0/-22) lib/lp/oci/model/ocirecipe.py (+13/-35) lib/lp/oci/model/ociregistryclient.py (+32/-28) lib/lp/oci/templates/ocirecipe-index.pt (+27/-40) lib/lp/oci/templates/ocirecipe-new.pt (+0/-5) lib/lp/oci/tests/test_ocirecipe.py (+17/-53) lib/lp/oci/tests/test_ociregistryclient.py (+20/-56) lib/lp/registry/browser/configure.zcml (+1/-1) lib/lp/registry/browser/distribution.py (+2/-115) lib/lp/registry/browser/tests/test_distribution_views.py (+1/-122) lib/lp/registry/configure.zcml (+0/-1) lib/lp/registry/interfaces/distribution.py (+0/-8) lib/lp/registry/model/distribution.py (+0/-7) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Needs Information | ||
Thiago F. Pappacena (community) | Approve | ||
Review via email: mp+396085@code.launchpad.net |
This proposal has been superseded by a proposal from 2021-01-15.
Commit message
Extract tag names from branch names in distribution scenario
Description of the change
In the case of a distribution with registry credentials, we should use the git branch name as a tag.
The format for this is not enforced by this MP.
Colin Watson (cjwatson) wrote : | # |
I'd like to see this disconnected from the distribution credentials work if possible, by using the new tag format across the board as long as the branch name is suitable (for appropriate values of "suitable"). Do you have a sense of how far away from that we are with current production data?
- 4b5163f... by Tom Wardill
-
Rely on the branch format rather than distribution credentials
Unmerged commits
- 4b5163f... by Tom Wardill
-
Rely on the branch format rather than distribution credentials
- 5c57317... by Tom Wardill
-
Extract tag names from branch names in distribution scenario
Preview Diff
1 | diff --git a/database/schema/patch-2210-24-0.sql b/database/schema/patch-2210-24-0.sql |
2 | deleted file mode 100644 |
3 | index 833fa88..0000000 |
4 | --- a/database/schema/patch-2210-24-0.sql |
5 | +++ /dev/null |
6 | @@ -1,11 +0,0 @@ |
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 0d3f5bc..a2543e6 100644 |
20 | --- a/lib/lp/oci/browser/ocirecipe.py |
21 | +++ b/lib/lp/oci/browser/ocirecipe.py |
22 | @@ -742,15 +742,6 @@ class OCIRecipeFormMixin: |
23 | build_args[k] = v |
24 | data['build_args'] = build_args |
25 | |
26 | - @property |
27 | - def use_distribution_credentials(self): |
28 | - if hasattr(self.context, 'oci_project'): |
29 | - project = self.context.oci_project |
30 | - else: |
31 | - project = self.context |
32 | - distro = project.distribution |
33 | - return bool(distro and distro.oci_registry_credentials) |
34 | - |
35 | |
36 | class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, |
37 | OCIRecipeFormMixin): |
38 | @@ -784,14 +775,6 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, |
39 | "The architectures that this OCI recipe builds for. Some " |
40 | "architectures are restricted and may only be enabled or " |
41 | "disabled by administrators.") |
42 | - if self.use_distribution_credentials: |
43 | - self.form_fields += FormFields(TextLine( |
44 | - __name__='image_name', |
45 | - title=u"Image name", |
46 | - description=( |
47 | - "Name to use for registry upload. " |
48 | - "Defaults to the name of the recipe."), |
49 | - required=False, readonly=False)) |
50 | |
51 | def setUpGitRefWidget(self): |
52 | """Setup GitRef widget indicating the user to use the default |
53 | @@ -854,9 +837,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, |
54 | oci_project=self.context, git_ref=data["git_ref"], |
55 | build_file=data["build_file"], description=data["description"], |
56 | build_daily=data["build_daily"], build_args=data["build_args"], |
57 | - build_path=data["build_path"], processors=data["processors"], |
58 | - # image_name is only available if using distribution credentials. |
59 | - image_name=data.get("image_name")) |
60 | + build_path=data["build_path"], processors=data["processors"]) |
61 | self.next_url = canonical_url(recipe) |
62 | |
63 | |
64 | @@ -957,14 +938,6 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin, |
65 | "The architectures that this OCI recipe builds for. Some " |
66 | "architectures are restricted and may only be enabled or " |
67 | "disabled by administrators.") |
68 | - if self.use_distribution_credentials: |
69 | - self.form_fields += FormFields(TextLine( |
70 | - __name__='image_name', |
71 | - title=u"Image name", |
72 | - description=( |
73 | - "Name to use for registry upload. " |
74 | - "Defaults to the name of the recipe."), |
75 | - required=False, readonly=False)) |
76 | |
77 | def validate(self, data): |
78 | """See `LaunchpadFormView`.""" |
79 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py |
80 | index 543d524..afea558 100644 |
81 | --- a/lib/lp/oci/browser/tests/test_ocirecipe.py |
82 | +++ b/lib/lp/oci/browser/tests/test_ocirecipe.py |
83 | @@ -244,29 +244,6 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView): |
84 | "Build-time\nARG variables:\nVAR1=10\nVAR2=20", |
85 | MatchesTagText(content, "build-args")) |
86 | |
87 | - def test_create_new_recipe_with_image_name(self): |
88 | - oci_project = self.factory.makeOCIProject() |
89 | - credentials = self.factory.makeOCIRegistryCredentials() |
90 | - with person_logged_in(oci_project.distribution.owner): |
91 | - oci_project.distribution.oci_registry_credentials = credentials |
92 | - [git_ref] = self.factory.makeGitRefs() |
93 | - browser = self.getViewBrowser( |
94 | - oci_project, view_name="+new-recipe", user=self.person) |
95 | - browser.getControl(name="field.name").value = "recipe-name" |
96 | - browser.getControl("Description").value = "Recipe description" |
97 | - browser.getControl(name="field.git_ref.repository").value = ( |
98 | - git_ref.repository.identity) |
99 | - browser.getControl(name="field.git_ref.path").value = git_ref.path |
100 | - |
101 | - image_name = self.factory.getUniqueUnicode() |
102 | - browser.getControl(name="field.image_name").value = image_name |
103 | - browser.getControl("Create OCI recipe").click() |
104 | - |
105 | - content = find_main_content(browser.contents) |
106 | - self.assertThat( |
107 | - "Registry image name\n{}".format(image_name), |
108 | - MatchesTagText(content, "image-name")) |
109 | - |
110 | def test_create_new_recipe_users_teams_as_owner_options(self): |
111 | # Teams that the user is in are options for the OCI recipe owner. |
112 | self.factory.makeTeam( |
113 | @@ -596,25 +573,6 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView): |
114 | IStore(recipe).reload(recipe) |
115 | self.assertEqual({"VAR1": "xxx", "VAR2": "uu"}, recipe.build_args) |
116 | |
117 | - def test_edit_image_name(self): |
118 | - self.setUpDistroSeries() |
119 | - credentials = self.factory.makeOCIRegistryCredentials() |
120 | - with person_logged_in(self.distribution.owner): |
121 | - self.distribution.oci_registry_credentials = credentials |
122 | - oci_project = self.factory.makeOCIProject(pillar=self.distribution) |
123 | - recipe = self.factory.makeOCIRecipe( |
124 | - registrant=self.person, owner=self.person, oci_project=oci_project) |
125 | - browser = self.getViewBrowser( |
126 | - recipe, view_name="+edit", user=recipe.owner) |
127 | - image_name = self.factory.getUniqueUnicode() |
128 | - field = browser.getControl(name="field.image_name") |
129 | - field.value = image_name |
130 | - browser.getControl("Update OCI recipe").click() |
131 | - content = find_main_content(browser.contents) |
132 | - self.assertThat( |
133 | - "Registry image name\n{}".format(image_name), |
134 | - MatchesTagText(content, "image-name")) |
135 | - |
136 | def test_edit_with_invisible_processor(self): |
137 | # It's possible for existing recipes to have an enabled processor |
138 | # that's no longer usable with the current distroseries, which will |
139 | diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml |
140 | index 86c235b..1f7a6c7 100644 |
141 | --- a/lib/lp/oci/configure.zcml |
142 | +++ b/lib/lp/oci/configure.zcml |
143 | @@ -155,18 +155,6 @@ |
144 | interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleSet"/> |
145 | </securedutility> |
146 | |
147 | - <!-- OCIDistributionPushRule --> |
148 | - <class class="lp.oci.model.ocipushrule.OCIDistributionPushRule"> |
149 | - <require |
150 | - permission="launchpad.View" |
151 | - interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleView |
152 | - lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" /> |
153 | - <require |
154 | - permission="launchpad.Edit" |
155 | - interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleEdit" |
156 | - set_schema="lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" /> |
157 | - </class> |
158 | - |
159 | <!-- OCI related jobs --> |
160 | <securedutility |
161 | component="lp.oci.model.ocirecipebuildjob.OCIRegistryUploadJob" |
162 | diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py |
163 | index 51cf370..a27c5fd 100644 |
164 | --- a/lib/lp/oci/interfaces/ocirecipe.py |
165 | +++ b/lib/lp/oci/interfaces/ocirecipe.py |
166 | @@ -280,10 +280,10 @@ class IOCIRecipeView(Interface): |
167 | "Whether everything is set up to allow uploading builds of " |
168 | "this OCI recipe to a registry.")) |
169 | |
170 | - use_distribution_credentials = Bool( |
171 | - title=_("Use Distribution credentials"), required=True, readonly=True, |
172 | - description=_("Use the credentials on a Distribution for " |
173 | - "registry upload")) |
174 | + is_valid_branch_format = Bool( |
175 | + title=_("Is valid branch format"), required=True, readonly=True, |
176 | + description=_("Whether the git branch name is the correct " |
177 | + "format for using as a tag name.")) |
178 | |
179 | def requestBuild(requester, architecture): |
180 | """Request that the OCI recipe is built. |
181 | @@ -439,13 +439,6 @@ class IOCIRecipeEditableAttributes(IHasOwner): |
182 | description=_("If True, this recipe should be built daily."), |
183 | readonly=False)) |
184 | |
185 | - image_name = exported(TextLine( |
186 | - title=_("Image name"), |
187 | - description=_("Image name to use on upload to registry. " |
188 | - "Defaults to recipe name if not set."), |
189 | - required=False, |
190 | - readonly=False)) |
191 | - |
192 | |
193 | class IOCIRecipeAdminAttributes(Interface): |
194 | """`IOCIRecipe` attributes that can be edited by admins. |
195 | diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py |
196 | index f7736e6..24d5e27 100644 |
197 | --- a/lib/lp/oci/model/ocipushrule.py |
198 | +++ b/lib/lp/oci/model/ocipushrule.py |
199 | @@ -7,7 +7,6 @@ from __future__ import absolute_import, print_function, unicode_literals |
200 | |
201 | __metaclass__ = type |
202 | __all__ = [ |
203 | - 'OCIDistributionPushRule', |
204 | 'OCIPushRule', |
205 | 'OCIPushRuleSet', |
206 | ] |
207 | @@ -73,27 +72,6 @@ class OCIPushRule(Storm): |
208 | IStore(OCIPushRule).remove(self) |
209 | |
210 | |
211 | -@implementer(IOCIPushRule) |
212 | -class OCIDistributionPushRule: |
213 | - """A none-database instance that is synthesised from data elsewhere.""" |
214 | - |
215 | - registry_credentials = None |
216 | - |
217 | - def __init__(self, recipe, registry_credentials, image_name): |
218 | - self.id = -1 |
219 | - self.recipe = recipe |
220 | - self.registry_credentials = registry_credentials |
221 | - self.image_name = image_name |
222 | - |
223 | - @property |
224 | - def registry_url(self): |
225 | - return self.registry_credentials.url |
226 | - |
227 | - @property |
228 | - def username(self): |
229 | - return self.registry_credentials.username |
230 | - |
231 | - |
232 | @implementer(IOCIPushRuleSet) |
233 | class OCIPushRuleSet: |
234 | |
235 | diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py |
236 | index 34fd348..8e5d6f5 100644 |
237 | --- a/lib/lp/oci/model/ocirecipe.py |
238 | +++ b/lib/lp/oci/model/ocirecipe.py |
239 | @@ -12,6 +12,8 @@ __all__ = [ |
240 | 'OCIRecipeSet', |
241 | ] |
242 | |
243 | +import re |
244 | + |
245 | from lazr.lifecycle.event import ObjectCreatedEvent |
246 | import pytz |
247 | import six |
248 | @@ -74,10 +76,7 @@ from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource |
249 | from lp.oci.interfaces.ociregistrycredentials import ( |
250 | IOCIRegistryCredentialsSet, |
251 | ) |
252 | -from lp.oci.model.ocipushrule import ( |
253 | - OCIDistributionPushRule, |
254 | - OCIPushRule, |
255 | - ) |
256 | +from lp.oci.model.ocipushrule import OCIPushRule |
257 | from lp.oci.model.ocirecipebuild import OCIRecipeBuild |
258 | from lp.oci.model.ocirecipejob import OCIRecipeJob |
259 | from lp.registry.interfaces.distribution import IDistributionSet |
260 | @@ -163,13 +162,10 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
261 | |
262 | build_daily = Bool(name="build_daily", default=False) |
263 | |
264 | - _image_name = Unicode(name="image_name", allow_none=True) |
265 | - |
266 | def __init__(self, name, registrant, owner, oci_project, git_ref, |
267 | description=None, official=False, require_virtualized=True, |
268 | build_file=None, build_daily=False, date_created=DEFAULT, |
269 | - allow_internet=True, build_args=None, build_path=None, |
270 | - image_name=None): |
271 | + allow_internet=True, build_args=None, build_path=None): |
272 | if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE): |
273 | raise OCIRecipeFeatureDisabled() |
274 | super(OCIRecipe, self).__init__() |
275 | @@ -188,7 +184,6 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
276 | self.allow_internet = allow_internet |
277 | self.build_args = build_args or {} |
278 | self.build_path = build_path |
279 | - self.image_name = image_name |
280 | |
281 | def __repr__(self): |
282 | return "<OCIRecipe ~%s/%s/+oci/%s/+recipe/%s>" % ( |
283 | @@ -205,6 +200,11 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
284 | return self._official |
285 | |
286 | @property |
287 | + def is_valid_branch_format(self): |
288 | + format_regex = "^(?!(stable|candidate|beta|edge)).*-(\d{2}\.\d{2})$" |
289 | + return bool(re.match(format_regex, self.git_ref.name)) |
290 | + |
291 | + @property |
292 | def build_args(self): |
293 | return self._build_args or {} |
294 | |
295 | @@ -470,18 +470,10 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
296 | |
297 | @property |
298 | def push_rules(self): |
299 | - # if we're in a distribution that has credentials set at that level |
300 | - # create a push rule using those credentials |
301 | - if self.use_distribution_credentials: |
302 | - push_rule = OCIDistributionPushRule( |
303 | - self, |
304 | - self.oci_project.distribution.oci_registry_credentials, |
305 | - self.image_name) |
306 | - return [push_rule] |
307 | rules = IStore(self).find( |
308 | OCIPushRule, |
309 | OCIPushRule.recipe == self.id) |
310 | - return list(rules) |
311 | + return rules |
312 | |
313 | @property |
314 | def _pending_states(self): |
315 | @@ -542,20 +534,7 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
316 | |
317 | @property |
318 | def can_upload_to_registry(self): |
319 | - return bool(self.push_rules) |
320 | - |
321 | - @property |
322 | - def use_distribution_credentials(self): |
323 | - distribution = self.oci_project.distribution |
324 | - return distribution and distribution.oci_registry_credentials |
325 | - |
326 | - @property |
327 | - def image_name(self): |
328 | - return self._image_name or self.name |
329 | - |
330 | - @image_name.setter |
331 | - def image_name(self, value): |
332 | - self._image_name = value |
333 | + return not self.push_rules.is_empty() |
334 | |
335 | def newPushRule(self, registrant, registry_url, image_name, credentials, |
336 | credentials_owner=None): |
337 | @@ -597,8 +576,7 @@ class OCIRecipeSet: |
338 | def new(self, name, registrant, owner, oci_project, git_ref, build_file, |
339 | description=None, official=False, require_virtualized=True, |
340 | build_daily=False, processors=None, date_created=DEFAULT, |
341 | - allow_internet=True, build_args=None, build_path=None, |
342 | - image_name=None): |
343 | + allow_internet=True, build_args=None, build_path=None): |
344 | """See `IOCIRecipeSet`.""" |
345 | if not registrant.inTeam(owner): |
346 | if owner.is_team: |
347 | @@ -623,7 +601,7 @@ class OCIRecipeSet: |
348 | oci_recipe = OCIRecipe( |
349 | name, registrant, owner, oci_project, git_ref, description, |
350 | official, require_virtualized, build_file, build_daily, |
351 | - date_created, allow_internet, build_args, build_path, image_name) |
352 | + date_created, allow_internet, build_args, build_path) |
353 | store.add(oci_recipe) |
354 | |
355 | if processors is None: |
356 | diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py |
357 | index e528281..5470b6a 100644 |
358 | --- a/lib/lp/oci/model/ociregistryclient.py |
359 | +++ b/lib/lp/oci/model/ociregistryclient.py |
360 | @@ -15,6 +15,8 @@ from functools import partial |
361 | import hashlib |
362 | from io import BytesIO |
363 | import json |
364 | + |
365 | + |
366 | try: |
367 | from json.decoder import JSONDecodeError |
368 | except ImportError: |
369 | @@ -243,22 +245,22 @@ class OCIRegistryClient: |
370 | return data |
371 | |
372 | @classmethod |
373 | - def _calculateTag(cls, build, push_rule): |
374 | + def _calculateTags(cls, recipe): |
375 | """Work out the base tag for the image should be. |
376 | |
377 | - :param build: `OCIRecipeBuild` representing this build. |
378 | :param push_rule: `OCIPushRule` that we are using. |
379 | """ |
380 | - # XXX twom 2020-04-17 This needs to include OCIProjectSeries and |
381 | - # base image name |
382 | - return "{}".format("edge") |
383 | + tags = [] |
384 | + if recipe.is_valid_branch_format: |
385 | + tags.append("{}_{}".format(recipe.git_ref.name, "edge")) |
386 | + tags.append("edge") |
387 | + return tags |
388 | |
389 | @classmethod |
390 | - def _getCurrentRegistryManifest(cls, http_client, push_rule): |
391 | + def _getCurrentRegistryManifest(cls, http_client, tag): |
392 | """Get the current manifest for the given push rule. If manifest |
393 | doesn't exist, raises HTTPError. |
394 | """ |
395 | - tag = cls._calculateTag(None, push_rule) |
396 | url = "/manifests/{}".format(tag) |
397 | accept = "application/vnd.docker.distribution.manifest.list.v2+json" |
398 | response = http_client.requestPath( |
399 | @@ -267,7 +269,7 @@ class OCIRegistryClient: |
400 | |
401 | @classmethod |
402 | def _uploadRegistryManifest(cls, http_client, registry_manifest, |
403 | - push_rule, build=None): |
404 | + push_rule, tag, build=None): |
405 | """Uploads the build manifest, returning its content information. |
406 | |
407 | The returned information can be used to create a Manifest list |
408 | @@ -276,18 +278,15 @@ class OCIRegistryClient: |
409 | |
410 | :return: A dict with {"digest": "sha256:xxx", "size": total_bytes} |
411 | """ |
412 | + # This is |
413 | + # https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list |
414 | + # Specifically the Schema 2 manifest. |
415 | digest = None |
416 | data = json.dumps(registry_manifest) |
417 | size = len(data) |
418 | content_type = registry_manifest.get( |
419 | "mediaType", |
420 | "application/vnd.docker.distribution.manifest.v2+json") |
421 | - if build is None: |
422 | - # When uploading a manifest list, use the tag. |
423 | - tag = cls._calculateTag(build, push_rule) |
424 | - else: |
425 | - # When uploading individual build manifests, use their digest. |
426 | - tag = "sha256:%s" % hashlib.sha256(data).hexdigest() |
427 | try: |
428 | manifest_response = http_client.requestPath( |
429 | "/manifests/{}".format(tag), |
430 | @@ -311,7 +310,7 @@ class OCIRegistryClient: |
431 | |
432 | @classmethod |
433 | def _upload_to_push_rule( |
434 | - cls, push_rule, build, manifest, digests, preloaded_data): |
435 | + cls, push_rule, build, manifest, digests, preloaded_data, tag): |
436 | http_client = RegistryHTTPClient.getInstance(push_rule) |
437 | |
438 | for section in manifest: |
439 | @@ -346,7 +345,7 @@ class OCIRegistryClient: |
440 | |
441 | # Upload the registry manifest |
442 | manifest = cls._uploadRegistryManifest( |
443 | - http_client, registry_manifest, push_rule, build) |
444 | + http_client, registry_manifest, push_rule, tag, build) |
445 | |
446 | # Save the uploaded manifest location, so we can use it in case |
447 | # this is a multi-arch image upload. |
448 | @@ -376,11 +375,13 @@ class OCIRegistryClient: |
449 | |
450 | exceptions = [] |
451 | for push_rule in build.recipe.push_rules: |
452 | - try: |
453 | - cls._upload_to_push_rule( |
454 | - push_rule, build, manifest, digests, preloaded_data) |
455 | - except Exception as e: |
456 | - exceptions.append(e) |
457 | + for tag in cls._calculateTags(build.recipe): |
458 | + try: |
459 | + cls._upload_to_push_rule( |
460 | + push_rule, build, manifest, digests, preloaded_data, |
461 | + tag) |
462 | + except Exception as e: |
463 | + exceptions.append(e) |
464 | if len(exceptions) == 1: |
465 | raise exceptions[0] |
466 | elif len(exceptions) > 1: |
467 | @@ -388,7 +389,7 @@ class OCIRegistryClient: |
468 | |
469 | @classmethod |
470 | def makeMultiArchManifest(cls, http_client, push_rule, build_request, |
471 | - uploaded_builds): |
472 | + uploaded_builds, tag): |
473 | """Returns the multi-arch manifest content including all uploaded |
474 | builds of the given build_request. |
475 | """ |
476 | @@ -402,7 +403,7 @@ class OCIRegistryClient: |
477 | |
478 | try: |
479 | current_manifest = cls._getCurrentRegistryManifest( |
480 | - http_client, push_rule) |
481 | + http_client, tag) |
482 | # Check if the current manifest is not an incompatible version. |
483 | version = current_manifest.get("schemaVersion", 1) |
484 | if version < 2 or "manifests" not in current_manifest: |
485 | @@ -481,11 +482,14 @@ class OCIRegistryClient: |
486 | if not uploaded_builds: |
487 | return |
488 | for push_rule in build_request.recipe.push_rules: |
489 | - http_client = RegistryHTTPClient.getInstance(push_rule) |
490 | - multi_manifest_content = cls.makeMultiArchManifest( |
491 | - http_client, push_rule, build_request, uploaded_builds) |
492 | - cls._uploadRegistryManifest( |
493 | - http_client, multi_manifest_content, push_rule, build=None) |
494 | + for tag in cls._calculateTags(build_request.recipe): |
495 | + http_client = RegistryHTTPClient.getInstance(push_rule) |
496 | + multi_manifest_content = cls.makeMultiArchManifest( |
497 | + http_client, push_rule, build_request, uploaded_builds, |
498 | + tag) |
499 | + cls._uploadRegistryManifest( |
500 | + http_client, multi_manifest_content, push_rule, tag, |
501 | + build=None) |
502 | |
503 | |
504 | class OCIRegistryAuthenticationError(Exception): |
505 | diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt |
506 | index 662c742..9a9aefc 100644 |
507 | --- a/lib/lp/oci/templates/ocirecipe-index.pt |
508 | +++ b/lib/lp/oci/templates/ocirecipe-index.pt |
509 | @@ -81,12 +81,6 @@ |
510 | <pre tal:content="build_args" /> |
511 | </dd> |
512 | </dl> |
513 | - <dl id="image-name" tal:condition="context/use_distribution_credentials"> |
514 | - <dt>Registry image name</dt> |
515 | - <dd> |
516 | - <span tal:content="context/image_name" /> |
517 | - </dd> |
518 | - </dl> |
519 | </div> |
520 | |
521 | <h2>Latest builds</h2> |
522 | @@ -150,42 +144,35 @@ |
523 | </div> |
524 | |
525 | |
526 | - <div tal:condition=context/use_distribution_credentials> |
527 | - <h3>Registry upload</h3> |
528 | - <p>This recipe will use the registry credentials set by the parent distribution</p> |
529 | - </div> |
530 | - |
531 | - <div tal:condition="not: context/use_distribution_credentials"> |
532 | - <h2>Recipe push rules</h2> |
533 | - <table id="push-rules-listing" tal:condition="view/has_push_rules" class="listing" |
534 | - style="margin-bottom: 1em; "> |
535 | - <thead> |
536 | - <tr> |
537 | - <th>Registry URL</th> |
538 | - <th>Username</th> |
539 | - <th>Image Name</th> |
540 | + <h2>Recipe push rules</h2> |
541 | + <table id="push-rules-listing" tal:condition="view/has_push_rules" class="listing" |
542 | + style="margin-bottom: 1em; "> |
543 | + <thead> |
544 | + <tr> |
545 | + <th>Registry URL</th> |
546 | + <th>Username</th> |
547 | + <th>Image Name</th> |
548 | + </tr> |
549 | + </thead> |
550 | + <tbody> |
551 | + <tal:recipe-push-rules repeat="item view/push_rules"> |
552 | + <tr tal:define="rule item; |
553 | + show_credentials rule/registry_credentials/required:launchpad.View" |
554 | + tal:attributes="id string:rule-${rule/id}"> |
555 | + <td tal:content="python: rule.registry_credentials.url if show_credentials else ''"/> |
556 | + <td tal:content="python: rule.registry_credentials.username if show_credentials else ''"/> |
557 | + <td tal:content="rule/image_name"/> |
558 | </tr> |
559 | - </thead> |
560 | - <tbody> |
561 | - <tal:recipe-push-rules repeat="item view/push_rules"> |
562 | - <tr tal:define="rule item; |
563 | - show_credentials rule/registry_credentials/required:launchpad.View" |
564 | - tal:attributes="id string:rule-${rule/id}"> |
565 | - <td tal:content="python: rule.registry_credentials.url if show_credentials else ''"/> |
566 | - <td tal:content="python: rule.registry_credentials.username if show_credentials else ''"/> |
567 | - <td tal:content="rule/image_name"/> |
568 | - </tr> |
569 | - </tal:recipe-push-rules> |
570 | - </tbody> |
571 | - </table> |
572 | - <p tal:condition="not: view/has_push_rules"> |
573 | - This OCI recipe has no push rules defined yet. |
574 | - </p> |
575 | + </tal:recipe-push-rules> |
576 | + </tbody> |
577 | + </table> |
578 | + <p tal:condition="not: view/has_push_rules"> |
579 | + This OCI recipe has no push rules defined yet. |
580 | + </p> |
581 | |
582 | - <div tal:define="link context/menu:context/edit_push_rules" |
583 | - tal:condition="link/enabled"> |
584 | - <tal:edit-push-rules replace="structure link/fmt:link"/> |
585 | - </div> |
586 | + <div tal:define="link context/menu:context/edit_push_rules" |
587 | + tal:condition="link/enabled"> |
588 | + <tal:edit-push-rules replace="structure link/fmt:link"/> |
589 | </div> |
590 | |
591 | </div> |
592 | diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt |
593 | index 00d9b08..1cae71e 100644 |
594 | --- a/lib/lp/oci/templates/ocirecipe-new.pt |
595 | +++ b/lib/lp/oci/templates/ocirecipe-new.pt |
596 | @@ -41,11 +41,6 @@ |
597 | <tal:widget define="widget nocall:view/widgets/processors"> |
598 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
599 | </tal:widget> |
600 | - <span tal:condition="view/use_distribution_credentials"> |
601 | - <tal:widget define="widget nocall:view/widgets/image_name" > |
602 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
603 | - </tal:widget> |
604 | - </span> |
605 | </table> |
606 | </metal:formbody> |
607 | </div> |
608 | diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py |
609 | index 699954a..69f7cbf 100644 |
610 | --- a/lib/lp/oci/tests/test_ocirecipe.py |
611 | +++ b/lib/lp/oci/tests/test_ocirecipe.py |
612 | @@ -749,36 +749,6 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory): |
613 | "VAR3": "A string", |
614 | }, recipe.build_args) |
615 | |
616 | - def test_use_distribution_credentials_set(self): |
617 | - distribution = self.factory.makeDistribution() |
618 | - credentials = self.factory.makeOCIRegistryCredentials() |
619 | - with person_logged_in(distribution.owner): |
620 | - distribution.oci_registry_credentials = credentials |
621 | - project = self.factory.makeOCIProject(pillar=distribution) |
622 | - recipe = self.factory.makeOCIRecipe(oci_project=project) |
623 | - self.assertTrue(recipe.use_distribution_credentials) |
624 | - |
625 | - def test_use_distribution_credentials_not_set(self): |
626 | - distribution = self.factory.makeDistribution() |
627 | - project = self.factory.makeOCIProject(pillar=distribution) |
628 | - recipe = self.factory.makeOCIRecipe(oci_project=project) |
629 | - self.assertFalse(recipe.use_distribution_credentials) |
630 | - |
631 | - def test_image_name_set(self): |
632 | - distribution = self.factory.makeDistribution() |
633 | - project = self.factory.makeOCIProject(pillar=distribution) |
634 | - recipe = self.factory.makeOCIRecipe(oci_project=project) |
635 | - image_name = self.factory.getUniqueUnicode() |
636 | - with person_logged_in(recipe.owner): |
637 | - recipe.image_name = image_name |
638 | - self.assertEqual(image_name, removeSecurityProxy(recipe)._image_name) |
639 | - |
640 | - def test_image_name_not_set(self): |
641 | - distribution = self.factory.makeDistribution() |
642 | - project = self.factory.makeOCIProject(pillar=distribution) |
643 | - recipe = self.factory.makeOCIRecipe(oci_project=project) |
644 | - self.assertEqual(recipe.name, recipe.image_name) |
645 | - |
646 | |
647 | class TestOCIRecipeProcessors(TestCaseWithFactory): |
648 | |
649 | @@ -900,6 +870,23 @@ class TestOCIRecipeProcessors(TestCaseWithFactory): |
650 | self.assertContentEqual( |
651 | [self.default_procs[0], self.arm, hppa], recipe.processors) |
652 | |
653 | + def test_valid_branch_format(self): |
654 | + [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v1.0-20.04"]) |
655 | + recipe = self.factory.makeOCIRecipe(git_ref=git_ref) |
656 | + self.assertTrue(recipe.is_valid_branch_format) |
657 | + |
658 | + def test_valid_branch_format_invalid(self): |
659 | + [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v1.0-foo"]) |
660 | + recipe = self.factory.makeOCIRecipe(git_ref=git_ref) |
661 | + self.assertFalse(recipe.is_valid_branch_format) |
662 | + |
663 | + def test_valid_branch_format_invalid_uses_risk(self): |
664 | + for risk in ["stable", "candidate", "beta", "edge"]: |
665 | + path = "refs/heads/{}-20.04".format(risk) |
666 | + [git_ref] = self.factory.makeGitRefs(paths=[path]) |
667 | + recipe = self.factory.makeOCIRecipe(git_ref=git_ref) |
668 | + self.assertFalse(recipe.is_valid_branch_format) |
669 | + |
670 | |
671 | class TestOCIRecipeSet(TestCaseWithFactory): |
672 | |
673 | @@ -1314,29 +1301,6 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory): |
674 | self.assertEqual( |
675 | image_name, push_rules["entries"][0]["image_name"]) |
676 | |
677 | - def test_api_set_image_name(self): |
678 | - """Can you set and retrieve the image name via the API?""" |
679 | - self.setConfig() |
680 | - |
681 | - image_name = self.factory.getUniqueUnicode() |
682 | - |
683 | - with person_logged_in(self.person): |
684 | - oci_project = self.factory.makeOCIProject( |
685 | - registrant=self.person) |
686 | - recipe = self.factory.makeOCIRecipe( |
687 | - oci_project=oci_project, owner=self.person, |
688 | - registrant=self.person) |
689 | - url = api_url(recipe) |
690 | - |
691 | - resp = self.webservice.patch( |
692 | - url, 'application/json', |
693 | - json.dumps({'image_name': image_name})) |
694 | - |
695 | - self.assertEqual(209, resp.status, resp.body) |
696 | - |
697 | - ws_project = self.load_from_api(url) |
698 | - self.assertEqual(image_name, ws_project['image_name']) |
699 | - |
700 | |
701 | class TestOCIRecipeAsyncWebservice(TestCaseWithFactory): |
702 | layer = LaunchpadFunctionalLayer |
703 | diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py |
704 | index 403662d..3cfc920 100644 |
705 | --- a/lib/lp/oci/tests/test_ociregistryclient.py |
706 | +++ b/lib/lp/oci/tests/test_ociregistryclient.py |
707 | @@ -69,7 +69,6 @@ from lp.services.compat import mock |
708 | from lp.services.features.testing import FeatureFixture |
709 | from lp.testing import ( |
710 | admin_logged_in, |
711 | - person_logged_in, |
712 | TestCaseWithFactory, |
713 | ) |
714 | from lp.testing.fixture import ZopeUtilityFixture |
715 | @@ -182,6 +181,14 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin, |
716 | ) |
717 | responses.add("PUT", manifests_url, status=status_code, json=json) |
718 | |
719 | + # format for distribution credential upload |
720 | + manifests_url = "{}/v2/{}/manifests/{}_edge".format( |
721 | + push_rule.registry_credentials.url, |
722 | + push_rule.image_name, |
723 | + push_rule.recipe.git_ref.name |
724 | + ) |
725 | + responses.add("PUT", manifests_url, status=status_code, json=json) |
726 | + |
727 | @responses.activate |
728 | def test_upload(self): |
729 | self._makeFiles() |
730 | @@ -227,7 +234,6 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin, |
731 | })) |
732 | |
733 | @responses.activate |
734 | -<<<<<<< lib/lp/oci/tests/test_ociregistryclient.py |
735 | def test_upload_ignores_superseded_builds(self): |
736 | self.build.updateStatus(BuildStatus.FULLYBUILT) |
737 | recipe = self.build.recipe |
738 | @@ -251,56 +257,6 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin, |
739 | OCIRecipeBuildRegistryUploadStatus.SUPERSEDED, |
740 | self.build.registry_upload_status) |
741 | self.assertEqual(0, len(responses.calls)) |
742 | -======= |
743 | - def test_upload_with_distribution_credentials(self): |
744 | - self._makeFiles() |
745 | - self.useFixture(MockPatch( |
746 | - "lp.oci.model.ociregistryclient.OCIRegistryClient._upload")) |
747 | - self.useFixture(MockPatch( |
748 | - "lp.oci.model.ociregistryclient.OCIRegistryClient._upload_layer", |
749 | - return_value=999)) |
750 | - credentials = self.factory.makeOCIRegistryCredentials() |
751 | - image_name = self.factory.getUniqueUnicode() |
752 | - self.build.recipe.image_name = image_name |
753 | - distro = self.build.recipe.oci_project.distribution |
754 | - with person_logged_in(distro.owner): |
755 | - distro.oci_registry_credentials = credentials |
756 | - # we have distribution credentials, we should have a 'push rule' |
757 | - push_rule = self.build.recipe.push_rules[0] |
758 | - responses.add("GET", "%s/v2/" % push_rule.registry_url, status=200) |
759 | - self.addManifestResponses(push_rule) |
760 | - |
761 | - self.client.upload(self.build) |
762 | - |
763 | - request = json.loads(responses.calls[1].request.body) |
764 | - |
765 | - self.assertThat(request, MatchesDict({ |
766 | - "layers": MatchesListwise([ |
767 | - MatchesDict({ |
768 | - "mediaType": Equals( |
769 | - "application/vnd.docker.image.rootfs.diff.tar.gzip"), |
770 | - "digest": Equals("diff_id_1"), |
771 | - "size": Equals(999)}), |
772 | - MatchesDict({ |
773 | - "mediaType": Equals( |
774 | - "application/vnd.docker.image.rootfs.diff.tar.gzip"), |
775 | - "digest": Equals("diff_id_2"), |
776 | - "size": Equals(999)}) |
777 | - ]), |
778 | - "schemaVersion": Equals(2), |
779 | - "config": MatchesDict({ |
780 | - "mediaType": Equals( |
781 | - "application/vnd.docker.container.image.v1+json"), |
782 | - "digest": Equals( |
783 | - "sha256:33b69b4b6e106f9fc7a8b93409" |
784 | - "36c85cf7f84b2d017e7b55bee6ab214761f6ab"), |
785 | - "size": Equals(52) |
786 | - }), |
787 | - "mediaType": Equals( |
788 | - "application/vnd.docker.distribution.manifest.v2+json") |
789 | - })) |
790 | - |
791 | ->>>>>>> lib/lp/oci/tests/test_ociregistryclient.py |
792 | |
793 | @responses.activate |
794 | def test_upload_formats_credentials(self): |
795 | @@ -390,10 +346,18 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin, |
796 | 'diff_id_1': Equals(self.layer_files[0].library_file), |
797 | 'diff_id_2': Equals(self.layer_files[1].library_file)})})) |
798 | |
799 | - def test_calculateTag(self): |
800 | - result = self.client._calculateTag( |
801 | - self.build, self.build.recipe.push_rules[0]) |
802 | - self.assertEqual("edge", result) |
803 | + def test_calculateTags_invalid_format(self): |
804 | + [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/invalid"]) |
805 | + self.build.recipe.git_ref = git_ref |
806 | + result = self.client._calculateTags(self.build.recipe) |
807 | + self.assertThat(result, MatchesListwise([Equals("edge")])) |
808 | + |
809 | + def test_calculateTags_valid_format(self): |
810 | + [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v1.0-20.04"]) |
811 | + self.build.recipe.git_ref = git_ref |
812 | + result = self.client._calculateTags(self.build.recipe) |
813 | + self.assertThat(result, MatchesListwise( |
814 | + [Equals("v1.0-20.04_edge"), Equals("edge")])) |
815 | |
816 | def test_build_registry_manifest(self): |
817 | self._makeFiles() |
818 | diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml |
819 | index 6954e2c..cdaa3da 100644 |
820 | --- a/lib/lp/registry/browser/configure.zcml |
821 | +++ b/lib/lp/registry/browser/configure.zcml |
822 | @@ -2275,7 +2275,7 @@ |
823 | for="lp.registry.interfaces.distribution.IDistribution" |
824 | class="lp.registry.browser.distribution.DistributionEditView" |
825 | permission="launchpad.Edit" |
826 | - template="../templates/distribution-edit.pt" |
827 | + template="../../app/templates/generic-edit.pt" |
828 | /> |
829 | <browser:page |
830 | name="+admin" |
831 | diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py |
832 | index 25950ae..c499689 100644 |
833 | --- a/lib/lp/registry/browser/distribution.py |
834 | +++ b/lib/lp/registry/browser/distribution.py |
835 | @@ -49,15 +49,10 @@ from zope.formlib.boolwidgets import CheckBoxWidget |
836 | from zope.formlib.widget import CustomWidgetFactory |
837 | from zope.interface import implementer |
838 | from zope.lifecycleevent import ObjectCreatedEvent |
839 | -from zope.schema import ( |
840 | - Bool, |
841 | - Password, |
842 | - TextLine, |
843 | - ) |
844 | +from zope.schema import Bool |
845 | from zope.security.checker import canWrite |
846 | from zope.security.interfaces import Unauthorized |
847 | |
848 | -from lp import _ |
849 | from lp.answers.browser.faqtarget import FAQTargetNavigationMixin |
850 | from lp.answers.browser.questiontarget import QuestionTargetTraversalMixin |
851 | from lp.app.browser.launchpadform import ( |
852 | @@ -85,9 +80,6 @@ from lp.bugs.browser.structuralsubscription import ( |
853 | ) |
854 | from lp.buildmaster.interfaces.processor import IProcessorSet |
855 | from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin |
856 | -from lp.oci.interfaces.ociregistrycredentials import ( |
857 | - IOCIRegistryCredentialsSet, |
858 | - ) |
859 | from lp.registry.browser import ( |
860 | add_subscribe_link, |
861 | RegistryEditFormView, |
862 | @@ -1027,101 +1019,6 @@ class DistributionEditView(RegistryEditFormView, |
863 | """See `LaunchpadFormView`.""" |
864 | return 'Change %s details' % self.context.displayname |
865 | |
866 | - def createOCICredentials(self): |
867 | - return form.Fields( |
868 | - TextLine( |
869 | - __name__='oci_credentials_url', |
870 | - title=u"Registry URL", |
871 | - description=( |
872 | - u"URL for the OCI registry to upload images to." |
873 | - ), |
874 | - required=False), |
875 | - TextLine( |
876 | - __name__='oci_credentials_region', |
877 | - title=u"OCI registry region", |
878 | - description=u"Region for the OCI Registry.", |
879 | - required=False), |
880 | - TextLine( |
881 | - __name__='oci_credentials_username', |
882 | - title=u"OCI registry username", |
883 | - description=u"Username for the OCI Registry.", |
884 | - required=False), |
885 | - Password( |
886 | - __name__='oci_credentials_password', |
887 | - title=u"OCI registry password", |
888 | - description=u"Password for the OCI Registry.", |
889 | - required=False), |
890 | - Password( |
891 | - __name__='oci_credentials_confirm_password', |
892 | - title=u"Confirm password", |
893 | - required=False), |
894 | - Bool( |
895 | - __name__='oci_credentials_delete', |
896 | - title=u"Delete", |
897 | - description=u"Delete these credentials.", |
898 | - required=False,) |
899 | - ) |
900 | - |
901 | - def changeOCICredentials(self, data): |
902 | - delete = data.pop("oci_credentials_delete", None) |
903 | - if delete and self.context.oci_registry_credentials: |
904 | - credentials = self.context.oci_registry_credentials |
905 | - self.context.oci_registry_credentials = None |
906 | - credentials.destroySelf() |
907 | - return |
908 | - |
909 | - url = data.pop("oci_credentials_url", None) |
910 | - username = data.pop("oci_credentials_username", None) |
911 | - region = data.pop("oci_credentials_region", None) |
912 | - # validated against confirm password in validateOCICredentials |
913 | - password = data.pop("oci_credentials_password", None) |
914 | - if "oci_credentials_confirm_password" in data: |
915 | - del(data["oci_credentials_confirm_password"]) |
916 | - |
917 | - # If we're not deleting, but don't have a url, then don't do anything |
918 | - if not url: |
919 | - return |
920 | - |
921 | - current_credentials = self.context.oci_registry_credentials |
922 | - if current_credentials: |
923 | - current_credentials.url = url |
924 | - current_credentials.setCredentials({ |
925 | - "username": username, |
926 | - "password": password, |
927 | - "region": region}) |
928 | - return |
929 | - credentials = getUtility(IOCIRegistryCredentialsSet).new( |
930 | - self.context.owner, |
931 | - self.context.owner, |
932 | - url, |
933 | - {"username": username, |
934 | - "password": password, |
935 | - "region": region}) |
936 | - self.context.oci_registry_credentials = credentials |
937 | - |
938 | - def validateOCICredentials(self, data): |
939 | - # if we're deleting credentials, we don't need to validate |
940 | - if data.get("oci_credentials_delete"): |
941 | - return |
942 | - url = data.get("oci_credentials_url") |
943 | - username = data.get("oci_credentials_username") |
944 | - if username and not url: |
945 | - self.setFieldError( |
946 | - 'oci_credentials_url', |
947 | - _("A URL is required if a username is present.")) |
948 | - password = data.get("oci_credentials_password") |
949 | - confirm_password = data.get("oci_credentials_confirm_password") |
950 | - if password != confirm_password: |
951 | - self.setFieldError( |
952 | - "oci_credentials_password", |
953 | - _("Passwords must match.")) |
954 | - existing_credentials = self.context.oci_registry_credentials |
955 | - if existing_credentials and not url: |
956 | - self.setFieldError( |
957 | - "oci_credentials_url", |
958 | - _("URL must be specified. " |
959 | - "Delete credentials to unset URL.")) |
960 | - |
961 | def setUpFields(self): |
962 | """See `LaunchpadFormView`.""" |
963 | RegistryEditFormView.setUpFields(self) |
964 | @@ -1130,22 +1027,14 @@ class DistributionEditView(RegistryEditFormView, |
965 | getUtility(IProcessorSet).getAll(), |
966 | u"The architectures on which the distribution's main archive can " |
967 | u"build.") |
968 | - self.form_fields += self.createOCICredentials() |
969 | |
970 | @property |
971 | def initial_values(self): |
972 | - data = { |
973 | + return { |
974 | 'require_virtualized': |
975 | self.context.main_archive.require_virtualized, |
976 | 'processors': self.context.main_archive.processors, |
977 | } |
978 | - # Do OCI initial values |
979 | - oci_credentials = self.context.oci_registry_credentials |
980 | - if oci_credentials: |
981 | - data["oci_credentials_url"] = oci_credentials.url |
982 | - data["oci_credentials_username"] = oci_credentials.username |
983 | - data["oci_credentials_region"] = oci_credentials.region |
984 | - return data |
985 | |
986 | def validate(self, data): |
987 | """Constrain bug expiration to Launchpad Bugs tracker.""" |
988 | @@ -1155,7 +1044,6 @@ class DistributionEditView(RegistryEditFormView, |
989 | official_malone = data.get('official_malone', False) |
990 | if not official_malone: |
991 | data['enable_bug_expiration'] = False |
992 | - self.validateOCICredentials(data) |
993 | |
994 | def change_archive_fields(self, data): |
995 | # Update context.main_archive. |
996 | @@ -1175,7 +1063,6 @@ class DistributionEditView(RegistryEditFormView, |
997 | @action("Change", name='change') |
998 | def change_action(self, action, data): |
999 | self.change_archive_fields(data) |
1000 | - self.changeOCICredentials(data) |
1001 | self.updateContextFromData(data) |
1002 | |
1003 | |
1004 | diff --git a/lib/lp/registry/browser/tests/test_distribution_views.py b/lib/lp/registry/browser/tests/test_distribution_views.py |
1005 | index 195560d..fde5a8a 100644 |
1006 | --- a/lib/lp/registry/browser/tests/test_distribution_views.py |
1007 | +++ b/lib/lp/registry/browser/tests/test_distribution_views.py |
1008 | @@ -9,7 +9,6 @@ from zope.component import getUtility |
1009 | |
1010 | from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet |
1011 | from lp.buildmaster.interfaces.processor import IProcessorSet |
1012 | -from lp.oci.tests.helpers import OCIConfigHelperMixin |
1013 | from lp.registry.browser.distribution import DistributionPublisherConfigView |
1014 | from lp.registry.enums import DistributionDefaultTraversalPolicy |
1015 | from lp.registry.interfaces.distribution import IDistributionSet |
1016 | @@ -184,7 +183,7 @@ class TestDistroAddView(TestCaseWithFactory): |
1017 | self.assertContentEqual([], distribution.main_archive.processors) |
1018 | |
1019 | |
1020 | -class TestDistroEditView(OCIConfigHelperMixin, TestCaseWithFactory): |
1021 | +class TestDistroEditView(TestCaseWithFactory): |
1022 | """Test the +edit page for a distribution.""" |
1023 | |
1024 | layer = DatabaseFunctionalLayer |
1025 | @@ -194,7 +193,6 @@ class TestDistroEditView(OCIConfigHelperMixin, TestCaseWithFactory): |
1026 | self.admin = login_celebrity('admin') |
1027 | self.distribution = self.factory.makeDistribution() |
1028 | self.all_processors = getUtility(IProcessorSet).getAll() |
1029 | - self.setConfig() |
1030 | |
1031 | def test_edit_distro_init_value_require_virtualized(self): |
1032 | view = create_initialized_view( |
1033 | @@ -262,125 +260,6 @@ class TestDistroEditView(OCIConfigHelperMixin, TestCaseWithFactory): |
1034 | method="POST", form=edit_form) |
1035 | self.assertEqual(self.distribution.package_derivatives_email, email) |
1036 | |
1037 | - def test_oci_validation_username_no_url(self): |
1038 | - edit_form = self.getDefaultEditDict() |
1039 | - edit_form["field.oci_credentials_username"] = "username" |
1040 | - |
1041 | - view = create_initialized_view( |
1042 | - self.distribution, '+edit', principal=self.admin, |
1043 | - method='POST', form=edit_form) |
1044 | - self.assertEqual( |
1045 | - "A URL is required if a username is present.", |
1046 | - view.getFieldError("oci_credentials_url")) |
1047 | - |
1048 | - def test_oci_validation_different_passwords(self): |
1049 | - edit_form = self.getDefaultEditDict() |
1050 | - edit_form["field.oci_credentials_password"] = "password1" |
1051 | - edit_form["field.oci_credentials_confirm_password"] = "password2" |
1052 | - view = create_initialized_view( |
1053 | - self.distribution, '+edit', principal=self.admin, |
1054 | - method='POST', form=edit_form) |
1055 | - self.assertEqual( |
1056 | - "Passwords must match.", |
1057 | - view.getFieldError("oci_credentials_password")) |
1058 | - |
1059 | - def test_oci_validation_url_unset(self): |
1060 | - edit_form = self.getDefaultEditDict() |
1061 | - edit_form["field.oci_credentials_url"] = "" |
1062 | - |
1063 | - credentials = self.factory.makeOCIRegistryCredentials( |
1064 | - registrant=self.distribution.owner, |
1065 | - owner=self.distribution.owner) |
1066 | - self.distribution.oci_registry_credentials = credentials |
1067 | - |
1068 | - view = create_initialized_view( |
1069 | - self.distribution, '+edit', principal=self.admin, |
1070 | - method='POST', form=edit_form) |
1071 | - self.assertEqual( |
1072 | - "URL must be specified. Delete credentials to unset URL.", |
1073 | - view.getFieldError("oci_credentials_url")) |
1074 | - |
1075 | - def test_oci_create_credentials_url_only(self): |
1076 | - edit_form = self.getDefaultEditDict() |
1077 | - registry_url = self.factory.getUniqueURL() |
1078 | - edit_form["field.oci_credentials_url"] = registry_url |
1079 | - |
1080 | - create_initialized_view( |
1081 | - self.distribution, '+edit', principal=self.admin, |
1082 | - method='POST', form=edit_form) |
1083 | - self.assertEqual( |
1084 | - registry_url, self.distribution.oci_registry_credentials.url) |
1085 | - |
1086 | - def test_oci_create_credentials(self): |
1087 | - edit_form = self.getDefaultEditDict() |
1088 | - registry_url = self.factory.getUniqueURL() |
1089 | - username = self.factory.getUniqueUnicode() |
1090 | - password = self.factory.getUniqueUnicode() |
1091 | - edit_form["field.oci_credentials_url"] = registry_url |
1092 | - edit_form["field.oci_credentials_username"] = username |
1093 | - edit_form["field.oci_credentials_password"] = password |
1094 | - edit_form["field.oci_credentials_confirm_password"] = password |
1095 | - |
1096 | - create_initialized_view( |
1097 | - self.distribution, '+edit', principal=self.admin, |
1098 | - method='POST', form=edit_form) |
1099 | - self.assertEqual( |
1100 | - username, self.distribution.oci_registry_credentials.username) |
1101 | - |
1102 | - def test_oci_create_credentials_change_url(self): |
1103 | - edit_form = self.getDefaultEditDict() |
1104 | - credentials = self.factory.makeOCIRegistryCredentials( |
1105 | - registrant=self.distribution.owner, |
1106 | - owner=self.distribution.owner) |
1107 | - self.distribution.oci_registry_credentials = credentials |
1108 | - registry_url = self.factory.getUniqueURL() |
1109 | - edit_form["field.oci_credentials_url"] = registry_url |
1110 | - |
1111 | - create_initialized_view( |
1112 | - self.distribution, '+edit', principal=self.admin, |
1113 | - method='POST', form=edit_form) |
1114 | - self.assertEqual( |
1115 | - registry_url, self.distribution.oci_registry_credentials.url) |
1116 | - # This should have mutated, not created new credentials records |
1117 | - self.assertEqual( |
1118 | - credentials.id, self.distribution.oci_registry_credentials.id) |
1119 | - |
1120 | - def test_oci_create_credentials_change_password(self): |
1121 | - edit_form = self.getDefaultEditDict() |
1122 | - credentials = self.factory.makeOCIRegistryCredentials( |
1123 | - registrant=self.distribution.owner, |
1124 | - owner=self.distribution.owner) |
1125 | - self.distribution.oci_registry_credentials = credentials |
1126 | - password = self.factory.getUniqueUnicode() |
1127 | - edit_form["field.oci_credentials_url"] = credentials.url |
1128 | - edit_form["field.oci_credentials_username"] = credentials.username |
1129 | - edit_form["field.oci_credentials_password"] = password |
1130 | - edit_form["field.oci_credentials_confirm_password"] = password |
1131 | - |
1132 | - create_initialized_view( |
1133 | - self.distribution, '+edit', principal=self.admin, |
1134 | - method='POST', form=edit_form) |
1135 | - distro_credentials = self.distribution.oci_registry_credentials |
1136 | - unencrypted_credentials = distro_credentials.getCredentials() |
1137 | - self.assertEqual( |
1138 | - password, unencrypted_credentials["password"]) |
1139 | - # This should not have changed |
1140 | - self.assertEqual( |
1141 | - distro_credentials.url, credentials.url) |
1142 | - |
1143 | - def test_oci_delete_credentials(self): |
1144 | - edit_form = self.getDefaultEditDict() |
1145 | - credentials = self.factory.makeOCIRegistryCredentials( |
1146 | - registrant=self.distribution.owner, |
1147 | - owner=self.distribution.owner) |
1148 | - self.distribution.oci_registry_credentials = credentials |
1149 | - edit_form['field.oci_credentials_delete'] = 'on' |
1150 | - |
1151 | - create_initialized_view( |
1152 | - self.distribution, '+edit', principal=self.admin, |
1153 | - method='POST', form=edit_form) |
1154 | - self.assertIsNone(self.distribution.oci_registry_credentials) |
1155 | - |
1156 | |
1157 | class TestDistributionAdminView(TestCaseWithFactory): |
1158 | """Test the +admin page for a distribution.""" |
1159 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml |
1160 | index 7a7a396..7c2dad8 100644 |
1161 | --- a/lib/lp/registry/configure.zcml |
1162 | +++ b/lib/lp/registry/configure.zcml |
1163 | @@ -1833,7 +1833,6 @@ |
1164 | mirror_admin |
1165 | mugshot |
1166 | oci_project_admin |
1167 | - oci_registry_credentials |
1168 | official_answers |
1169 | official_blueprints |
1170 | official_malone |
1171 | diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py |
1172 | index 6d13dc5..8c23b90 100644 |
1173 | --- a/lib/lp/registry/interfaces/distribution.py |
1174 | +++ b/lib/lp/registry/interfaces/distribution.py |
1175 | @@ -73,7 +73,6 @@ from lp.bugs.interfaces.bugtarget import ( |
1176 | from lp.bugs.interfaces.structuralsubscription import ( |
1177 | IStructuralSubscriptionTarget, |
1178 | ) |
1179 | -from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials |
1180 | from lp.registry.enums import ( |
1181 | DistributionDefaultTraversalPolicy, |
1182 | VCSType, |
1183 | @@ -720,13 +719,6 @@ class IDistributionPublic( |
1184 | def newOCIProject(registrant, name, description=None): |
1185 | """Create an `IOCIProject` for this distro.""" |
1186 | |
1187 | - oci_registry_credentials = Reference( |
1188 | - IOCIRegistryCredentials, |
1189 | - title=_("OCI registry credentials"), |
1190 | - description=_("Credentials and URL to use for uploading all OCI " |
1191 | - "Images in this distribution to a registry."), |
1192 | - required=False, readonly=True) |
1193 | - |
1194 | |
1195 | @exported_as_webservice_entry(as_of="beta") |
1196 | class IDistribution( |
1197 | diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py |
1198 | index bc4690d..0db17cb 100644 |
1199 | --- a/lib/lp/registry/model/distribution.py |
1200 | +++ b/lib/lp/registry/model/distribution.py |
1201 | @@ -33,10 +33,6 @@ from storm.expr import ( |
1202 | SQL, |
1203 | ) |
1204 | from storm.info import ClassAlias |
1205 | -from storm.locals import ( |
1206 | - Int, |
1207 | - Reference, |
1208 | - ) |
1209 | from storm.store import Store |
1210 | from zope.component import getUtility |
1211 | from zope.interface import implementer |
1212 | @@ -271,9 +267,6 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements, |
1213 | enum=DistributionDefaultTraversalPolicy, notNull=False, |
1214 | default=DistributionDefaultTraversalPolicy.SERIES) |
1215 | redirect_default_traversal = BoolCol(notNull=False, default=False) |
1216 | - oci_registry_credentialsID = Int(name='oci_credentials', allow_none=True) |
1217 | - oci_registry_credentials = Reference( |
1218 | - oci_registry_credentialsID, "OCIRegistryCredentials.id") |
1219 | |
1220 | def __repr__(self): |
1221 | display_name = six.ensure_str( |
1222 | diff --git a/lib/lp/registry/templates/distribution-edit.pt b/lib/lp/registry/templates/distribution-edit.pt |
1223 | deleted file mode 100644 |
1224 | index a8562b9..0000000 |
1225 | --- a/lib/lp/registry/templates/distribution-edit.pt |
1226 | +++ /dev/null |
1227 | @@ -1,98 +0,0 @@ |
1228 | -<html |
1229 | - xmlns="http://www.w3.org/1999/xhtml" |
1230 | - xmlns:tal="http://xml.zope.org/namespaces/tal" |
1231 | - xmlns:metal="http://xml.zope.org/namespaces/metal" |
1232 | - xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
1233 | - metal:use-macro="view/macro:page/main_side" |
1234 | - i18n:domain="launchpad" |
1235 | -> |
1236 | -<body> |
1237 | - |
1238 | -<tal:main metal:fill-slot="main"> |
1239 | - |
1240 | - <div metal:use-macro="context/@@launchpad_form/form"> |
1241 | - <metal:formbody fill-slot="widgets"> |
1242 | - <table class="form"> |
1243 | - <tal:widget define="widget nocall:view/widgets/display_name"> |
1244 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1245 | - </tal:widget> |
1246 | - <tal:widget define="widget nocall:view/widgets/summary"> |
1247 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1248 | - </tal:widget> |
1249 | - <tal:widget define="widget nocall:view/widgets/description"> |
1250 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1251 | - </tal:widget> |
1252 | - <tal:widget define="widget nocall:view/widgets/bug_reporting_guidelines"> |
1253 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1254 | - </tal:widget> |
1255 | - <tal:widget define="widget nocall:view/widgets/bug_reported_acknowledgement"> |
1256 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1257 | - </tal:widget> |
1258 | - <tal:widget define="widget nocall:view/widgets/package_derivatives_email"> |
1259 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1260 | - </tal:widget> |
1261 | - <tal:widget define="widget nocall:view/widgets/icon"> |
1262 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1263 | - </tal:widget> |
1264 | - <tal:widget define="widget nocall:view/widgets/logo"> |
1265 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1266 | - </tal:widget> |
1267 | - <tal:widget define="widget nocall:view/widgets/mugshot"> |
1268 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1269 | - </tal:widget> |
1270 | - <tal:widget define="widget nocall:view/widgets/official_malone"> |
1271 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1272 | - </tal:widget> |
1273 | - <tal:widget define="widget nocall:view/widgets/enable_bug_expiration"> |
1274 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1275 | - </tal:widget> |
1276 | - <tal:widget define="widget nocall:view/widgets/blueprints_usage"> |
1277 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1278 | - </tal:widget> |
1279 | - <tal:widget define="widget nocall:view/widgets/translations_usage"> |
1280 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1281 | - </tal:widget> |
1282 | - <tal:widget define="widget nocall:view/widgets/answers_usage"> |
1283 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1284 | - </tal:widget> |
1285 | - <tal:widget define="widget nocall:view/widgets/translation_focus"> |
1286 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1287 | - </tal:widget> |
1288 | - <tal:widget define="widget nocall:view/widgets/default_traversal_policy"> |
1289 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1290 | - </tal:widget> |
1291 | - <tal:widget define="widget nocall:view/widgets/redirect_default_traversal"> |
1292 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
1293 | - </tal:widget> |
1294 | - |
1295 | - <tr> |
1296 | - <td><label>OCI registry credentials</label></td> |
1297 | - <tr> |
1298 | - <tr> |
1299 | - <td tal:define="widget nocall:view/widgets/oci_credentials_url"> |
1300 | - <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1301 | - </td> |
1302 | - <td tal:define="widget nocall:view/widgets/oci_credentials_region"> |
1303 | - <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1304 | - </td> |
1305 | - <td tal:define="widget nocall:view/widgets/oci_credentials_username"> |
1306 | - <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1307 | - </td> |
1308 | - <td tal:define="widget nocall:view/widgets/oci_credentials_password"> |
1309 | - <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1310 | - </td> |
1311 | - <td tal:define="widget nocall:view/widgets/oci_credentials_confirm_password"> |
1312 | - <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1313 | - </td> |
1314 | - <td tal:define="widget nocall:view/widgets/oci_credentials_delete"> |
1315 | - <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
1316 | - </td> |
1317 | - </tr> |
1318 | - </table> |
1319 | - </metal:formbody> |
1320 | - </div> |
1321 | - |
1322 | -</tal:main> |
1323 | - |
1324 | -</body> |
1325 | -</html> |
LGTM