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

Percentage-based Enablement for Feature Toggle #1952

Merged
65 changes: 65 additions & 0 deletions samtranslator/feature_toggle/dialup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import hashlib


class BaseDialup(object):
def __init__(self, region_config, **kwargs):
self.region_config = region_config

def is_enabled(self):
raise NotImplementedError

def __str__(self):
return self.__class__.__name__


class NeverEnabledDialup(BaseDialup):
hawflau marked this conversation as resolved.
Show resolved Hide resolved
"""
A dialup that is never enabled
"""

def __init__(self, region_config, **kwargs):
super(NeverEnabledDialup, self).__init__(region_config)

def is_enabled(self):
return False


class ToggleDialup(BaseDialup):
"""
A simple toggle Dialup
Example of region_config: { "type": "toggle", "enabled": True }
"""

def __init__(self, region_config, **kwargs):
super(ToggleDialup, self).__init__(region_config)
self.region_config = region_config

def is_enabled(self):
return self.region_config.get("enabled", False)


class SimpleAccountPercentileDialup(BaseDialup):
"""
Simple account percentile dialup, enabling X% of
Example of region_config: { "type": "account-percentile", "enabled-%": 20 }
"""

def __init__(self, region_config, account_id, feature_name, **kwargs):
super(SimpleAccountPercentileDialup, self).__init__(region_config)
self.account_id = account_id
self.feature_name = feature_name

def _get_account_percentile(self):
"""
Get account percentile based on sha256 hash of account ID and feature_name

:returns: integer n, where 0 <= n < 100
"""
m = hashlib.sha256()
m.update(self.account_id.encode())
m.update(self.feature_name.encode())
return int(m.hexdigest(), 16) % 100

def is_enabled(self):
target_percentile = self.region_config.get("enabled-%", 0)
return self._get_account_percentile() < target_percentile
83 changes: 54 additions & 29 deletions samtranslator/feature_toggle/feature_toggle.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
import json
import boto3
import logging
import hashlib

from botocore.config import Config
from samtranslator.feature_toggle.dialup import (
NeverEnabledDialup,
ToggleDialup,
SimpleAccountPercentileDialup,
)

my_path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, my_path + "/..")
Expand All @@ -18,50 +24,69 @@ class FeatureToggle:
SAM is executing or not.
"""

def __init__(self, config_provider):
DIALUP_RESOLVER = {
"toggle": ToggleDialup,
"account-percentile": SimpleAccountPercentileDialup,
}

def __init__(self, config_provider, stage, account_id, region):
self.feature_config = config_provider.config
self.stage = stage
self.account_id = account_id
self.region = region

def is_enabled_for_stage_in_region(self, feature_name, stage, region="default"):
def _get_dialup(self, region_config, feature_name):
"""
To check if feature is available for a particular stage or not.
:param feature_name: name of feature
:param stage: stage where SAM is running
:param region: region in which SAM is running
:return:
get the right dialup instance
if no dialup type is provided or the specified dialup is not supported,
an instance of NeverEnabledDialup will be returned

:param region_config: region config
:param feature_name: feature_name
:return: an instance of
"""
if feature_name not in self.feature_config:
LOG.warning("Feature '{}' not available in Feature Toggle Config.".format(feature_name))
return False
stage_config = self.feature_config.get(feature_name, {}).get(stage, {})
if not stage_config:
LOG.info("Stage '{}' not enabled for Feature '{}'.".format(stage, feature_name))
return False
region_config = stage_config.get(region, {}) if region in stage_config else stage_config.get("default", {})
is_enabled = region_config.get("enabled", False)
LOG.info("Feature '{}' is enabled: '{}'".format(feature_name, is_enabled))
return is_enabled
dialup_type = region_config.get("type")
if dialup_type in FeatureToggle.DIALUP_RESOLVER:
hawflau marked this conversation as resolved.
Show resolved Hide resolved
return FeatureToggle.DIALUP_RESOLVER[dialup_type](
region_config, account_id=self.account_id, feature_name=feature_name
)
LOG.warning("Dialup type '{}' is None or is not supported.".format(dialup_type))
return NeverEnabledDialup(region_config)

def is_enabled_for_account_in_region(self, feature_name, stage, account_id, region="default"):
def is_enabled(self, feature_name):
"""
To check if feature is available for a particular account or not.
To check if feature is available

:param feature_name: name of feature
:param stage: stage where SAM is running
:param account_id: account_id who is executing SAM template
:param region: region in which SAM is running
:return:
"""
if feature_name not in self.feature_config:
LOG.warning("Feature '{}' not available in Feature Toggle Config.".format(feature_name))
return False

stage = self.stage
region = self.region
account_id = self.account_id
if not stage or not region or not account_id:
LOG.warning(
"One or more of stage, region and account_id is not set. Feature '{}' not enabled.".format(feature_name)
)
return False

stage_config = self.feature_config.get(feature_name, {}).get(stage, {})
if not stage_config:
LOG.info("Stage '{}' not enabled for Feature '{}'.".format(stage, feature_name))
return False
account_config = stage_config.get(account_id) if account_id in stage_config else stage_config.get("default", {})
region_config = (
account_config.get(region, {}) if region in account_config else account_config.get("default", {})
)
is_enabled = region_config.get("enabled", False)

if account_id in stage_config:
account_config = stage_config[account_id]
region_config = account_config[region] if region in account_config else account_config.get("default", {})
else:
region_config = stage_config[region] if region in stage_config else stage_config.get("default", {})

dialup = self._get_dialup(region_config, feature_name=feature_name)
LOG.info("Using Dialip {}".format(dialup))
is_enabled = dialup.is_enabled()

LOG.info("Feature '{}' is enabled: '{}'".format(feature_name, is_enabled))
return is_enabled

Expand Down
6 changes: 5 additions & 1 deletion samtranslator/translator/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ def translate(self, sam_template, parameter_values, feature_toggle=None):
:returns: a copy of the template with SAM resources replaced with the corresponding CloudFormation, which may \
be dumped into a valid CloudFormation JSON or YAML template
"""
self.feature_toggle = feature_toggle if feature_toggle else FeatureToggle(FeatureToggleDefaultConfigProvider())
self.feature_toggle = (
feature_toggle
if feature_toggle
else FeatureToggle(FeatureToggleDefaultConfigProvider(), stage=None, account_id=None, region=None)
)
self.function_names = dict()
self.redeploy_restapi_parameters = dict()
sam_parameter_values = SamParameterValues(parameter_values)
Expand Down
17 changes: 12 additions & 5 deletions tests/feature_toggle/input/feature_toggle_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
"__note__": "This is a dummy config for local testing. Any change here need to be migrated to SAM service.",
"feature-1": {
"beta": {
"us-west-2": {"enabled": true},
"default": {"enabled": false},
"123456789123": {"us-west-2": {"enabled": true}, "default": {"enabled": false}}
"us-west-2": {"type": "toggle", "enabled": true},
"us-east-1": {"type": "account-percentile", "enabled-%": 10},
"default": {"type": "toggle", "enabled": false},
"123456789123": {
"us-west-2": {"type": "toggle", "enabled": true},
"default": {"type": "toggle", "enabled": false}
}
},
"gamma": {
"default": {"enabled": false},
"123456789123": {"us-east-1": {"enabled": false}, "default": {"enabled": false}}
"default": {"type": "toggle", "enabled": false},
"123456789123": {
"us-east-1": {"type": "toggle", "enabled": false},
"default": {"type": "toggle", "enabled": false}
}
},
"prod": {"default": {"enabled": false}}
}
Expand Down
65 changes: 65 additions & 0 deletions tests/feature_toggle/test_dialup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from unittest import TestCase

from parameterized import parameterized, param
from samtranslator.feature_toggle.dialup import *


class TestBaseDialup(TestCase):
def test___str__(self):
region_config = {}
dialup = BaseDialup(region_config)
self.assertEqual(str(dialup), "BaseDialup")


class TestNeverEnabledDialup(TestCase):
def test_is_enabled(self):
region_config = {}
dialup = NeverEnabledDialup(region_config)
self.assertFalse(dialup.is_enabled())


class TestToggleDialUp(TestCase):
@parameterized.expand(
[
param({"type": "toggle", "enabled": True}, True),
param({"type": "toggle", "enabled": False}, False),
param({"type": "toggle"}, False), # missing "enabled" key
]
)
def test_is_enabled(self, region_config, expected):
dialup = ToggleDialup(region_config)
self.assertEqual(dialup.is_enabled(), expected)


class TestSimpleAccountPercentileDialup(TestCase):
@parameterized.expand(
[
param({"type": "account-percentile", "enabled-%": 10}, "feature-1", "123456789100", True),
param({"type": "account-percentile", "enabled-%": 10}, "feautre-1", "123456789123", False),
param({"type": "account-percentile", "enabled": True}, "feature-1", "123456789100", False),
]
)
def test_is_enabled(self, region_config, feature_name, account_id, expected):
dialup = SimpleAccountPercentileDialup(
region_config=region_config,
account_id=account_id,
feature_name=feature_name,
)
self.assertEqual(dialup.is_enabled(), expected)

@parameterized.expand(
[
param("feature-1", "123456789123"),
param("feature-2", "000000000000"),
param("feature-3", "432187654321"),
param("feature-4", "111222333444"),
]
)
def test__get_account_percentile(self, account_id, feature_name):
region_config = {"type": "account-percentile", "enabled-%": 10}
dialup = SimpleAccountPercentileDialup(
region_config=region_config,
account_id=account_id,
feature_name=feature_name,
)
self.assertTrue(0 <= dialup._get_account_percentile() < 100)
Loading