Merge ~twom/launchpad:oci-policy-tags-hang-off-branches into launchpad:master

Proposed by Tom Wardill
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)
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.

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

LGTM

review: Approve
Revision history for this message
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?

review: Needs Information
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/database/schema/patch-2210-24-0.sql b/database/schema/patch-2210-24-0.sql
2deleted file mode 100644
3index 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);
18diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
19index 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`."""
79diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
80index 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
139diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml
140index 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"
162diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
163index 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.
195diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py
196index 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
235diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
236index 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:
356diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py
357index 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):
505diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt
506index 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>
592diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt
593index 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>
608diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
609index 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
703diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py
704index 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()
818diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
819index 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"
831diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py
832index 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
1004diff --git a/lib/lp/registry/browser/tests/test_distribution_views.py b/lib/lp/registry/browser/tests/test_distribution_views.py
1005index 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."""
1159diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
1160index 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
1171diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py
1172index 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(
1197diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
1198index 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(
1222diff --git a/lib/lp/registry/templates/distribution-edit.pt b/lib/lp/registry/templates/distribution-edit.pt
1223deleted file mode 100644
1224index 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>

Subscribers

People subscribed via source and target branches

to status/vote changes: