From d0c038854c70a64e03857cd804210b331a0f37c2 Mon Sep 17 00:00:00 2001 From: "Gerald W. Lester" Date: Wed, 29 Sep 2021 12:59:25 -0500 Subject: [PATCH 1/6] Changes to "contains" logic to clarify and expand. --- .../utilities/feature_flags/feature_flags.py | 4 + .../utilities/feature_flags/schema.py | 4 + docs/utilities/feature_flags.md | 2 +- .../feature_flags/test_feature_flags.py | 195 ++++++++++++++++++ .../feature_flags/test_schema_validation.py | 20 ++ 5 files changed, 224 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index d04e74ff293..b74711bfd52 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -48,6 +48,10 @@ def _match_by_action(action: str, condition_value: Any, context_value: Any) -> b schema.RuleAction.ENDSWITH.value: lambda a, b: a.endswith(b), schema.RuleAction.IN.value: lambda a, b: a in b, schema.RuleAction.NOT_IN.value: lambda a, b: a not in b, + schema.RuleAction.KEY_IN_VALUE.value: lambda a, b: a in b, + schema.RuleAction.KEY_NOT_IN_VALUE.value: lambda a, b: a not in b, + schema.RuleAction.VALUE_IN_KEY.value: lambda a, b: b in a, + schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a, } try: diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index efce82018db..21a303f49dc 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -22,6 +22,10 @@ class RuleAction(str, Enum): ENDSWITH = "ENDSWITH" IN = "IN" NOT_IN = "NOT_IN" + KEY_IN_VALUE = "KEY_IN_VALUE" + KEY_NOT_IN_VALUE = "KEY_NOT_IN_VALUE" + VALUE_IN_KEY = "VALUE_IN_KEY" + VALUE_NOT_IN_KEY = "VALUE_NOT_IN_KEY" class SchemaValidator(BaseValidator): diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index d22f9c03296..453ab123d72 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -450,7 +450,7 @@ The `conditions` block is a list of conditions that contain `action`, `key`, and } ``` -The `action` configuration can have 5 different values: `EQUALS`, `STARTSWITH`, `ENDSWITH`, `IN`, `NOT_IN`. +The `action` configuration can have 5 different values: `EQUALS`, `STARTSWITH`, `ENDSWITH`, `KEY_IN_VALUE`, `KEY_NOT_IN_VALUE`, `VALUE_IN_KEY`, and `VALUE_NOT_IN_KEY`. Note that `IN` and `NOT_IN` are also defined and are synonymous with `KEY_IN_VALUE` and `KEY_NOT_IN_VALUE` respectively. The `key` and `value` will be compared to the input from the context parameter. diff --git a/tests/functional/feature_flags/test_feature_flags.py b/tests/functional/feature_flags/test_feature_flags.py index 5342105da3d..25a8220b247 100644 --- a/tests/functional/feature_flags/test_feature_flags.py +++ b/tests/functional/feature_flags/test_feature_flags.py @@ -301,6 +301,8 @@ def test_flags_conditions_rule_match_multiple_actions_multiple_rules_multiple_co # check a case where the feature exists but the rule doesn't match so we revert to the default value of the feature + +# Check IN/NOT_IN/KEY_IN_VALUE/KEY_NOT_IN_VALUE/VALUE_IN_KEY/VALUE_NOT_IN_KEY conditions def test_flags_match_rule_with_in_action(mocker, config): expected_value = True mocked_app_config_schema = { @@ -395,8 +397,201 @@ def test_flags_no_match_rule_with_not_in_action(mocker, config): feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False) assert toggle == expected_value + +def test_flags_match_rule_with_key_in_value_action(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant id is contained in [6, 2]": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.KEY_IN_VALUE.value, + "key": "tenant_id", + "value": ["6", "2"], + } + ], + } + }, + } + } + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False) + assert toggle == expected_value + + +def test_flags_no_match_rule_with_key_in_value_action(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + "default": expected_value, + "rules": { + "tenant id is contained in [8, 2]": { + "when_match": True, + "conditions": [ + { + "action": RuleAction.KEY_IN_VALUE.value, + "key": "tenant_id", + "value": ["8", "2"], + } + ], + } + }, + } + } + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False) + assert toggle == expected_value + + +def test_flags_match_rule_with_key_not_in_value_action(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant id is contained in [8, 2]": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.KEY_NOT_IN_VALUE.value, + "key": "tenant_id", + "value": ["10", "4"], + } + ], + } + }, + } + } + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False) + assert toggle == expected_value + + +def test_flags_no_match_rule_with_key_not_in_value_action(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + "default": expected_value, + "rules": { + "tenant id is contained in [8, 2]": { + "when_match": True, + "conditions": [ + { + "action": RuleAction.KEY_NOT_IN_VALUE.value, + "key": "tenant_id", + "value": ["6", "4"], + } + ], + } + }, + } + } + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False) + assert toggle == expected_value + +def test_flags_match_rule_with_value_in_key_action(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant id is contained in [6, 2]": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.VALUE_IN_KEY.value, + "key": "groups", + "value": "SYSADMIN", + } + ], + } + }, + } + } + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False) + assert toggle == expected_value + + +def test_flags_no_match_rule_with_value_in_key_action(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + "default": expected_value, + "rules": { + "tenant id is contained in [8, 2]": { + "when_match": True, + "conditions": [ + { + "action": RuleAction.VALUE_IN_KEY.value, + "key": "groups", + "value": "GUEST", + } + ], + } + }, + } + } + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False) + assert toggle == expected_value + + +def test_flags_match_rule_with_value_not_in_key_action(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + "default": False, + "rules": { + "tenant id is contained in [8, 2]": { + "when_match": expected_value, + "conditions": [ + { + "action": RuleAction.VALUE_NOT_IN_KEY.value, + "key": "groups", + "value": "GUEST", + } + ], + } + }, + } + } + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False) + assert toggle == expected_value + + +def test_flags_no_match_rule_with_value_not_in_key_action(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + "default": expected_value, + "rules": { + "tenant id is contained in [8, 2]": { + "when_match": True, + "conditions": [ + { + "action": RuleAction.VALUE_NOT_IN_KEY.value, + "key": "groups", + "value": "SYSADMIN", + } + ], + } + }, + } + } + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False) + assert toggle == expected_value + + +# Check multiple features def test_multiple_features_enabled(mocker, config): expected_value = ["my_feature", "my_feature2"] mocked_app_config_schema = { diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/test_schema_validation.py index ce85494afce..1cd14aa4287 100644 --- a/tests/functional/feature_flags/test_schema_validation.py +++ b/tests/functional/feature_flags/test_schema_validation.py @@ -220,6 +220,26 @@ def test_valid_condition_all_actions(): CONDITION_KEY: "username", CONDITION_VALUE: ["c"], }, + { + CONDITION_ACTION: RuleAction.KEY_IN_VALUE.value, + CONDITION_KEY: "username", + CONDITION_VALUE: ["a", "b"], + }, + { + CONDITION_ACTION: RuleAction.KEY_NOT_IN_VALUE.value, + CONDITION_KEY: "username", + CONDITION_VALUE: ["c"], + }, + { + CONDITION_ACTION: RuleAction.VALUE_IN_KEY.value, + CONDITION_KEY: "groups", + CONDITION_VALUE: "SYSADMIN", + }, + { + CONDITION_ACTION: RuleAction.VALUE_NOT_IN_KEY.value, + CONDITION_KEY: "groups", + CONDITION_VALUE: "GUEST", + }, ], } }, From a17060e5679f3544384d187ffea0bf7f769b31d2 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 1 Oct 2021 16:58:28 +0200 Subject: [PATCH 2/6] chore: update schema spec w/ new conditions --- aws_lambda_powertools/utilities/feature_flags/schema.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index 21a303f49dc..c24a1ba5048 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -84,7 +84,9 @@ class SchemaValidator(BaseValidator): The value MUST contain the following members: * **action**: `str`. Operation to perform to match a key and value. - The value MUST be either EQUALS, STARTSWITH, ENDSWITH, IN, NOT_IN + The value MUST be either EQUALS, STARTSWITH, ENDSWITH, IN, NOT_IN, + KEY_IN_VALUE KEY_NOT_IN_VALUE VALUE_IN_KEY VALUE_NOT_IN_KEY + * **key**: `str`. Key in given context to perform operation * **value**: `Any`. Value in given context that should match action operation. From 4c559eb8346cdccb4195cb5977ac00d566a649c6 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 1 Oct 2021 17:19:06 +0200 Subject: [PATCH 3/6] fix: feat flag description for new rule actions --- .../feature_flags/test_feature_flags.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/functional/feature_flags/test_feature_flags.py b/tests/functional/feature_flags/test_feature_flags.py index 25a8220b247..4b96ce8fa74 100644 --- a/tests/functional/feature_flags/test_feature_flags.py +++ b/tests/functional/feature_flags/test_feature_flags.py @@ -397,7 +397,8 @@ def test_flags_no_match_rule_with_not_in_action(mocker, config): feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False) assert toggle == expected_value - + + def test_flags_match_rule_with_key_in_value_action(mocker, config): expected_value = True mocked_app_config_schema = { @@ -492,14 +493,15 @@ def test_flags_no_match_rule_with_key_not_in_value_action(mocker, config): feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False) assert toggle == expected_value - + + def test_flags_match_rule_with_value_in_key_action(mocker, config): expected_value = True mocked_app_config_schema = { "my_feature": { "default": False, "rules": { - "tenant id is contained in [6, 2]": { + "user is in the SYSADMIN group": { "when_match": expected_value, "conditions": [ { @@ -513,7 +515,9 @@ def test_flags_match_rule_with_value_in_key_action(mocker, config): } } feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False) + toggle = feature_flags.evaluate( + name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False + ) assert toggle == expected_value @@ -537,7 +541,9 @@ def test_flags_no_match_rule_with_value_in_key_action(mocker, config): } } feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False) + toggle = feature_flags.evaluate( + name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False + ) assert toggle == expected_value @@ -547,7 +553,7 @@ def test_flags_match_rule_with_value_not_in_key_action(mocker, config): "my_feature": { "default": False, "rules": { - "tenant id is contained in [8, 2]": { + "user is in the GUEST group": { "when_match": expected_value, "conditions": [ { @@ -561,7 +567,9 @@ def test_flags_match_rule_with_value_not_in_key_action(mocker, config): } } feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False) + toggle = feature_flags.evaluate( + name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False + ) assert toggle == expected_value @@ -571,7 +579,7 @@ def test_flags_no_match_rule_with_value_not_in_key_action(mocker, config): "my_feature": { "default": expected_value, "rules": { - "tenant id is contained in [8, 2]": { + "user is in the SYSADMIN group": { "when_match": True, "conditions": [ { @@ -585,12 +593,12 @@ def test_flags_no_match_rule_with_value_not_in_key_action(mocker, config): } } feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False) + toggle = feature_flags.evaluate( + name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False + ) assert toggle == expected_value - - # Check multiple features def test_multiple_features_enabled(mocker, config): expected_value = ["my_feature", "my_feature2"] From 4623e30c1f0804ca09ccb5d2b8dd5f47fced7e32 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 1 Oct 2021 17:19:42 +0200 Subject: [PATCH 4/6] docs: add new section on GA deprecations --- docs/utilities/feature_flags.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 453ab123d72..76671fbd98a 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -650,3 +650,11 @@ Method | When to use | Requires new deployment on changes | Supported services **[Environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html){target="_blank"}** | Simple configuration that will rarely if ever change, because changing it requires a Lambda function deployment. | Yes | Lambda **[Parameters utility](parameters.md)** | Access to secrets, or fetch parameters in different formats from AWS System Manager Parameter Store or Amazon DynamoDB. | No | Parameter Store, DynamoDB, Secrets Manager, AppConfig **Feature flags utility** | Rule engine to define when one or multiple features should be enabled depending on the input. | No | AppConfig + + +## Deprecation list when GA + +Breaking change | Recommendation +------------------------------------------------- | --------------------------------------------------------------------------------- +`IN` RuleAction | Use `KEY_IN_VALUE` instead +`NOT_IN` RuleAction | Use `KEY_NOT_IN_VALUE` instead From 8f0d26a5a18d3e477a1908049ace5b8e538ce6b4 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 1 Oct 2021 17:35:01 +0200 Subject: [PATCH 5/6] docs: table for RuleAction, remove mentions --- docs/utilities/feature_flags.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 76671fbd98a..eed4978983e 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -366,7 +366,7 @@ You can use `get_enabled_features` method for scenarios where you need a list of "when_match": true, "conditions": [ { - "action": "IN", + "action": "KEY_IN_VALUE", "key": "CloudFront-Viewer-Country", "value": ["NL", "IE", "UK", "PL", "PT"] } @@ -450,9 +450,20 @@ The `conditions` block is a list of conditions that contain `action`, `key`, and } ``` -The `action` configuration can have 5 different values: `EQUALS`, `STARTSWITH`, `ENDSWITH`, `KEY_IN_VALUE`, `KEY_NOT_IN_VALUE`, `VALUE_IN_KEY`, and `VALUE_NOT_IN_KEY`. Note that `IN` and `NOT_IN` are also defined and are synonymous with `KEY_IN_VALUE` and `KEY_NOT_IN_VALUE` respectively. +The `action` configuration can have the following values, where the expressions **`a`** is the `key` and **`b`** is the `value` above: -The `key` and `value` will be compared to the input from the context parameter. +Action | Equivalent expression +------------------------------------------------- | --------------------------------------------------------------------------------- +**EQUALS** | `lambda a, b: a == b` +**STARTSWITH** | `lambda a, b: a.startswith(b)` +**ENDSWITH** | `lambda a, b: a.endswith(b)` +**KEY_IN_VALUE** | `lambda a, b: a in b` +**KEY_NOT_IN_VALUE** | `lambda a, b: a not in b` +**VALUE_IN_KEY** | `lambda a, b: b in a` +**VALUE_NOT_IN_KEY** | `lambda a, b: b not in a` + + +!!! info "The `**key**` and `**value**` will be compared to the input from the `**context**` parameter." **For multiple conditions**, we will evaluate the list of conditions as a logical `AND`, so all conditions needs to match to return `when_match` value. From 6cfb9db20e758fd874e9eeadc9d48931de662d4d Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 1 Oct 2021 17:35:59 +0200 Subject: [PATCH 6/6] chore: remove IN, NOT_IN from schema docs --- aws_lambda_powertools/utilities/feature_flags/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index c24a1ba5048..68f8ebd9bca 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -84,7 +84,7 @@ class SchemaValidator(BaseValidator): The value MUST contain the following members: * **action**: `str`. Operation to perform to match a key and value. - The value MUST be either EQUALS, STARTSWITH, ENDSWITH, IN, NOT_IN, + The value MUST be either EQUALS, STARTSWITH, ENDSWITH, KEY_IN_VALUE KEY_NOT_IN_VALUE VALUE_IN_KEY VALUE_NOT_IN_KEY * **key**: `str`. Key in given context to perform operation