launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05013
[Merge] lp:~mwhudson/launchpad/feature-flag-xmlrpc into lp:launchpad
Michael Hudson-Doyle has proposed merging lp:~mwhudson/launchpad/feature-flag-xmlrpc into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~mwhudson/launchpad/feature-flag-xmlrpc/+merge/75673
This branch adds a private XML-RPC interface to query feature flags. It supports the team: scope (if you pass a username) and arbitrary other scopes.
It's been a while, so I've probably forgotten some coding standards things. I did remember to run "make lint" though :)
--
https://code.launchpad.net/~mwhudson/launchpad/feature-flag-xmlrpc/+merge/75673
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~mwhudson/launchpad/feature-flag-xmlrpc into lp:launchpad.
=== modified file 'lib/canonical/launchpad/interfaces/launchpad.py'
--- lib/canonical/launchpad/interfaces/launchpad.py 2011-05-27 19:53:20 +0000
+++ lib/canonical/launchpad/interfaces/launchpad.py 2011-09-19 00:33:20 +0000
@@ -152,6 +152,8 @@
softwarecenteragent = Attribute(
"""Software center agent XML-RPC end point.""")
+ featureflags = Attribute("""Feature flag information endpoint""")
+
class IAuthServerApplication(ILaunchpadApplication):
"""Launchpad legacy AuthServer application root."""
=== modified file 'lib/canonical/launchpad/xmlrpc/application.py'
--- lib/canonical/launchpad/xmlrpc/application.py 2011-04-28 21:36:47 +0000
+++ lib/canonical/launchpad/xmlrpc/application.py 2011-09-19 00:33:20 +0000
@@ -36,6 +36,7 @@
)
from lp.registry.interfaces.mailinglist import IMailingListApplication
from lp.registry.interfaces.person import ISoftwareCenterAgentApplication
+from lp.services.features.xmlrpc import IFeatureFlagApplication
# NOTE: If you add a traversal here, you should update
@@ -73,6 +74,11 @@
"""See `IPrivateApplication`."""
return getUtility(ISoftwareCenterAgentApplication)
+ @property
+ def featureflags(self):
+ """See `IPrivateApplication`."""
+ return getUtility(IFeatureFlagApplication)
+
class ISelfTest(Interface):
"""XMLRPC external interface for testing the XMLRPC external interface."""
=== modified file 'lib/lp/services/features/configure.zcml'
--- lib/lp/services/features/configure.zcml 2010-08-12 06:55:37 +0000
+++ lib/lp/services/features/configure.zcml 2011-09-19 00:33:20 +0000
@@ -19,4 +19,11 @@
handler="lp.services.features.webapp.end_request"
/>
+ <securedutility
+ class="lp.services.features.xmlrpc.FeatureFlagApplication"
+ provides="lp.services.features.xmlrpc.IFeatureFlagApplication">
+ <allow
+ interface="lp.services.features.xmlrpc.IFeatureFlagApplication"/>
+ </securedutility>
+
</configure>
=== modified file 'lib/lp/services/features/scopes.py'
--- lib/lp/services/features/scopes.py 2011-08-02 23:42:08 +0000
+++ lib/lp/services/features/scopes.py 2011-09-19 00:33:20 +0000
@@ -9,7 +9,9 @@
"""
__all__ = [
+ 'DefaultScope',
'HANDLERS',
+ 'MultiScopeHandler',
'ScopesForScript',
'ScopesFromRequest',
'undocumented_scopes',
=== added file 'lib/lp/services/features/tests/test_xmlrpc.py'
--- lib/lp/services/features/tests/test_xmlrpc.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/features/tests/test_xmlrpc.py 2011-09-19 00:33:20 +0000
@@ -0,0 +1,103 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for FeatureFlagApplication."""
+
+__metaclass__ = type
+
+import xmlrpclib
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.services import features
+from lp.services.features.flags import FeatureController
+from lp.services.features.rulesource import StormFeatureRuleSource
+from lp.services.features.scopes import (
+ BaseScope,
+ DefaultScope,
+ MultiScopeHandler,
+ )
+from lp.services.features.xmlrpc import FeatureFlagApplication
+from lp.testing import (
+ feature_flags,
+ set_feature_flag,
+ TestCaseWithFactory,
+ )
+from lp.testing.xmlrpc import XMLRPCTestTransport
+
+
+class FixedScope(BaseScope):
+ pattern = r'fixed$'
+
+ def lookup(self, scope_name):
+ return True
+
+
+class TestGetFeatureFlag(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ self.endpoint = FeatureFlagApplication()
+
+ def installFeatureController(self, feature_controller):
+ old_features = features.get_relevant_feature_controller()
+ features.install_feature_controller(feature_controller)
+ self.addCleanup(
+ features.install_feature_controller, old_features)
+
+ def test_getFeatureFlag_returns_None_by_default(self):
+ self.assertIs(None, self.endpoint.getFeatureFlag(u'unknown'))
+
+ def test_getFeatureFlag_returns_true_for_set_flag(self):
+ flag_name = u'flag'
+ with feature_flags():
+ set_feature_flag(flag_name, u'1')
+ self.assertEqual(u'1', self.endpoint.getFeatureFlag(flag_name))
+
+ def test_getFeatureFlag_ignores_relevant_feature_controller(self):
+ # getFeatureFlag should only consider the scopes it is asked to
+ # consider, not any that happen to be active due to the XML-RPC
+ # request itself.
+ self.installFeatureController(
+ FeatureController(
+ MultiScopeHandler([DefaultScope(), FixedScope()]).lookup,
+ StormFeatureRuleSource()))
+ flag_name = u'flag'
+ set_feature_flag(flag_name, u'1', u'fixed')
+ self.assertEqual(None, self.endpoint.getFeatureFlag(flag_name))
+
+ def test_getFeatureFlag_considers_supplied_scope(self):
+ flag_name = u'flag'
+ scope_name = u'scope'
+ with feature_flags():
+ set_feature_flag(flag_name, u'value', scope_name)
+ self.assertEqual(
+ u'value',
+ self.endpoint.getFeatureFlag(flag_name, scopes=[scope_name]))
+
+ def test_getFeatureFlag_evaluates_team_scope(self):
+ flag_name = u'flag'
+ person = self.factory.makePerson()
+ team = self.factory.makeTeam(members=[person])
+ with feature_flags():
+ set_feature_flag(flag_name, u'value', u'team:' + team.name)
+ self.assertEqual(
+ u'value',
+ self.endpoint.getFeatureFlag(
+ flag_name, username=person.name))
+
+ def test_xmlrpc_interface_unset(self):
+ sp = xmlrpclib.ServerProxy(
+ 'http://xmlrpc-private.launchpad.dev:8087/featureflags/',
+ transport=XMLRPCTestTransport(), allow_none=True)
+ self.assertEqual(None, sp.getFeatureFlag(u'flag'))
+
+ def test_xmlrpc_interface_set(self):
+ sp = xmlrpclib.ServerProxy(
+ 'http://xmlrpc-private.launchpad.dev:8087/featureflags/',
+ transport=XMLRPCTestTransport(), allow_none=True)
+ flag_name = u'flag'
+ with feature_flags():
+ set_feature_flag(flag_name, u'1')
+ self.assertEqual(u'1', sp.getFeatureFlag(flag_name))
=== added file 'lib/lp/services/features/xmlrpc.py'
--- lib/lp/services/features/xmlrpc.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/features/xmlrpc.py 2011-09-19 00:33:20 +0000
@@ -0,0 +1,57 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""FeatureFlagApplication allows access to information about feature flags."""
+
+__metaclass__ = type
+__all__ = [
+ 'IFeatureFlagApplication',
+ 'FeatureFlagApplication',
+ ]
+
+from zope.component import getUtility
+from zope.interface import implements
+
+from canonical.launchpad.webapp.interfaces import ILaunchpadApplication
+from lp.registry.interfaces.person import IPersonSet
+from lp.services.features.flags import FeatureController
+from lp.services.features.rulesource import StormFeatureRuleSource
+from lp.services.features.scopes import (
+ DefaultScope,
+ MultiScopeHandler,
+ )
+
+
+class IFeatureFlagApplication(ILaunchpadApplication):
+ """Mailing lists application root."""
+
+ def getFeatureFlag(flag_name, username=None, scopes=()):
+ """Return the value of the given feature flag.
+
+ :param flag_name: The name of the flag to query.
+ :param username: If supplied, the name of a Person to use in
+ evaluating the 'team:' scope.
+ :param scopes: A list of scopes to consider active. The 'default'
+ scope is always considered to be active, and does not need to be
+ included here.
+ """
+
+
+class FeatureFlagApplication:
+
+ implements(IFeatureFlagApplication)
+
+ def getFeatureFlag(self, flag_name, username=None, scopes=()):
+ scopes = tuple(scopes) + ('default',)
+ person = None
+ if username:
+ person = getUtility(IPersonSet).getByName(username)
+ def scope_lookup(scope):
+ if person is not None and scope.startswith('team:'):
+ team_name = scope[len('team:'):]
+ return person.inTeam(team_name)
+ else:
+ return scope in scopes
+ flag_name = unicode(flag_name)
+ controller = FeatureController(scope_lookup, StormFeatureRuleSource())
+ return controller.getFlag(flag_name)
=== modified file 'utilities/page-performance-report.ini'
--- utilities/page-performance-report.ini 2011-04-28 21:52:17 +0000
+++ utilities/page-performance-report.ini 2011-09-19 00:33:20 +0000
@@ -11,7 +11,8 @@
[^/]+($|/
(?!\+haproxy|\+opstats|\+access-token
|((authserver|bugs|bazaar|codehosting|
- codeimportscheduler|mailinglists|softwarecenteragent)/\w+$)))
+ codeimportscheduler|mailinglists|softwarecenteragent|
+ featureflags)/\w+$)))
Other=^/
Launchpad Frontpage=^https?://launchpad\.[^/]+(/index\.html)?$
@@ -54,7 +55,7 @@
Private XML-RPC=^https://(launchpad|xmlrpc)[^/]+/
(authserver|bugs|codehosting|
codeimportscheduler|mailinglists|
- softwarecenteragent)/\w+$
+ softwarecenteragent|featureflags)/\w+$
[metrics]
ppr_all=All Launchpad except operational pages
Follow ups