1import os 2import sys 3import json 4import boto3 5import logging 6import hashlib 7 8from botocore.config import Config 9from samtranslator.feature_toggle.dialup import ( 10 DisabledDialup, 11 ToggleDialup, 12 SimpleAccountPercentileDialup, 13) 14 15my_path = os.path.dirname(os.path.abspath(__file__)) 16sys.path.insert(0, my_path + "/..") 17 18LOG = logging.getLogger(__name__) 19 20 21class FeatureToggle: 22 """ 23 FeatureToggle is the class which will provide methods to query and decide if a feature is enabled based on where 24 SAM is executing or not. 25 """ 26 27 DIALUP_RESOLVER = { 28 "toggle": ToggleDialup, 29 "account-percentile": SimpleAccountPercentileDialup, 30 } 31 32 def __init__(self, config_provider, stage, account_id, region): 33 self.feature_config = config_provider.config 34 self.stage = stage 35 self.account_id = account_id 36 self.region = region 37 38 def _get_dialup(self, region_config, feature_name): 39 """ 40 get the right dialup instance 41 if no dialup type is provided or the specified dialup is not supported, 42 an instance of DisabledDialup will be returned 43 44 :param region_config: region config 45 :param feature_name: feature_name 46 :return: an instance of 47 """ 48 dialup_type = region_config.get("type") 49 if dialup_type in FeatureToggle.DIALUP_RESOLVER: 50 return FeatureToggle.DIALUP_RESOLVER[dialup_type]( 51 region_config, account_id=self.account_id, feature_name=feature_name 52 ) 53 LOG.warning("Dialup type '{}' is None or is not supported.".format(dialup_type)) 54 return DisabledDialup(region_config) 55 56 def is_enabled(self, feature_name): 57 """ 58 To check if feature is available 59 60 :param feature_name: name of feature 61 """ 62 if feature_name not in self.feature_config: 63 LOG.warning("Feature '{}' not available in Feature Toggle Config.".format(feature_name)) 64 return False 65 66 stage = self.stage 67 region = self.region 68 account_id = self.account_id 69 if not stage or not region or not account_id: 70 LOG.warning( 71 "One or more of stage, region and account_id is not set. Feature '{}' not enabled.".format(feature_name) 72 ) 73 return False 74 75 stage_config = self.feature_config.get(feature_name, {}).get(stage, {}) 76 if not stage_config: 77 LOG.info("Stage '{}' not enabled for Feature '{}'.".format(stage, feature_name)) 78 return False 79 80 if account_id in stage_config: 81 account_config = stage_config[account_id] 82 region_config = account_config[region] if region in account_config else account_config.get("default", {}) 83 else: 84 region_config = stage_config[region] if region in stage_config else stage_config.get("default", {}) 85 86 dialup = self._get_dialup(region_config, feature_name=feature_name) 87 LOG.info("Using Dialip {}".format(dialup)) 88 is_enabled = dialup.is_enabled() 89 90 LOG.info("Feature '{}' is enabled: '{}'".format(feature_name, is_enabled)) 91 return is_enabled 92 93 94class FeatureToggleConfigProvider: 95 """Interface for all FeatureToggle config providers""" 96 97 def __init__(self): 98 pass 99 100 @property 101 def config(self): 102 raise NotImplementedError 103 104 105class FeatureToggleDefaultConfigProvider(FeatureToggleConfigProvider): 106 """Default config provider, always return False for every query.""" 107 108 def __init__(self): 109 FeatureToggleConfigProvider.__init__(self) 110 111 @property 112 def config(self): 113 return {} 114 115 116class FeatureToggleLocalConfigProvider(FeatureToggleConfigProvider): 117 """Feature toggle config provider which uses a local file. This is to facilitate local testing.""" 118 119 def __init__(self, local_config_path): 120 FeatureToggleConfigProvider.__init__(self) 121 with open(local_config_path, "r") as f: 122 config_json = f.read() 123 self.feature_toggle_config = json.loads(config_json) 124 125 @property 126 def config(self): 127 return self.feature_toggle_config 128 129 130class FeatureToggleAppConfigConfigProvider(FeatureToggleConfigProvider): 131 """Feature toggle config provider which loads config from AppConfig.""" 132 133 def __init__(self, application_id, environment_id, configuration_profile_id): 134 FeatureToggleConfigProvider.__init__(self) 135 try: 136 LOG.info("Loading feature toggle config from AppConfig...") 137 # Lambda function has 120 seconds limit 138 # (5 + 5) * 2, 20 seconds maximum timeout duration 139 # In case of high latency from AppConfig, we can always fall back to use an empty config and continue transform 140 client_config = Config(connect_timeout=5, read_timeout=5, retries={"total_max_attempts": 2}) 141 self.app_config_client = boto3.client("appconfig", config=client_config) 142 response = self.app_config_client.get_configuration( 143 Application=application_id, 144 Environment=environment_id, 145 Configuration=configuration_profile_id, 146 ClientId="FeatureToggleAppConfigConfigProvider", 147 ) 148 binary_config_string = response["Content"].read() 149 self.feature_toggle_config = json.loads(binary_config_string.decode("utf-8")) 150 LOG.info("Finished loading feature toggle config from AppConfig.") 151 except Exception as ex: 152 LOG.error("Failed to load config from AppConfig: {}. Using empty config.".format(ex)) 153 # There is chance that AppConfig is not available in a particular region. 154 self.feature_toggle_config = json.loads("{}") 155 156 @property 157 def config(self): 158 return self.feature_toggle_config 159