-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PRMDR-674: AppConfig Feature Flags lambda (#299)
Feature flags added as a python service and api gateway endpoint. Can handle singular or full list of flags. --------- Co-authored-by: Scott Alexander <scott.alexander@madetech.com>
- Loading branch information
1 parent
ccf6a5d
commit dfbce6a
Showing
11 changed files
with
506 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import json | ||
|
||
from enums.logging_app_interaction import LoggingAppInteraction | ||
from services.feature_flags_service import FeatureFlagService | ||
from utils.audit_logging_setup import LoggingService | ||
from utils.decorators.ensure_env_var import ensure_environment_variables | ||
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions | ||
from utils.decorators.override_error_check import override_error_check | ||
from utils.decorators.set_audit_arg import set_request_context_for_logging | ||
from utils.lambda_response import ApiGatewayResponse | ||
from utils.request_context import request_context | ||
|
||
logger = LoggingService(__name__) | ||
|
||
|
||
@set_request_context_for_logging | ||
@ensure_environment_variables( | ||
names=["APPCONFIG_APPLICATION", "APPCONFIG_CONFIGURATION", "APPCONFIG_ENVIRONMENT"] | ||
) | ||
@handle_lambda_exceptions | ||
@override_error_check | ||
def lambda_handler(event, context): | ||
request_context.app_interaction = LoggingAppInteraction.FEATURE_FLAGS.value | ||
logger.info("Starting feature flag retrieval process") | ||
|
||
query_string_params = event.get("queryStringParameters") | ||
feature_flag_service = FeatureFlagService() | ||
|
||
if query_string_params and "flagName" in query_string_params: | ||
flag_name = query_string_params["flagName"] | ||
response = feature_flag_service.get_feature_flags_by_flag(flag_name) | ||
else: | ||
response = feature_flag_service.get_feature_flags() | ||
|
||
return ApiGatewayResponse( | ||
200, json.dumps(response), "GET" | ||
).create_api_gateway_response() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from typing import Dict, Optional | ||
|
||
from pydantic import BaseModel | ||
|
||
|
||
class FlagStatus(BaseModel): | ||
enabled: bool | ||
|
||
|
||
class FeatureFlag(BaseModel): | ||
feature_flags: Optional[Dict[str, FlagStatus]] = {} | ||
|
||
def format_flags(self): | ||
return {key: value.enabled for key, value in self.feature_flags.items()} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,5 @@ pip-audit==2.6.1 | |
pytest-cov==4.1.0 | ||
pytest-mock==3.11.1 | ||
pytest==7.4.3 | ||
requests_mock==1.11.0 | ||
ruff==0.0.284 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import os | ||
|
||
import requests | ||
from enums.lambda_error import LambdaError | ||
from models.feature_flags import FeatureFlag | ||
from pydantic import ValidationError | ||
from requests.exceptions import JSONDecodeError | ||
from utils.audit_logging_setup import LoggingService | ||
from utils.lambda_exceptions import FeatureFlagsException | ||
|
||
logger = LoggingService(__name__) | ||
|
||
|
||
class FeatureFlagService: | ||
def __init__(self): | ||
app_config_port = 2772 | ||
self.app_config_url = ( | ||
f"http://localhost:{app_config_port}" | ||
+ f'/applications/{os.environ["APPCONFIG_APPLICATION"]}' | ||
+ f'/environments/{os.environ["APPCONFIG_ENVIRONMENT"]}' | ||
+ f'/configurations/{os.environ["APPCONFIG_CONFIGURATION"]}' | ||
) | ||
|
||
@staticmethod | ||
def request_app_config_data(url: str): | ||
config_data = requests.get(url) | ||
try: | ||
data = config_data.json() | ||
except JSONDecodeError as e: | ||
logger.error( | ||
str(e), | ||
{"Result": "Error when retrieving feature flag from AppConfig profile"}, | ||
) | ||
raise FeatureFlagsException( | ||
error=LambdaError.FeatureFlagParseError, | ||
status_code=config_data.status_code, | ||
) | ||
|
||
if config_data.status_code == 200: | ||
return data | ||
if config_data.status_code == 400: | ||
logger.error( | ||
str(data), | ||
{"Result": "Error when retrieving feature flag from AppConfig profile"}, | ||
) | ||
raise FeatureFlagsException( | ||
error=LambdaError.FeatureFlagNotFound, | ||
status_code=404, | ||
) | ||
else: | ||
logger.error( | ||
str(data), | ||
{"Result": "Error when retrieving feature flag from AppConfig profile"}, | ||
) | ||
raise FeatureFlagsException( | ||
error=LambdaError.FeatureFlagFailure, | ||
status_code=config_data.status_code, | ||
) | ||
|
||
def get_feature_flags(self) -> dict: | ||
logger.info("Retrieving all feature flags") | ||
|
||
url = self.app_config_url | ||
response = self.request_app_config_data(url) | ||
|
||
try: | ||
feature_flags = FeatureFlag(feature_flags=response) | ||
return feature_flags.format_flags() | ||
except ValidationError as e: | ||
logger.error( | ||
str(e), | ||
{"Result": "Error when retrieving feature flag from AppConfig profile"}, | ||
) | ||
raise FeatureFlagsException( | ||
error=LambdaError.FeatureFlagParseError, | ||
status_code=500, | ||
) | ||
|
||
def get_feature_flags_by_flag(self, flag: str): | ||
logger.info(f"Retrieving feature flag: {flag}") | ||
|
||
config_url = self.app_config_url | ||
url = config_url + f"?flag={flag}" | ||
|
||
response = self.request_app_config_data(url) | ||
|
||
try: | ||
feature_flag = FeatureFlag(feature_flags={flag: response}) | ||
return feature_flag.format_flags() | ||
except ValidationError as e: | ||
logger.error( | ||
str(e), | ||
{"Result": "Error when retrieving feature flag from AppConfig profile"}, | ||
) | ||
raise FeatureFlagsException( | ||
error=LambdaError.FeatureFlagParseError, | ||
status_code=500, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
lambdas/tests/unit/handlers/test_feature_flags_handler.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import json | ||
|
||
import pytest | ||
from handlers.feature_flags_handler import lambda_handler | ||
from tests.unit.conftest import MockError | ||
from utils.lambda_exceptions import FeatureFlagsException | ||
from utils.lambda_response import ApiGatewayResponse | ||
|
||
test_all_feature_flags = { | ||
"testFeature1": True, | ||
"testFeature2": True, | ||
"testFeature3": False, | ||
} | ||
|
||
|
||
@pytest.fixture | ||
def all_feature_flags_event(): | ||
api_gateway_proxy_event = { | ||
"httpMethod": "GET", | ||
"queryStringParameters": {}, | ||
} | ||
return api_gateway_proxy_event | ||
|
||
|
||
@pytest.fixture | ||
def filter_feature_flags_event(): | ||
api_gateway_proxy_event = { | ||
"httpMethod": "GET", | ||
"queryStringParameters": {"flagName": "testFeature1"}, | ||
} | ||
return api_gateway_proxy_event | ||
|
||
|
||
@pytest.fixture | ||
def mock_feature_flag_service(set_env, mocker): | ||
mocked_class = mocker.patch("handlers.feature_flags_handler.FeatureFlagService") | ||
mocked_instance = mocked_class.return_value | ||
yield mocked_instance | ||
|
||
|
||
@pytest.fixture | ||
def mock_service_all_feature_flags(mock_feature_flag_service): | ||
service = mock_feature_flag_service | ||
service.get_feature_flags.return_value = test_all_feature_flags | ||
yield service | ||
|
||
|
||
def test_lambda_handler_all_flags_returns_200( | ||
all_feature_flags_event, context, mock_service_all_feature_flags | ||
): | ||
expected = ApiGatewayResponse( | ||
200, json.dumps(test_all_feature_flags), "GET" | ||
).create_api_gateway_response() | ||
|
||
actual = lambda_handler(all_feature_flags_event, context) | ||
|
||
assert expected == actual | ||
|
||
|
||
def test_lambda_handler_all_flags_without_query_string_params_returns_200( | ||
event, context, mock_service_all_feature_flags | ||
): | ||
expected = ApiGatewayResponse( | ||
200, json.dumps(test_all_feature_flags), "GET" | ||
).create_api_gateway_response() | ||
|
||
actual = lambda_handler(event, context) | ||
|
||
assert expected == actual | ||
|
||
|
||
def test_lambda_handler_with_filter_flags_returns_200( | ||
filter_feature_flags_event, context, mock_feature_flag_service | ||
): | ||
feature_flags = {"testFeature1": True} | ||
mock_feature_flag_service.get_feature_flags_by_flag.return_value = feature_flags | ||
|
||
expected = ApiGatewayResponse( | ||
200, json.dumps(feature_flags), "GET" | ||
).create_api_gateway_response() | ||
|
||
actual = lambda_handler(filter_feature_flags_event, context) | ||
|
||
assert expected == actual | ||
|
||
|
||
def test_lambda_handler_handles_service_raises_exception( | ||
event, context, mock_feature_flag_service | ||
): | ||
mock_feature_flag_service.get_feature_flags.side_effect = FeatureFlagsException( | ||
500, MockError.Error | ||
) | ||
|
||
expected = ApiGatewayResponse( | ||
500, json.dumps(MockError.Error.value), "GET" | ||
).create_api_gateway_response() | ||
|
||
actual = lambda_handler(event, context) | ||
|
||
assert expected == actual |
Oops, something went wrong.