Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proper feature flags #2354

Merged
merged 8 commits into from
Jul 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions conf/development.ini
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,6 @@ es.host: http://localhost:9200

h.autologin: True

h.feature.accounts: True
h.feature.api: True
h.feature.claim: False
h.feature.notification: False
h.feature.queue: False
h.feature.streamer: False

mail.default_sender: "Annotation Daemon" <no-reply@localhost>

pyramid.debug_all: True
Expand Down
8 changes: 0 additions & 8 deletions conf/production.ini
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@ use: egg:h
#h.client_id:
#h.client_secret:

# Feature flags
h.feature.accounts: True
h.feature.api: True
h.feature.claim: False
h.feature.notification: True
h.feature.queue: True
h.feature.streamer: True

# Mail server configuration -- see the pyramid_mailer documentation
mail.default_sender: "Annotation Daemon" <no-reply@localhost>
#mail.host: localhost
Expand Down
7 changes: 0 additions & 7 deletions conf/testext.ini
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
[app:main]
use: egg:h

h.feature.accounts: True
h.feature.api: True
h.feature.claim: True
h.feature.notification: True
h.feature.queue: True
h.feature.streamer: True

sqlalchemy.url: sqlite://

webassets.base_dir: h:static
Expand Down
14 changes: 7 additions & 7 deletions h/accounts/test/views_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pyramid import httpexceptions
from pyramid.testing import DummyRequest as _DummyRequest

from h.conftest import DummyFeature
from h.conftest import DummySession

from h.accounts.views import ajax_form
Expand All @@ -20,8 +21,12 @@

class DummyRequest(_DummyRequest):
def __init__(self, *args, **kwargs):
# Add a dummy database session to the request
params = {'db': DummySession()}
params = {
# Add a dummy database session to the request
'db': DummySession(),
# Add a dummy feature flag querier to the request
'feature': DummyFeature(),
}
params.update(kwargs)
super(DummyRequest, self).__init__(*args, **params)

Expand All @@ -32,11 +37,6 @@ def __init__(self, **kwargs):
setattr(self, k, kwargs[k])


def configure(config):
config.registry.feature = MagicMock()
config.registry.feature.return_value = None


# A fake version of colander.Invalid for use when testing validate_form
class FakeInvalid(object):
def __init__(self, errors):
Expand Down
2 changes: 1 addition & 1 deletion h/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ def profile(self):
model = {}
if userid:
model["email"] = User.get_by_id(request, userid).email
if request.registry.feature('notification'):
if request.feature('notification'):
model['subscriptions'] = Subscriptions.get_subscriptions_for_uri(
userid)
return {'model': model}
Expand Down
5 changes: 5 additions & 0 deletions h/api/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ def annotation(event):
request = event.request
annotation = event.annotation
action = event.action

# We only publish these events to NSQ if the 'queue' feature is enabled.
if not request.feature('queue'):
return

queue = request.get_queue_writer()
data = {
'action': action,
Expand Down
45 changes: 16 additions & 29 deletions h/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,31 +58,21 @@ def create_app(global_config, **settings):

config.add_tween('h.tweens.csrf_tween_factory')

if config.registry.feature('accounts'):
config.set_authentication_policy(session_authn)
config.set_authorization_policy(acl_authz)
config.include('h.accounts')

if config.registry.feature('api'):
api_app = create_api(settings)
api_view = wsgiapp2(api_app)
config.add_view(api_view, name='api', decorator=strip_vhm)
# Add the view again with the 'index' route name, otherwise it will
# not take precedence over the index when a virtual root is in use.
config.add_view(api_view, name='api', decorator=strip_vhm,
route_name='index')

if config.registry.feature('claim'):
config.include('h.claim')

if config.registry.feature('queue'):
config.include('h.queue')

if config.registry.feature('streamer'):
config.include('h.streamer')

if config.registry.feature('notification'):
config.include('h.notification')
config.set_authentication_policy(session_authn)
config.set_authorization_policy(acl_authz)
config.include('h.accounts')
config.include('h.claim')
config.include('h.notification')
config.include('h.queue')
config.include('h.streamer')

api_app = create_api(settings)
api_view = wsgiapp2(api_app)
config.add_view(api_view, name='api', decorator=strip_vhm)
# Add the view again with the 'index' route name, otherwise it will
# not take precedence over the index when a virtual root is in use.
config.add_view(api_view, name='api', decorator=strip_vhm,
route_name='index')

return config.make_wsgi_app()

Expand All @@ -104,12 +94,9 @@ def create_api(global_config, **settings):

config.include('h.auth')
config.include('h.api.db')
config.include('h.api.queue')
config.include('h.api.views')

if config.registry.feature('queue'):
config.include('h.queue')
config.include('h.api.queue')

app = config.make_wsgi_app()
app = permit_cors(app,
allow_headers=('Authorization',
Expand Down
3 changes: 3 additions & 0 deletions h/assets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,11 @@ app_js:
contents: h:static/scripts/app.coffee
depends:
- h:static/scripts/*.coffee
- h:static/scripts/features.js
- h:static/scripts/directive/*.coffee
- h:static/scripts/directive/*.js
- h:static/scripts/filter/*.coffee
- h:static/scripts/filter/*.js
- config

app_css:
Expand Down
14 changes: 13 additions & 1 deletion h/claim/test/views_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@

import deform
from pytest import raises
from pyramid.testing import DummyRequest
from pyramid.testing import DummyRequest as _DummyRequest
from pyramid.httpexceptions import HTTPFound, HTTPNotFound

from h.conftest import DummyFeature

from ..views import claim_account
from ..views import update_account


class DummyRequest(_DummyRequest):
def __init__(self, *args, **kwargs):
params = {
# Add a dummy feature flag querier to the request
'feature': DummyFeature(),
}
params.update(kwargs)
super(DummyRequest, self).__init__(*args, **params)


def test_claim_account_returns_form():
request = _get_request()

Expand Down
4 changes: 4 additions & 0 deletions h/claim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@ def _validate_request(request):
Check that the passed request is appropriate for proceeding with account
claim. Asserts that:

- the 'claim' feature is toggled on
- no-one is logged in
- the claim token is provided and authentic
- the user referred to in the token exists
- the user referred to in the token has not already claimed their account

and raises for redirect or 404 otherwise.
"""
if not request.feature('claim'):
raise exc.HTTPNotFound()

# If signed in, redirect to stream
if request.authenticated_userid is not None:
_perform_logged_in_redirect(request)
Expand Down
14 changes: 0 additions & 14 deletions h/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import logging
import re
import urlparse

from pyramid.settings import asbool
Expand All @@ -16,7 +15,6 @@ def settings_from_environment():
_setup_db(settings)
_setup_elasticsearch(settings)
_setup_email(settings)
_setup_features(settings)
_setup_nsqd(settings)
_setup_redis(settings)
_setup_secrets(settings)
Expand Down Expand Up @@ -114,18 +112,6 @@ def _setup_email(settings):
settings['mail.port'] = mail_port


def _setup_features(settings):
# Allow feature flag overrides from the environment. For example, to
# override the 'foo_bar' feature you can set the environment variable
# FEATURE_FOO_BAR.
patt = re.compile(r'FEATURE_([A-Z_]+)')
keys = [k for k in os.environ.iterkeys() if patt.match(k)]
for k in keys:
name = 'h.feature.{}'.format(patt.match(k).group(1).lower())
value = asbool(os.environ[k])
settings[name] = value


def _setup_nsqd(settings):
if 'NSQD_PORT' in os.environ:
r_host = os.environ['NSQD_PORT_4150_TCP_ADDR']
Expand Down
28 changes: 17 additions & 11 deletions h/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@
from h import db


class DummyFeature(object):

"""
A dummy feature flag looker-upper.

Because we're probably testing all feature-flagged functionality, this
feature client defaults every flag to *True*, which is the exact opposite
of what happens outside of testing.
"""

def __init__(self):
self.flags = {}

def __call__(self, name, *args, **kwargs):
return self.flags.get(name, True)


class DummySession(object):

"""
Expand Down Expand Up @@ -83,17 +100,6 @@ def destroy():
return db.Session


@pytest.fixture(autouse=True)
def feature_flags(config):
class DummyFeatureClient(object):
def __init__(self):
self.flags = {}
def __call__(self, name, *args, **kwargs):
return self.flags.get(name, True)

config.registry.feature = DummyFeatureClient()


@pytest.fixture()
def mailer(config):
from pyramid_mailer.interfaces import IMailer
Expand Down
Loading