Skip to content

Commit 2a72a92

Browse files
author
Ran Isenberg
committed
added list all enabled feature toggles
1 parent 78f6eb3 commit 2a72a92

File tree

2 files changed

+153
-31
lines changed

2 files changed

+153
-31
lines changed

aws_lambda_powertools/utilities/feature_toggles/configuration_store.py

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,29 @@ def _match_by_action(self, action: str, restriction_value: Any, context_value: A
4949
self._logger.error(f"caught exception while matching action, action={action}, exception={str(exc)}")
5050
return False
5151

52+
def _is_rule_matched(self, feature_name: str, rule: Dict[str, Any], rules_context: Dict[str, Any]) -> bool:
53+
rule_name = rule.get(schema.RULE_NAME_KEY, "")
54+
rule_default_value = rule.get(schema.RULE_DEFAULT_VALUE)
55+
restrictions: Dict[str, str] = rule.get(schema.RESTRICTIONS_KEY)
56+
57+
for restriction in restrictions:
58+
context_value = rules_context.get(restriction.get(schema.RESTRICTION_KEY))
59+
if not self._match_by_action(
60+
restriction.get(schema.RESTRICTION_ACTION),
61+
restriction.get(schema.RESTRICTION_VALUE),
62+
context_value,
63+
):
64+
logger.debug(
65+
f"rule did not match action, rule_name={rule_name}, rule_default_value={rule_default_value}, feature_name={feature_name}, context_value={str(context_value)}" # noqa: E501
66+
)
67+
# context doesn't match restriction
68+
return False
69+
# if we got here, all restrictions match
70+
logger.debug(
71+
f"rule matched, rule_name={rule_name}, rule_default_value={rule_default_value}, feature_name={feature_name}" # noqa: E501
72+
)
73+
return True
74+
5275
def _handle_rules(
5376
self,
5477
*,
@@ -58,34 +81,14 @@ def _handle_rules(
5881
rules: List[Dict[str, Any]],
5982
) -> bool:
6083
for rule in rules:
61-
rule_name = rule.get(schema.RULE_NAME_KEY, "")
6284
rule_default_value = rule.get(schema.RULE_DEFAULT_VALUE)
63-
is_match = True
64-
restrictions: Dict[str, str] = rule.get(schema.RESTRICTIONS_KEY)
65-
66-
for restriction in restrictions:
67-
context_value = rules_context.get(restriction.get(schema.RESTRICTION_KEY))
68-
if not self._match_by_action(
69-
restriction.get(schema.RESTRICTION_ACTION),
70-
restriction.get(schema.RESTRICTION_VALUE),
71-
context_value,
72-
):
73-
logger.debug(
74-
f"rule did not match action, rule_name={rule_name}, rule_default_value={rule_default_value}, feature_name={feature_name}, context_value={str(context_value)}" # noqa: E501
75-
)
76-
is_match = False # rules doesn't match restriction
77-
break
78-
# if we got here, all restrictions match
79-
if is_match:
80-
logger.debug(
81-
f"rule matched, rule_name={rule_name}, rule_default_value={rule_default_value}, feature_name={feature_name}" # noqa: E501
82-
)
85+
if self._is_rule_matched(feature_name, rule, rules_context):
8386
return rule_default_value
84-
# no rule matched, return default value of feature
85-
logger.debug(
86-
f"no rule matched, returning default value of feature, feature_default_value={feature_default_value}, feature_name={feature_name}" # noqa: E501
87-
)
88-
return feature_default_value
87+
# no rule matched, return default value of feature
88+
logger.debug(
89+
f"no rule matched, returning default value of feature, feature_default_value={feature_default_value}, feature_name={feature_name}" # noqa: E501
90+
)
91+
return feature_default_value
8992

9093
def get_configuration(self) -> Dict[str, Any]:
9194
"""Get configuration string from AWs AppConfig and returned the parsed JSON dictionary
@@ -162,3 +165,39 @@ def get_feature_toggle(self, *, feature_name: str, rules_context: Dict[str, Any]
162165
feature_default_value=feature_default_value,
163166
rules=rules_list,
164167
)
168+
169+
def get_all_enabled_feature_toggles(self, *, rules_context: Dict[str, Any]) -> List[str]:
170+
"""Get all enabled feature toggles while also taking into account rule_context (when a feature has defined rules)
171+
172+
Args:
173+
rules_context (Dict[str, Any]): dict of attributes that you would like to match the rules
174+
against, can be {'tenant_id: 'X', 'username':' 'Y', 'region': 'Z'} etc.
175+
176+
Returns:
177+
List[str]: a list of all features name that are enabled by also taking into account
178+
rule_context (when a feature has defined rules)
179+
"""
180+
try:
181+
toggles_dict: Dict[str, Any] = self.get_configuration()
182+
except ConfigurationException:
183+
logger.error("unable to get feature toggles JSON") # noqa: E501
184+
return []
185+
ret_list = []
186+
features: Dict[str, Any] = toggles_dict.get(schema.FEATURES_KEY, {})
187+
for feature_name, feature_dict_def in features.items():
188+
rules_list = feature_dict_def.get(schema.RULES_KEY, [])
189+
feature_default_value = feature_dict_def.get(schema.FEATURE_DEFAULT_VAL_KEY)
190+
if feature_default_value and not rules_list:
191+
self._logger.debug(
192+
f"feature is enabled by default and has no defined rules, feature_name={feature_name}"
193+
)
194+
ret_list.append(feature_name)
195+
elif self._handle_rules(
196+
feature_name=feature_name,
197+
rules_context=rules_context,
198+
feature_default_value=feature_default_value,
199+
rules=rules_list,
200+
):
201+
self._logger.debug(f"feature's calculated value is True, feature_name={feature_name}")
202+
ret_list.append(feature_name)
203+
return ret_list

tests/functional/feature_toggles/test_feature_toggles.py

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict
1+
from typing import Dict, List
22

33
import pytest # noqa: F401
44
from botocore.config import Config
@@ -279,11 +279,11 @@ def test_toggles_match_rule_with_contains_action(mocker, config):
279279
mocked_app_config_schema = {
280280
"features": {
281281
"my_feature": {
282-
"feature_default_value": expected_value,
282+
"feature_default_value": False,
283283
"rules": [
284284
{
285-
"rule_name": "tenant id equals 345345435",
286-
"value_when_applies": True,
285+
"rule_name": "tenant id is contained in [6,2] ",
286+
"value_when_applies": expected_value,
287287
"restrictions": [
288288
{
289289
"action": ACTION.CONTAINS.value,
@@ -313,7 +313,7 @@ def test_toggles_no_match_rule_with_contains_action(mocker, config):
313313
"feature_default_value": expected_value,
314314
"rules": [
315315
{
316-
"rule_name": "tenant id equals 345345435",
316+
"rule_name": "tenant id is contained in [6,2] ",
317317
"value_when_applies": True,
318318
"restrictions": [
319319
{
@@ -334,3 +334,86 @@ def test_toggles_no_match_rule_with_contains_action(mocker, config):
334334
value_if_missing=False,
335335
)
336336
assert toggle == expected_value
337+
338+
339+
def test_multiple_features_enabled(mocker, config):
340+
expected_value = ["my_feature", "my_feature2"]
341+
mocked_app_config_schema = {
342+
"features": {
343+
"my_feature": {
344+
"feature_default_value": False,
345+
"rules": [
346+
{
347+
"rule_name": "tenant id is contained in [6,2] ",
348+
"value_when_applies": True,
349+
"restrictions": [
350+
{
351+
"action": ACTION.CONTAINS.value,
352+
"key": "tenant_id",
353+
"value": ["6", "2"],
354+
}
355+
],
356+
},
357+
],
358+
},
359+
"my_feature2": {
360+
"feature_default_value": True,
361+
},
362+
},
363+
}
364+
conf_store = init_configuration_store(mocker, mocked_app_config_schema, config)
365+
enabled_list: List[str] = conf_store.get_all_enabled_feature_toggles(
366+
rules_context={"tenant_id": "6", "username": "a"}
367+
)
368+
assert enabled_list == expected_value
369+
370+
371+
def test_multiple_features_only_some_enabled(mocker, config):
372+
expected_value = ["my_feature", "my_feature2", "my_feature4"]
373+
mocked_app_config_schema = {
374+
"features": {
375+
"my_feature": { # rule will match here, feature is enabled due to rule match
376+
"feature_default_value": False,
377+
"rules": [
378+
{
379+
"rule_name": "tenant id is contained in [6,2] ",
380+
"value_when_applies": True,
381+
"restrictions": [
382+
{
383+
"action": ACTION.CONTAINS.value,
384+
"key": "tenant_id",
385+
"value": ["6", "2"],
386+
}
387+
],
388+
},
389+
],
390+
},
391+
"my_feature2": {
392+
"feature_default_value": True,
393+
},
394+
"my_feature3": {
395+
"feature_default_value": False,
396+
},
397+
"my_feature4": { # rule will not match here, feature is enabled by default
398+
"feature_default_value": True,
399+
"rules": [
400+
{
401+
"rule_name": "tenant id equals 7",
402+
"value_when_applies": False,
403+
"restrictions": [
404+
{
405+
"action": ACTION.EQUALS.value,
406+
"key": "tenant_id",
407+
"value": "7",
408+
}
409+
],
410+
},
411+
],
412+
},
413+
},
414+
}
415+
conf_store = init_configuration_store(mocker, mocked_app_config_schema, config)
416+
enabled_list: List[str] = conf_store.get_all_enabled_feature_toggles(
417+
rules_context={"tenant_id": "6", "username": "a"}
418+
)
419+
assert enabled_list == expected_value

0 commit comments

Comments
 (0)