-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Expand file tree
/
Copy pathfeature_toggle.py
More file actions
169 lines (136 loc) · 6.31 KB
/
feature_toggle.py
File metadata and controls
169 lines (136 loc) · 6.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import json
import logging
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Dict, Optional, cast
import boto3
from botocore.config import Config
from samtranslator.feature_toggle.dialup import (
DisabledDialup,
SimpleAccountPercentileDialup,
ToggleDialup,
)
from samtranslator.metrics.method_decorator import cw_timer
from samtranslator.utils.constants import BOTO3_CONNECT_TIMEOUT
LOG = logging.getLogger(__name__)
class FeatureToggle:
"""
FeatureToggle is the class which will provide methods to query and decide if a feature is enabled based on where
SAM is executing or not.
"""
DIALUP_RESOLVER = {
"toggle": ToggleDialup,
"account-percentile": SimpleAccountPercentileDialup,
}
def __init__(
self,
config_provider: "FeatureToggleConfigProvider",
stage: Optional[str],
account_id: Optional[str],
region: Optional[str],
) -> None:
self.feature_config = config_provider.config
self.stage = stage
self.account_id = account_id
self.region = region
def _get_dialup(self, region_config, feature_name): # type: ignore[no-untyped-def]
"""
get the right dialup instance
if no dialup type is provided or the specified dialup is not supported,
an instance of DisabledDialup will be returned
:param region_config: region config
:param feature_name: feature_name
:return: an instance of
"""
dialup_type = region_config.get("type")
if dialup_type in FeatureToggle.DIALUP_RESOLVER:
return FeatureToggle.DIALUP_RESOLVER[dialup_type](
region_config, account_id=self.account_id, feature_name=feature_name
)
LOG.warning(f"Dialup type '{dialup_type}' is None or is not supported.")
return DisabledDialup(region_config)
def is_enabled(self, feature_name: str) -> bool:
"""
To check if feature is available
:param feature_name: name of feature
"""
# Check if feature is enabled for this stage, region, and account
if feature_name not in self.feature_config:
LOG.warning(f"Feature '{feature_name}' not available in Feature Toggle Config.")
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(
f"One or more of stage, region and account_id is not set. Feature '{feature_name}' not enabled."
)
return False
stage_config = self.feature_config.get(feature_name, {}).get(stage, {})
if not stage_config:
LOG.info(f"Stage '{stage}' not enabled for Feature '{feature_name}'.")
return 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) # type: ignore[no-untyped-call]
LOG.info(f"Using Dialip {dialup}")
is_enabled: bool = dialup.is_enabled()
LOG.info(f"Feature '{feature_name}' is enabled: '{is_enabled}'")
return is_enabled
class FeatureToggleConfigProvider(ABC):
"""Interface for all FeatureToggle config providers"""
@property
@abstractmethod
def config(self) -> Dict[str, Any]:
pass
class FeatureToggleDefaultConfigProvider(FeatureToggleConfigProvider):
"""Default config provider, always return False for every query."""
def __init__(self) -> None:
FeatureToggleConfigProvider.__init__(self)
@property
def config(self) -> Dict[str, Any]:
return {}
class FeatureToggleLocalConfigProvider(FeatureToggleConfigProvider):
"""Feature toggle config provider which uses a local file. This is to facilitate local testing."""
def __init__(self, local_config_path: str) -> None:
FeatureToggleConfigProvider.__init__(self)
config_json = Path(local_config_path).read_text(encoding="utf-8")
self.feature_toggle_config = cast(Dict[str, Any], json.loads(config_json))
@property
def config(self) -> Dict[str, Any]:
return self.feature_toggle_config
class FeatureToggleAppConfigConfigProvider(FeatureToggleConfigProvider):
"""Feature toggle config provider which loads config from AppConfig."""
@cw_timer(prefix="External", name="AppConfig")
def __init__(self, application_id, environment_id, configuration_profile_id, app_config_client=None) -> None: # type: ignore[no-untyped-def]
FeatureToggleConfigProvider.__init__(self)
try:
LOG.info("Loading feature toggle config from AppConfig...")
# Lambda function has 120 seconds limit
# (5 + 5) * 2, 20 seconds maximum timeout duration
# In case of high latency from AppConfig, we can always fall back to use an empty config and continue transform
client_config = Config(
connect_timeout=BOTO3_CONNECT_TIMEOUT, read_timeout=5, retries={"total_max_attempts": 2}
)
self.app_config_client = (
app_config_client if app_config_client else boto3.client("appconfig", config=client_config)
)
response = self.app_config_client.get_configuration(
Application=application_id,
Environment=environment_id,
Configuration=configuration_profile_id,
ClientId="FeatureToggleAppConfigConfigProvider",
)
binary_config_string = response["Content"].read()
self.feature_toggle_config = cast(Dict[str, Any], json.loads(binary_config_string.decode("utf-8")))
LOG.info("Finished loading feature toggle config from AppConfig.")
except Exception:
LOG.exception("Failed to load config from AppConfig. Using empty config.")
# There is chance that AppConfig is not available in a particular region.
self.feature_toggle_config = {}
@property
def config(self) -> Dict[str, Any]:
return self.feature_toggle_config