Merge ~twom/launchpad:oci-policy-use-distribution-credentials-in-upload into launchpad:master

Proposed by Tom Wardill
Status: Merged
Approved by: Tom Wardill
Approved revision: 47f07b559546d48b07f1863cb6317559d9b75fe1
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~twom/launchpad:oci-policy-use-distribution-credentials-in-upload
Merge into: launchpad:master
Prerequisite: ~twom/launchpad:oci-policy-distribute-the-credentials
Diff against target: 614 lines (+323/-36)
11 files modified
lib/lp/oci/browser/ocirecipe.py (+37/-1)
lib/lp/oci/browser/tests/test_ocirecipe.py (+46/-1)
lib/lp/oci/configure.zcml (+12/-0)
lib/lp/oci/interfaces/ocipushrule.py (+1/-1)
lib/lp/oci/interfaces/ocirecipe.py (+13/-0)
lib/lp/oci/model/ocipushrule.py (+22/-0)
lib/lp/oci/model/ocirecipe.py (+40/-6)
lib/lp/oci/templates/ocirecipe-index.pt (+41/-27)
lib/lp/oci/templates/ocirecipe-new.pt (+5/-0)
lib/lp/oci/tests/test_ocirecipe.py (+56/-0)
lib/lp/oci/tests/test_ociregistryclient.py (+50/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+395984@code.launchpad.net

This proposal supersedes a proposal from 2021-01-08.

Commit message

Use distribution credentials in OCI upload

To post a comment you must log in.
f5fd69e... by Tom Wardill

Remove accidental import

353dbae... by Tom Wardill

Fix index template

a31a614... by Tom Wardill

Integrate official status checks into use of distribution credentials

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
0e19af2... by Tom Wardill

Fix tests to work without OCI config

02d8de1... by Tom Wardill

oci_project over project

Revision history for this message
Tom Wardill (twom) :
bd75daa... by Tom Wardill

Wording fixes

51143cc... by Tom Wardill

Update tests for wording fixes

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
47f07b5... by Tom Wardill

Use None as sentinel

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
2index 4f4253a..55d17b8 100644
3--- a/lib/lp/oci/browser/ocirecipe.py
4+++ b/lib/lp/oci/browser/ocirecipe.py
5@@ -254,6 +254,15 @@ class OCIRecipeView(LaunchpadView):
6 "%s=%s" % (k, v)
7 for k, v in sorted(self.context.build_args.items()))
8
9+ @property
10+ def distribution_has_credentials(self):
11+ if hasattr(self.context, 'oci_project'):
12+ oci_project = self.context.oci_project
13+ else:
14+ oci_project = self.context
15+ distro = oci_project.distribution
16+ return bool(distro and distro.oci_registry_credentials)
17+
18
19 def builds_for_recipe(recipe):
20 """A list of interesting builds.
21@@ -765,6 +774,15 @@ class OCIRecipeFormMixin:
22 )
23 return False, message
24
25+ @property
26+ def distribution_has_credentials(self):
27+ if hasattr(self.context, 'oci_project'):
28+ oci_project = self.context.oci_project
29+ else:
30+ oci_project = self.context
31+ distro = oci_project.distribution
32+ return bool(distro and distro.oci_registry_credentials)
33+
34
35 class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
36 OCIRecipeFormMixin):
37@@ -808,6 +826,14 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
38 "May only be enabled by the owner of the OCI Project."),
39 default=False,
40 required=False, readonly=False))
41+ if self.distribution_has_credentials:
42+ self.form_fields += FormFields(TextLine(
43+ __name__='image_name',
44+ title=u"Image name",
45+ description=(
46+ "Name to use for registry upload. "
47+ "Defaults to the name of the recipe."),
48+ required=False, readonly=False))
49
50 def setUpGitRefWidget(self):
51 """Setup GitRef widget indicating the user to use the default
52@@ -886,7 +912,9 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
53 build_file=data["build_file"], description=data["description"],
54 build_daily=data["build_daily"], build_args=data["build_args"],
55 build_path=data["build_path"], processors=data["processors"],
56- official=data.get('official_recipe', False))
57+ official=data.get('official_recipe', False),
58+ # image_name is only available if using distribution credentials.
59+ image_name=data.get("image_name"))
60 self.next_url = canonical_url(recipe)
61
62
63@@ -1011,6 +1039,14 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
64 "May only be enabled by the owner of the OCI Project."),
65 default=self.context.official,
66 required=False, readonly=False))
67+ if self.distribution_has_credentials:
68+ self.form_fields += FormFields(TextLine(
69+ __name__='image_name',
70+ title=u"Image name",
71+ description=(
72+ "Name to use for registry upload. "
73+ "Defaults to the name of the recipe."),
74+ required=False, readonly=False))
75
76 def validate(self, data):
77 """See `LaunchpadFormView`."""
78diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
79index 970c49c..c473f4b 100644
80--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
81+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
82@@ -151,7 +151,7 @@ class BaseTestOCIRecipeView(BrowserTestCase):
83 name="test-person", displayname="Test Person")
84
85
86-class TestOCIRecipeAddView(BaseTestOCIRecipeView):
87+class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
88
89 def setUp(self):
90 super(TestOCIRecipeAddView, self).setUp()
91@@ -162,6 +162,7 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
92 "oci.build_series.%s" % self.distribution.name:
93 self.distroseries.name,
94 }))
95+ self.setConfig()
96
97 def setUpDistroSeries(self):
98 """Set up self.distroseries with some available processors."""
99@@ -263,6 +264,29 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
100 "Build-time\nARG variables:\nVAR1=10\nVAR2=20",
101 MatchesTagText(content, "build-args"))
102
103+ def test_create_new_recipe_with_image_name(self):
104+ oci_project = self.factory.makeOCIProject()
105+ credentials = self.factory.makeOCIRegistryCredentials()
106+ with person_logged_in(oci_project.distribution.owner):
107+ oci_project.distribution.oci_registry_credentials = credentials
108+ [git_ref] = self.factory.makeGitRefs(
109+ paths=['/refs/heads/v2.0-20.04'])
110+ browser = self.getViewBrowser(
111+ oci_project, view_name="+new-recipe", user=self.person)
112+ browser.getControl(name="field.name").value = "recipe-name"
113+ browser.getControl("Description").value = "Recipe description"
114+ browser.getControl(name="field.git_ref.repository").value = (
115+ git_ref.repository.identity)
116+ browser.getControl(name="field.git_ref.path").value = git_ref.path
117+
118+ image_name = self.factory.getUniqueUnicode()
119+ browser.getControl(name="field.image_name").value = image_name
120+ browser.getControl("Create OCI recipe").click()
121+ content = find_main_content(browser.contents)
122+ self.assertThat(
123+ "Registry image name:\n{}".format(image_name),
124+ MatchesTagText(content, "image-name"))
125+
126 def test_create_new_recipe_users_teams_as_owner_options(self):
127 # Teams that the user is in are options for the OCI recipe owner.
128 self.factory.makeTeam(
129@@ -741,6 +765,27 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
130 IStore(recipe).reload(recipe)
131 self.assertEqual({"VAR1": "xxx", "VAR2": "uu"}, recipe.build_args)
132
133+ def test_edit_image_name(self):
134+ self.setUpDistroSeries()
135+ credentials = self.factory.makeOCIRegistryCredentials()
136+ with person_logged_in(self.distribution.owner):
137+ self.distribution.oci_registry_credentials = credentials
138+ oci_project = self.factory.makeOCIProject(pillar=self.distribution)
139+ recipe = self.factory.makeOCIRecipe(
140+ registrant=self.person, owner=self.person,
141+ oci_project=oci_project)
142+ oci_project.setOfficialRecipeStatus(recipe, True)
143+ browser = self.getViewBrowser(
144+ recipe, view_name="+edit", user=recipe.owner)
145+ image_name = self.factory.getUniqueUnicode()
146+ field = browser.getControl(name="field.image_name")
147+ field.value = image_name
148+ browser.getControl("Update OCI recipe").click()
149+ content = find_main_content(browser.contents)
150+ self.assertThat(
151+ "Registry image name:\n{}".format(image_name),
152+ MatchesTagText(content, "image-name"))
153+
154 def test_edit_with_invisible_processor(self):
155 # It's possible for existing recipes to have an enabled processor
156 # that's no longer usable with the current distroseries, which will
157diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml
158index 9a887b2..1f0e284 100644
159--- a/lib/lp/oci/configure.zcml
160+++ b/lib/lp/oci/configure.zcml
161@@ -163,6 +163,18 @@
162 interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleSet"/>
163 </securedutility>
164
165+ <!-- OCIDistributionPushRule -->
166+ <class class="lp.oci.model.ocipushrule.OCIDistributionPushRule">
167+ <require
168+ permission="launchpad.View"
169+ interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleView
170+ lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" />
171+ <require
172+ permission="launchpad.Edit"
173+ interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleEdit"
174+ set_schema="lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" />
175+ </class>
176+
177 <!-- OCI related jobs -->
178 <securedutility
179 component="lp.oci.model.ocirecipebuildjob.OCIRegistryUploadJob"
180diff --git a/lib/lp/oci/interfaces/ocipushrule.py b/lib/lp/oci/interfaces/ocipushrule.py
181index 67db23e..15dddb4 100644
182--- a/lib/lp/oci/interfaces/ocipushrule.py
183+++ b/lib/lp/oci/interfaces/ocipushrule.py
184@@ -52,7 +52,7 @@ class IOCIPushRuleView(Interface):
185 permission.
186 """
187
188- id = Int(title=_("ID"), required=True, readonly=True)
189+ id = Int(title=_("ID"), required=False, readonly=True)
190
191 registry_url = exported(TextLine(
192 title=_("Registry URL"),
193diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
194index 633831f..a23fa03 100644
195--- a/lib/lp/oci/interfaces/ocirecipe.py
196+++ b/lib/lp/oci/interfaces/ocirecipe.py
197@@ -286,6 +286,11 @@ class IOCIRecipeView(Interface):
198 description=_("Whether the git branch name is the correct "
199 "format for using as a tag name."))
200
201+ use_distribution_credentials = Bool(
202+ title=_("Use Distribution credentials"), required=True, readonly=True,
203+ description=_("Use the credentials on a Distribution for "
204+ "registry upload"))
205+
206 def requestBuild(requester, architecture):
207 """Request that the OCI recipe is built.
208
209@@ -440,6 +445,14 @@ class IOCIRecipeEditableAttributes(IHasOwner):
210 description=_("If True, this recipe should be built daily."),
211 readonly=False))
212
213+ image_name = exported(TextLine(
214+ title=_("Image name"),
215+ description=_("Image name to use on upload to registry. "
216+ "Defaults to recipe name if not set. "
217+ "Only used when Distribution credentials are set."),
218+ required=False,
219+ readonly=False))
220+
221
222 class IOCIRecipeAdminAttributes(Interface):
223 """`IOCIRecipe` attributes that can be edited by admins.
224diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py
225index 24d5e27..ad9dd30 100644
226--- a/lib/lp/oci/model/ocipushrule.py
227+++ b/lib/lp/oci/model/ocipushrule.py
228@@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals
229
230 __metaclass__ = type
231 __all__ = [
232+ 'OCIDistributionPushRule',
233 'OCIPushRule',
234 'OCIPushRuleSet',
235 ]
236@@ -72,6 +73,27 @@ class OCIPushRule(Storm):
237 IStore(OCIPushRule).remove(self)
238
239
240+@implementer(IOCIPushRule)
241+class OCIDistributionPushRule:
242+ """A non-database instance that is synthesised from data elsewhere."""
243+
244+ registry_credentials = None
245+
246+ def __init__(self, recipe, registry_credentials, image_name):
247+ self.id = None # This is not a database instance
248+ self.recipe = recipe
249+ self.registry_credentials = registry_credentials
250+ self.image_name = image_name
251+
252+ @property
253+ def registry_url(self):
254+ return self.registry_credentials.url
255+
256+ @property
257+ def username(self):
258+ return self.registry_credentials.username
259+
260+
261 @implementer(IOCIPushRuleSet)
262 class OCIPushRuleSet:
263
264diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
265index fd76ae6..28ce8a0 100644
266--- a/lib/lp/oci/model/ocirecipe.py
267+++ b/lib/lp/oci/model/ocirecipe.py
268@@ -76,7 +76,10 @@ from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource
269 from lp.oci.interfaces.ociregistrycredentials import (
270 IOCIRegistryCredentialsSet,
271 )
272-from lp.oci.model.ocipushrule import OCIPushRule
273+from lp.oci.model.ocipushrule import (
274+ OCIDistributionPushRule,
275+ OCIPushRule,
276+ )
277 from lp.oci.model.ocirecipebuild import OCIRecipeBuild
278 from lp.oci.model.ocirecipejob import OCIRecipeJob
279 from lp.registry.interfaces.distribution import IDistributionSet
280@@ -162,10 +165,13 @@ class OCIRecipe(Storm, WebhookTargetMixin):
281
282 build_daily = Bool(name="build_daily", default=False)
283
284+ _image_name = Unicode(name="image_name", allow_none=True)
285+
286 def __init__(self, name, registrant, owner, oci_project, git_ref,
287 description=None, official=False, require_virtualized=True,
288 build_file=None, build_daily=False, date_created=DEFAULT,
289- allow_internet=True, build_args=None, build_path=None):
290+ allow_internet=True, build_args=None, build_path=None,
291+ image_name=None):
292 if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE):
293 raise OCIRecipeFeatureDisabled()
294 super(OCIRecipe, self).__init__()
295@@ -184,6 +190,7 @@ class OCIRecipe(Storm, WebhookTargetMixin):
296 self.allow_internet = allow_internet
297 self.build_args = build_args or {}
298 self.build_path = build_path
299+ self.image_name = image_name
300
301 def __repr__(self):
302 return "<OCIRecipe ~%s/%s/+oci/%s/+recipe/%s>" % (
303@@ -469,10 +476,18 @@ class OCIRecipe(Storm, WebhookTargetMixin):
304
305 @property
306 def push_rules(self):
307+ # if we're in a distribution that has credentials set at that level
308+ # create a push rule using those credentials
309+ if self.use_distribution_credentials:
310+ push_rule = OCIDistributionPushRule(
311+ self,
312+ self.oci_project.distribution.oci_registry_credentials,
313+ self.image_name)
314+ return [push_rule]
315 rules = IStore(self).find(
316 OCIPushRule,
317 OCIPushRule.recipe == self.id)
318- return rules
319+ return list(rules)
320
321 @property
322 def _pending_states(self):
323@@ -533,7 +548,25 @@ class OCIRecipe(Storm, WebhookTargetMixin):
324
325 @property
326 def can_upload_to_registry(self):
327- return not self.push_rules.is_empty()
328+ return bool(self.push_rules)
329+
330+ @property
331+ def use_distribution_credentials(self):
332+ distribution = self.oci_project.distribution
333+ # if we're not in a distribution, we can't use those credentials...
334+ if not distribution:
335+ return False
336+ official = self.official
337+ credentials = distribution.oci_registry_credentials
338+ return bool(distribution and official and credentials)
339+
340+ @property
341+ def image_name(self):
342+ return self._image_name or self.name
343+
344+ @image_name.setter
345+ def image_name(self, value):
346+ self._image_name = value
347
348 def newPushRule(self, registrant, registry_url, image_name, credentials,
349 credentials_owner=None):
350@@ -575,7 +608,8 @@ class OCIRecipeSet:
351 def new(self, name, registrant, owner, oci_project, git_ref, build_file,
352 description=None, official=False, require_virtualized=True,
353 build_daily=False, processors=None, date_created=DEFAULT,
354- allow_internet=True, build_args=None, build_path=None):
355+ allow_internet=True, build_args=None, build_path=None,
356+ image_name=None):
357 """See `IOCIRecipeSet`."""
358 if not registrant.inTeam(owner):
359 if owner.is_team:
360@@ -600,7 +634,7 @@ class OCIRecipeSet:
361 oci_recipe = OCIRecipe(
362 name, registrant, owner, oci_project, git_ref, description,
363 official, require_virtualized, build_file, build_daily,
364- date_created, allow_internet, build_args, build_path)
365+ date_created, allow_internet, build_args, build_path, image_name)
366 store.add(oci_recipe)
367
368 if processors is None:
369diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt
370index a3e67bb..b575488 100644
371--- a/lib/lp/oci/templates/ocirecipe-index.pt
372+++ b/lib/lp/oci/templates/ocirecipe-index.pt
373@@ -88,6 +88,12 @@
374 <span tal:condition="not: context/official">No</span>
375 </dd>
376 </dl>
377+ <dl id="image-name" tal:condition="view/distribution_has_credentials">
378+ <dt>Registry image name:</dt>
379+ <dd>
380+ <span tal:content="context/image_name" />
381+ </dd>
382+ </dl>
383 </div>
384
385 <h2>Latest builds</h2>
386@@ -151,35 +157,43 @@
387 </div>
388
389
390- <h2>Recipe push rules</h2>
391- <table id="push-rules-listing" tal:condition="view/has_push_rules" class="listing"
392- style="margin-bottom: 1em; ">
393- <thead>
394- <tr>
395- <th>Registry URL</th>
396- <th>Username</th>
397- <th>Image Name</th>
398- </tr>
399- </thead>
400- <tbody>
401- <tal:recipe-push-rules repeat="item view/push_rules">
402- <tr tal:define="rule item;
403- show_credentials rule/registry_credentials/required:launchpad.View"
404- tal:attributes="id string:rule-${rule/id}">
405- <td tal:content="python: rule.registry_credentials.url if show_credentials else ''"/>
406- <td tal:content="python: rule.registry_credentials.username if show_credentials else ''"/>
407- <td tal:content="rule/image_name"/>
408+ <div tal:condition="view/distribution_has_credentials">
409+ <h3>Registry upload</h3>
410+ <p tal:condition="context/use_distribution_credentials">This recipe will use the registry credentials set by the parent distribution.</p>
411+ <p tal:condition="not: context/use_distribution_credentials">This is not an official recipe for the OCI project, and will not be uploaded to a registry.</p>
412+ </div>
413+
414+ <div tal:condition="not: view/distribution_has_credentials">
415+ <h2>Recipe push rules</h2>
416+ <table id="push-rules-listing" tal:condition="view/has_push_rules" class="listing"
417+ style="margin-bottom: 1em; ">
418+ <thead>
419+ <tr>
420+ <th>Registry URL</th>
421+ <th>Username</th>
422+ <th>Image Name</th>
423 </tr>
424- </tal:recipe-push-rules>
425- </tbody>
426- </table>
427- <p tal:condition="not: view/has_push_rules">
428- This OCI recipe has no push rules defined yet.
429- </p>
430+ </thead>
431+ <tbody>
432+ <tal:recipe-push-rules repeat="item view/push_rules">
433+ <tr tal:define="rule item;
434+ show_credentials rule/registry_credentials/required:launchpad.View"
435+ tal:attributes="id string:rule-${rule/id}">
436+ <td tal:content="python: rule.registry_credentials.url if show_credentials else ''"/>
437+ <td tal:content="python: rule.registry_credentials.username if show_credentials else ''"/>
438+ <td tal:content="rule/image_name"/>
439+ </tr>
440+ </tal:recipe-push-rules>
441+ </tbody>
442+ </table>
443+ <p tal:condition="not: view/has_push_rules">
444+ This OCI recipe has no push rules defined yet.
445+ </p>
446
447- <div tal:define="link context/menu:context/edit_push_rules"
448- tal:condition="link/enabled">
449- <tal:edit-push-rules replace="structure link/fmt:link"/>
450+ <div tal:define="link context/menu:context/edit_push_rules"
451+ tal:condition="link/enabled">
452+ <tal:edit-push-rules replace="structure link/fmt:link"/>
453+ </div>
454 </div>
455
456 </div>
457diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt
458index 00ed4e2..668c33d 100644
459--- a/lib/lp/oci/templates/ocirecipe-new.pt
460+++ b/lib/lp/oci/templates/ocirecipe-new.pt
461@@ -44,6 +44,11 @@
462 <tal:widget define="widget nocall:view/widgets/official_recipe">
463 <metal:block use-macro="context/@@launchpad_form/widget_row" />
464 </tal:widget>
465+ <span tal:condition="view/distribution_has_credentials">
466+ <tal:widget define="widget nocall:view/widgets/image_name" >
467+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
468+ </tal:widget>
469+ </span>
470 </table>
471 </metal:formbody>
472 </div>
473diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
474index 1cb69af..3486d57 100644
475--- a/lib/lp/oci/tests/test_ocirecipe.py
476+++ b/lib/lp/oci/tests/test_ocirecipe.py
477@@ -750,6 +750,39 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
478 "VAR3": "A string",
479 }, recipe.build_args)
480
481+ def test_use_distribution_credentials_set(self):
482+ self.setConfig()
483+ distribution = self.factory.makeDistribution()
484+ credentials = self.factory.makeOCIRegistryCredentials()
485+ with person_logged_in(distribution.owner):
486+ distribution.oci_registry_credentials = credentials
487+ project = self.factory.makeOCIProject(pillar=distribution)
488+ recipe = self.factory.makeOCIRecipe(oci_project=project)
489+ with person_logged_in(distribution.owner):
490+ project.setOfficialRecipeStatus(recipe, True)
491+ self.assertTrue(recipe.use_distribution_credentials)
492+
493+ def test_use_distribution_credentials_not_set(self):
494+ distribution = self.factory.makeDistribution()
495+ project = self.factory.makeOCIProject(pillar=distribution)
496+ recipe = self.factory.makeOCIRecipe(oci_project=project)
497+ self.assertFalse(recipe.use_distribution_credentials)
498+
499+ def test_image_name_set(self):
500+ distribution = self.factory.makeDistribution()
501+ project = self.factory.makeOCIProject(pillar=distribution)
502+ recipe = self.factory.makeOCIRecipe(oci_project=project)
503+ image_name = self.factory.getUniqueUnicode()
504+ with person_logged_in(recipe.owner):
505+ recipe.image_name = image_name
506+ self.assertEqual(image_name, removeSecurityProxy(recipe)._image_name)
507+
508+ def test_image_name_not_set(self):
509+ distribution = self.factory.makeDistribution()
510+ project = self.factory.makeOCIProject(pillar=distribution)
511+ recipe = self.factory.makeOCIRecipe(oci_project=project)
512+ self.assertEqual(recipe.name, recipe.image_name)
513+
514
515 class TestOCIRecipeProcessors(TestCaseWithFactory):
516
517@@ -1307,6 +1340,29 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
518 self.assertEqual(
519 image_name, push_rules["entries"][0]["image_name"])
520
521+ def test_api_set_image_name(self):
522+ """Can you set and retrieve the image name via the API?"""
523+ self.setConfig()
524+
525+ image_name = self.factory.getUniqueUnicode()
526+
527+ with person_logged_in(self.person):
528+ oci_project = self.factory.makeOCIProject(
529+ registrant=self.person)
530+ recipe = self.factory.makeOCIRecipe(
531+ oci_project=oci_project, owner=self.person,
532+ registrant=self.person)
533+ url = api_url(recipe)
534+
535+ resp = self.webservice.patch(
536+ url, 'application/json',
537+ json.dumps({'image_name': image_name}))
538+
539+ self.assertEqual(209, resp.status, resp.body)
540+
541+ ws_project = self.load_from_api(url)
542+ self.assertEqual(image_name, ws_project['image_name'])
543+
544
545 class TestOCIRecipeAsyncWebservice(TestCaseWithFactory):
546 layer = LaunchpadFunctionalLayer
547diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py
548index aa71aa3..dcb2a8c 100644
549--- a/lib/lp/oci/tests/test_ociregistryclient.py
550+++ b/lib/lp/oci/tests/test_ociregistryclient.py
551@@ -69,6 +69,7 @@ from lp.services.compat import mock
552 from lp.services.features.testing import FeatureFixture
553 from lp.testing import (
554 admin_logged_in,
555+ person_logged_in,
556 TestCaseWithFactory,
557 )
558 from lp.testing.fixture import ZopeUtilityFixture
559@@ -264,6 +265,55 @@ class TestOCIRegistryClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
560 self.assertEqual(0, len(responses.calls))
561
562 @responses.activate
563+ def test_upload_with_distribution_credentials(self):
564+ self._makeFiles()
565+ self.useFixture(MockPatch(
566+ "lp.oci.model.ociregistryclient.OCIRegistryClient._upload"))
567+ self.useFixture(MockPatch(
568+ "lp.oci.model.ociregistryclient.OCIRegistryClient._upload_layer",
569+ return_value=999))
570+ credentials = self.factory.makeOCIRegistryCredentials()
571+ image_name = self.factory.getUniqueUnicode()
572+ self.build.recipe.image_name = image_name
573+ distro = self.build.recipe.oci_project.distribution
574+ with person_logged_in(distro.owner):
575+ distro.oci_registry_credentials = credentials
576+ # we have distribution credentials, we should have a 'push rule'
577+ push_rule = self.build.recipe.push_rules[0]
578+ responses.add("GET", "%s/v2/" % push_rule.registry_url, status=200)
579+ self.addManifestResponses(push_rule)
580+
581+ self.client.upload(self.build)
582+
583+ request = json.loads(responses.calls[1].request.body)
584+
585+ self.assertThat(request, MatchesDict({
586+ "layers": MatchesListwise([
587+ MatchesDict({
588+ "mediaType": Equals(
589+ "application/vnd.docker.image.rootfs.diff.tar.gzip"),
590+ "digest": Equals("diff_id_1"),
591+ "size": Equals(999)}),
592+ MatchesDict({
593+ "mediaType": Equals(
594+ "application/vnd.docker.image.rootfs.diff.tar.gzip"),
595+ "digest": Equals("diff_id_2"),
596+ "size": Equals(999)})
597+ ]),
598+ "schemaVersion": Equals(2),
599+ "config": MatchesDict({
600+ "mediaType": Equals(
601+ "application/vnd.docker.container.image.v1+json"),
602+ "digest": Equals(
603+ "sha256:33b69b4b6e106f9fc7a8b93409"
604+ "36c85cf7f84b2d017e7b55bee6ab214761f6ab"),
605+ "size": Equals(52)
606+ }),
607+ "mediaType": Equals(
608+ "application/vnd.docker.distribution.manifest.v2+json")
609+ }))
610+
611+ @responses.activate
612 def test_upload_formats_credentials(self):
613 self._makeFiles()
614 _upload_fixture = self.useFixture(MockPatch(

Subscribers

People subscribed via source and target branches

to status/vote changes: