launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #25927
[Merge] ~twom/launchpad:oci-policy-distribute-the-credentials into launchpad:master
Tom Wardill has proposed merging ~twom/launchpad:oci-policy-distribute-the-credentials into launchpad:master with ~twom/launchpad:db-oci-policy-distribute-the-credentials as a prerequisite.
Commit message:
Add OCI Credentials to the Distribution Edit page
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~twom/launchpad/+git/launchpad/+merge/395788
Use the OCIRegistryCredentials reference in Distribution to allow adding credentials to an Distribution.
UI similar to existing credentials management, but with only a single row.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~twom/launchpad:oci-policy-distribute-the-credentials into launchpad:master.
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index 7c97dc5..6be995f 100644
--- a/lib/lp/registry/browser/configure.zcml
+++ b/lib/lp/registry/browser/configure.zcml
@@ -2275,7 +2275,7 @@
for="lp.registry.interfaces.distribution.IDistribution"
class="lp.registry.browser.distribution.DistributionEditView"
permission="launchpad.Edit"
- template="../../app/templates/generic-edit.pt"
+ template="../templates/distribution-edit.pt"
/>
<browser:page
name="+admin"
diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py
index c499689..25950ae 100644
--- a/lib/lp/registry/browser/distribution.py
+++ b/lib/lp/registry/browser/distribution.py
@@ -49,10 +49,15 @@ from zope.formlib.boolwidgets import CheckBoxWidget
from zope.formlib.widget import CustomWidgetFactory
from zope.interface import implementer
from zope.lifecycleevent import ObjectCreatedEvent
-from zope.schema import Bool
+from zope.schema import (
+ Bool,
+ Password,
+ TextLine,
+ )
from zope.security.checker import canWrite
from zope.security.interfaces import Unauthorized
+from lp import _
from lp.answers.browser.faqtarget import FAQTargetNavigationMixin
from lp.answers.browser.questiontarget import QuestionTargetTraversalMixin
from lp.app.browser.launchpadform import (
@@ -80,6 +85,9 @@ from lp.bugs.browser.structuralsubscription import (
)
from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
+from lp.oci.interfaces.ociregistrycredentials import (
+ IOCIRegistryCredentialsSet,
+ )
from lp.registry.browser import (
add_subscribe_link,
RegistryEditFormView,
@@ -1019,6 +1027,101 @@ class DistributionEditView(RegistryEditFormView,
"""See `LaunchpadFormView`."""
return 'Change %s details' % self.context.displayname
+ def createOCICredentials(self):
+ return form.Fields(
+ TextLine(
+ __name__='oci_credentials_url',
+ title=u"Registry URL",
+ description=(
+ u"URL for the OCI registry to upload images to."
+ ),
+ required=False),
+ TextLine(
+ __name__='oci_credentials_region',
+ title=u"OCI registry region",
+ description=u"Region for the OCI Registry.",
+ required=False),
+ TextLine(
+ __name__='oci_credentials_username',
+ title=u"OCI registry username",
+ description=u"Username for the OCI Registry.",
+ required=False),
+ Password(
+ __name__='oci_credentials_password',
+ title=u"OCI registry password",
+ description=u"Password for the OCI Registry.",
+ required=False),
+ Password(
+ __name__='oci_credentials_confirm_password',
+ title=u"Confirm password",
+ required=False),
+ Bool(
+ __name__='oci_credentials_delete',
+ title=u"Delete",
+ description=u"Delete these credentials.",
+ required=False,)
+ )
+
+ def changeOCICredentials(self, data):
+ delete = data.pop("oci_credentials_delete", None)
+ if delete and self.context.oci_registry_credentials:
+ credentials = self.context.oci_registry_credentials
+ self.context.oci_registry_credentials = None
+ credentials.destroySelf()
+ return
+
+ url = data.pop("oci_credentials_url", None)
+ username = data.pop("oci_credentials_username", None)
+ region = data.pop("oci_credentials_region", None)
+ # validated against confirm password in validateOCICredentials
+ password = data.pop("oci_credentials_password", None)
+ if "oci_credentials_confirm_password" in data:
+ del(data["oci_credentials_confirm_password"])
+
+ # If we're not deleting, but don't have a url, then don't do anything
+ if not url:
+ return
+
+ current_credentials = self.context.oci_registry_credentials
+ if current_credentials:
+ current_credentials.url = url
+ current_credentials.setCredentials({
+ "username": username,
+ "password": password,
+ "region": region})
+ return
+ credentials = getUtility(IOCIRegistryCredentialsSet).new(
+ self.context.owner,
+ self.context.owner,
+ url,
+ {"username": username,
+ "password": password,
+ "region": region})
+ self.context.oci_registry_credentials = credentials
+
+ def validateOCICredentials(self, data):
+ # if we're deleting credentials, we don't need to validate
+ if data.get("oci_credentials_delete"):
+ return
+ url = data.get("oci_credentials_url")
+ username = data.get("oci_credentials_username")
+ if username and not url:
+ self.setFieldError(
+ 'oci_credentials_url',
+ _("A URL is required if a username is present."))
+ password = data.get("oci_credentials_password")
+ confirm_password = data.get("oci_credentials_confirm_password")
+ if password != confirm_password:
+ self.setFieldError(
+ "oci_credentials_password",
+ _("Passwords must match."))
+ existing_credentials = self.context.oci_registry_credentials
+ if existing_credentials and not url:
+ self.setFieldError(
+ "oci_credentials_url",
+ _("URL must be specified. "
+ "Delete credentials to unset URL."))
+
def setUpFields(self):
"""See `LaunchpadFormView`."""
RegistryEditFormView.setUpFields(self)
@@ -1027,14 +1130,22 @@ class DistributionEditView(RegistryEditFormView,
getUtility(IProcessorSet).getAll(),
u"The architectures on which the distribution's main archive can "
u"build.")
+ self.form_fields += self.createOCICredentials()
@property
def initial_values(self):
- return {
+ data = {
'require_virtualized':
self.context.main_archive.require_virtualized,
'processors': self.context.main_archive.processors,
}
+ # Do OCI initial values
+ oci_credentials = self.context.oci_registry_credentials
+ if oci_credentials:
+ data["oci_credentials_url"] = oci_credentials.url
+ data["oci_credentials_username"] = oci_credentials.username
+ data["oci_credentials_region"] = oci_credentials.region
+ return data
def validate(self, data):
"""Constrain bug expiration to Launchpad Bugs tracker."""
@@ -1044,6 +1155,7 @@ class DistributionEditView(RegistryEditFormView,
official_malone = data.get('official_malone', False)
if not official_malone:
data['enable_bug_expiration'] = False
+ self.validateOCICredentials(data)
def change_archive_fields(self, data):
# Update context.main_archive.
@@ -1063,6 +1175,7 @@ class DistributionEditView(RegistryEditFormView,
@action("Change", name='change')
def change_action(self, action, data):
self.change_archive_fields(data)
+ self.changeOCICredentials(data)
self.updateContextFromData(data)
diff --git a/lib/lp/registry/browser/tests/test_distribution_views.py b/lib/lp/registry/browser/tests/test_distribution_views.py
index fde5a8a..195560d 100644
--- a/lib/lp/registry/browser/tests/test_distribution_views.py
+++ b/lib/lp/registry/browser/tests/test_distribution_views.py
@@ -9,6 +9,7 @@ from zope.component import getUtility
from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
from lp.buildmaster.interfaces.processor import IProcessorSet
+from lp.oci.tests.helpers import OCIConfigHelperMixin
from lp.registry.browser.distribution import DistributionPublisherConfigView
from lp.registry.enums import DistributionDefaultTraversalPolicy
from lp.registry.interfaces.distribution import IDistributionSet
@@ -183,7 +184,7 @@ class TestDistroAddView(TestCaseWithFactory):
self.assertContentEqual([], distribution.main_archive.processors)
-class TestDistroEditView(TestCaseWithFactory):
+class TestDistroEditView(OCIConfigHelperMixin, TestCaseWithFactory):
"""Test the +edit page for a distribution."""
layer = DatabaseFunctionalLayer
@@ -193,6 +194,7 @@ class TestDistroEditView(TestCaseWithFactory):
self.admin = login_celebrity('admin')
self.distribution = self.factory.makeDistribution()
self.all_processors = getUtility(IProcessorSet).getAll()
+ self.setConfig()
def test_edit_distro_init_value_require_virtualized(self):
view = create_initialized_view(
@@ -260,6 +262,125 @@ class TestDistroEditView(TestCaseWithFactory):
method="POST", form=edit_form)
self.assertEqual(self.distribution.package_derivatives_email, email)
+ def test_oci_validation_username_no_url(self):
+ edit_form = self.getDefaultEditDict()
+ edit_form["field.oci_credentials_username"] = "username"
+
+ view = create_initialized_view(
+ self.distribution, '+edit', principal=self.admin,
+ method='POST', form=edit_form)
+ self.assertEqual(
+ "A URL is required if a username is present.",
+ view.getFieldError("oci_credentials_url"))
+
+ def test_oci_validation_different_passwords(self):
+ edit_form = self.getDefaultEditDict()
+ edit_form["field.oci_credentials_password"] = "password1"
+ edit_form["field.oci_credentials_confirm_password"] = "password2"
+ view = create_initialized_view(
+ self.distribution, '+edit', principal=self.admin,
+ method='POST', form=edit_form)
+ self.assertEqual(
+ "Passwords must match.",
+ view.getFieldError("oci_credentials_password"))
+
+ def test_oci_validation_url_unset(self):
+ edit_form = self.getDefaultEditDict()
+ edit_form["field.oci_credentials_url"] = ""
+
+ credentials = self.factory.makeOCIRegistryCredentials(
+ registrant=self.distribution.owner,
+ owner=self.distribution.owner)
+ self.distribution.oci_registry_credentials = credentials
+
+ view = create_initialized_view(
+ self.distribution, '+edit', principal=self.admin,
+ method='POST', form=edit_form)
+ self.assertEqual(
+ "URL must be specified. Delete credentials to unset URL.",
+ view.getFieldError("oci_credentials_url"))
+
+ def test_oci_create_credentials_url_only(self):
+ edit_form = self.getDefaultEditDict()
+ registry_url = self.factory.getUniqueURL()
+ edit_form["field.oci_credentials_url"] = registry_url
+
+ create_initialized_view(
+ self.distribution, '+edit', principal=self.admin,
+ method='POST', form=edit_form)
+ self.assertEqual(
+ registry_url, self.distribution.oci_registry_credentials.url)
+
+ def test_oci_create_credentials(self):
+ edit_form = self.getDefaultEditDict()
+ registry_url = self.factory.getUniqueURL()
+ username = self.factory.getUniqueUnicode()
+ password = self.factory.getUniqueUnicode()
+ edit_form["field.oci_credentials_url"] = registry_url
+ edit_form["field.oci_credentials_username"] = username
+ edit_form["field.oci_credentials_password"] = password
+ edit_form["field.oci_credentials_confirm_password"] = password
+
+ create_initialized_view(
+ self.distribution, '+edit', principal=self.admin,
+ method='POST', form=edit_form)
+ self.assertEqual(
+ username, self.distribution.oci_registry_credentials.username)
+
+ def test_oci_create_credentials_change_url(self):
+ edit_form = self.getDefaultEditDict()
+ credentials = self.factory.makeOCIRegistryCredentials(
+ registrant=self.distribution.owner,
+ owner=self.distribution.owner)
+ self.distribution.oci_registry_credentials = credentials
+ registry_url = self.factory.getUniqueURL()
+ edit_form["field.oci_credentials_url"] = registry_url
+
+ create_initialized_view(
+ self.distribution, '+edit', principal=self.admin,
+ method='POST', form=edit_form)
+ self.assertEqual(
+ registry_url, self.distribution.oci_registry_credentials.url)
+ # This should have mutated, not created new credentials records
+ self.assertEqual(
+ credentials.id, self.distribution.oci_registry_credentials.id)
+
+ def test_oci_create_credentials_change_password(self):
+ edit_form = self.getDefaultEditDict()
+ credentials = self.factory.makeOCIRegistryCredentials(
+ registrant=self.distribution.owner,
+ owner=self.distribution.owner)
+ self.distribution.oci_registry_credentials = credentials
+ password = self.factory.getUniqueUnicode()
+ edit_form["field.oci_credentials_url"] = credentials.url
+ edit_form["field.oci_credentials_username"] = credentials.username
+ edit_form["field.oci_credentials_password"] = password
+ edit_form["field.oci_credentials_confirm_password"] = password
+
+ create_initialized_view(
+ self.distribution, '+edit', principal=self.admin,
+ method='POST', form=edit_form)
+ distro_credentials = self.distribution.oci_registry_credentials
+ unencrypted_credentials = distro_credentials.getCredentials()
+ self.assertEqual(
+ password, unencrypted_credentials["password"])
+ # This should not have changed
+ self.assertEqual(
+ distro_credentials.url, credentials.url)
+
+ def test_oci_delete_credentials(self):
+ edit_form = self.getDefaultEditDict()
+ credentials = self.factory.makeOCIRegistryCredentials(
+ registrant=self.distribution.owner,
+ owner=self.distribution.owner)
+ self.distribution.oci_registry_credentials = credentials
+ edit_form['field.oci_credentials_delete'] = 'on'
+
+ create_initialized_view(
+ self.distribution, '+edit', principal=self.admin,
+ method='POST', form=edit_form)
+ self.assertIsNone(self.distribution.oci_registry_credentials)
+
class TestDistributionAdminView(TestCaseWithFactory):
"""Test the +admin page for a distribution."""
diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
index 7c2dad8..7a7a396 100644
--- a/lib/lp/registry/configure.zcml
+++ b/lib/lp/registry/configure.zcml
@@ -1833,6 +1833,7 @@
mirror_admin
mugshot
oci_project_admin
+ oci_registry_credentials
official_answers
official_blueprints
official_malone
diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py
index 8c23b90..6d13dc5 100644
--- a/lib/lp/registry/interfaces/distribution.py
+++ b/lib/lp/registry/interfaces/distribution.py
@@ -73,6 +73,7 @@ from lp.bugs.interfaces.bugtarget import (
from lp.bugs.interfaces.structuralsubscription import (
IStructuralSubscriptionTarget,
)
+from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials
from lp.registry.enums import (
DistributionDefaultTraversalPolicy,
VCSType,
@@ -719,6 +720,13 @@ class IDistributionPublic(
def newOCIProject(registrant, name, description=None):
"""Create an `IOCIProject` for this distro."""
+ oci_registry_credentials = Reference(
+ IOCIRegistryCredentials,
+ title=_("OCI registry credentials"),
+ description=_("Credentials and URL to use for uploading all OCI "
+ "Images in this distribution to a registry."),
+ required=False, readonly=True)
+
@exported_as_webservice_entry(as_of="beta")
class IDistribution(
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index a3d4062..cceef00 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -33,6 +33,10 @@ from storm.expr import (
SQL,
)
from storm.info import ClassAlias
+from storm.locals import (
+ Int,
+ Reference,
+ )
from storm.store import Store
from zope.component import getUtility
from zope.interface import implementer
@@ -267,6 +271,9 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
enum=DistributionDefaultTraversalPolicy, notNull=False,
default=DistributionDefaultTraversalPolicy.SERIES)
redirect_default_traversal = BoolCol(notNull=False, default=False)
+ oci_registry_credentialsID = Int(name='oci_credentials', allow_none=True)
+ oci_registry_credentials = Reference(
+ oci_registry_credentialsID, "OCIRegistryCredentials.id")
def __repr__(self):
display_name = self.display_name.encode('ASCII', 'backslashreplace')
diff --git a/lib/lp/registry/templates/distribution-edit.pt b/lib/lp/registry/templates/distribution-edit.pt
new file mode 100644
index 0000000..a8562b9
--- /dev/null
+++ b/lib/lp/registry/templates/distribution-edit.pt
@@ -0,0 +1,98 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ metal:use-macro="view/macro:page/main_side"
+ i18n:domain="launchpad"
+>
+<body>
+
+<tal:main metal:fill-slot="main">
+
+ <div metal:use-macro="context/@@launchpad_form/form">
+ <metal:formbody fill-slot="widgets">
+ <table class="form">
+ <tal:widget define="widget nocall:view/widgets/display_name">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/summary">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/description">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/bug_reporting_guidelines">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/bug_reported_acknowledgement">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/package_derivatives_email">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/icon">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/logo">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/mugshot">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/official_malone">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/enable_bug_expiration">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/blueprints_usage">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/translations_usage">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/answers_usage">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/translation_focus">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/default_traversal_policy">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/redirect_default_traversal">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+
+ <tr>
+ <td><label>OCI registry credentials</label></td>
+ <tr>
+ <tr>
+ <td tal:define="widget nocall:view/widgets/oci_credentials_url">
+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
+ </td>
+ <td tal:define="widget nocall:view/widgets/oci_credentials_region">
+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
+ </td>
+ <td tal:define="widget nocall:view/widgets/oci_credentials_username">
+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
+ </td>
+ <td tal:define="widget nocall:view/widgets/oci_credentials_password">
+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
+ </td>
+ <td tal:define="widget nocall:view/widgets/oci_credentials_confirm_password">
+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
+ </td>
+ <td tal:define="widget nocall:view/widgets/oci_credentials_delete">
+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
+ </td>
+ </tr>
+ </table>
+ </metal:formbody>
+ </div>
+
+</tal:main>
+
+</body>
+</html>