Allow for dynamic feature flags (#6808)
* Allow for dynamic feature flags Giving more control over feature flags, allowing administrator to define custom logic around whether features are enabled for particular users / roles. The exposed function can be used for things like: * progressive rollout of features (1%, 5%, 50%, 100%) * experimentation * role-based feature affectation (only admins see a particular feature) * fix build * Addressing comments * Addressing @hughhh's comments
This commit is contained in:
parent
e0feec9117
commit
3ae02d1a54
|
|
@ -16,6 +16,7 @@
|
|||
# under the License.
|
||||
# pylint: disable=C,R,W
|
||||
"""Package's main module!"""
|
||||
from copy import deepcopy
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
|
@ -212,15 +213,20 @@ security_manager = appbuilder.sm
|
|||
results_backend = app.config.get('RESULTS_BACKEND')
|
||||
|
||||
# Merge user defined feature flags with default feature flags
|
||||
feature_flags = app.config.get('DEFAULT_FEATURE_FLAGS')
|
||||
feature_flags.update(app.config.get('FEATURE_FLAGS') or {})
|
||||
_feature_flags = app.config.get('DEFAULT_FEATURE_FLAGS') or {}
|
||||
_feature_flags.update(app.config.get('FEATURE_FLAGS') or {})
|
||||
|
||||
|
||||
def get_feature_flags():
|
||||
GET_FEATURE_FLAGS_FUNC = app.config.get('GET_FEATURE_FLAGS_FUNC')
|
||||
if GET_FEATURE_FLAGS_FUNC:
|
||||
return GET_FEATURE_FLAGS_FUNC(deepcopy(_feature_flags))
|
||||
return _feature_flags
|
||||
|
||||
|
||||
def is_feature_enabled(feature):
|
||||
"""
|
||||
Utility function for checking whether a feature is turned on
|
||||
"""
|
||||
return feature_flags.get(feature)
|
||||
"""Utility function for checking whether a feature is turned on"""
|
||||
return get_feature_flags().get(feature)
|
||||
|
||||
|
||||
# Registering sources
|
||||
|
|
|
|||
|
|
@ -204,6 +204,22 @@ LANGUAGES = {
|
|||
# will result in combined feature flags of { 'FOO': True, 'BAR': True, 'BAZ': True }
|
||||
DEFAULT_FEATURE_FLAGS = {}
|
||||
|
||||
# A function that receives a dict of all feature flags
|
||||
# (DEFAULT_FEATURE_FLAGS merged with FEATURE_FLAGS)
|
||||
# can alter it, and returns a similar dict. Note the dict of feature
|
||||
# flags passed to the function is a deepcopy of the dict in the config,
|
||||
# and can therefore be mutated without side-effect
|
||||
#
|
||||
# GET_FEATURE_FLAGS_FUNC can be used to implement progressive rollouts,
|
||||
# role-based features, or a full on A/B testing framework.
|
||||
#
|
||||
# from flask import g, request
|
||||
# def GET_FEATURE_FLAGS_FUNC(feature_flags_dict):
|
||||
# feature_flags_dict['some_feature'] = g.user and g.user.id == 5
|
||||
# return feature_flags_dict
|
||||
GET_FEATURE_FLAGS_FUNC = None
|
||||
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Image and file configuration
|
||||
# ---------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from flask_babel import lazy_gettext as _
|
|||
import simplejson as json
|
||||
import yaml
|
||||
|
||||
from superset import conf, db, feature_flags, security_manager
|
||||
from superset import conf, db, get_feature_flags, security_manager
|
||||
from superset.exceptions import SupersetException, SupersetSecurityException
|
||||
from superset.translations.utils import get_language_pack
|
||||
from superset.utils import core as utils
|
||||
|
|
@ -157,7 +157,7 @@ class BaseSupersetView(BaseView):
|
|||
'conf': {k: conf.get(k) for k in FRONTEND_CONF_KEYS},
|
||||
'locale': locale,
|
||||
'language_pack': get_language_pack(locale),
|
||||
'feature_flags': feature_flags,
|
||||
'feature_flags': get_feature_flags(),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -190,10 +190,14 @@ class SupersetTestCase(unittest.TestCase):
|
|||
raise Exception('run_sql failed')
|
||||
return resp
|
||||
|
||||
@patch.dict('superset.feature_flags', {'FOO': True}, clear=True)
|
||||
@patch.dict('superset._feature_flags', {'FOO': True}, clear=True)
|
||||
def test_existing_feature_flags(self):
|
||||
self.assertTrue(is_feature_enabled('FOO'))
|
||||
|
||||
@patch.dict('superset.feature_flags', {}, clear=True)
|
||||
@patch.dict('superset._feature_flags', {}, clear=True)
|
||||
def test_nonexistent_feature_flags(self):
|
||||
self.assertFalse(is_feature_enabled('FOO'))
|
||||
|
||||
def test_feature_flags(self):
|
||||
self.assertEquals(is_feature_enabled('foo'), 'bar')
|
||||
self.assertEquals(is_feature_enabled('super'), 'set')
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# flake8: noqa
|
||||
from copy import copy
|
||||
from superset.config import *
|
||||
|
||||
AUTH_USER_REGISTRATION_ROLE = 'alpha'
|
||||
|
|
@ -29,6 +30,14 @@ if 'SUPERSET__SQLALCHEMY_DATABASE_URI' in os.environ:
|
|||
|
||||
SQL_SELECT_AS_CTA = True
|
||||
SQL_MAX_ROW = 666
|
||||
FEATURE_FLAGS = {
|
||||
'foo': 'bar',
|
||||
}
|
||||
def GET_FEATURE_FLAGS_FUNC(ff):
|
||||
ff_copy = copy(ff)
|
||||
ff_copy['super'] = 'set'
|
||||
return ff_copy
|
||||
|
||||
|
||||
TESTING = True
|
||||
SECRET_KEY = 'thisismyscretkey'
|
||||
|
|
|
|||
Loading…
Reference in New Issue