Skip to content

Commit 818190e

Browse files
Merge pull request #50 from hellohaptik/develop
Redis Auth sentinel support
2 parents 5090649 + 7e76fa6 commit 818190e

3 files changed

Lines changed: 142 additions & 26 deletions

File tree

FeatureToggle/__init__.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Python Imports
2-
import redis
2+
33
from redis.exceptions import LockError, BusyLoadingError, ConnectionError, RedisError
44
import pickle
55
from typing import Dict, Any, Optional
@@ -9,6 +9,7 @@
99
from UnleashClient import UnleashClient
1010
from UnleashClient.utils import LOGGER
1111
from FeatureToggle.utils import timed_lru_cache
12+
from FeatureToggle.redis_utils import RedisConnector
1213

1314

1415
def split_and_strip(parameters: str):
@@ -29,6 +30,11 @@ class FeatureToggles:
2930
__environment = None
3031
__cache = None
3132
__enable_toggle_service = True
33+
__sentinel_enabled = False
34+
__sentinels = None
35+
__sentinel_service_name = None
36+
__redis_auth_enabled = False
37+
__redis_password = None
3238

3339
@staticmethod
3440
def initialize(url: str,
@@ -37,9 +43,15 @@ def initialize(url: str,
3743
cas_name: str,
3844
environment: str,
3945
redis_host: str,
40-
redis_port: str,
41-
redis_db: str,
42-
enable_toggle_service: bool = True) -> None:
46+
redis_port: int,
47+
redis_db: int,
48+
enable_toggle_service: bool = True,
49+
sentinel_enabled: bool = False,
50+
sentinels: Optional[list] = None,
51+
sentinel_service_name: Optional[str] = None,
52+
redis_auth_enabled: bool = False,
53+
redis_password: Optional[str] = None
54+
) -> None:
4355
""" Static access method. """
4456
if FeatureToggles.__client is None:
4557
FeatureToggles.__url = url
@@ -51,6 +63,11 @@ def initialize(url: str,
5163
FeatureToggles.__redis_port = redis_port
5264
FeatureToggles.__redis_db = redis_db
5365
FeatureToggles.__enable_toggle_service = enable_toggle_service
66+
FeatureToggles.__sentinel_enabled = sentinel_enabled
67+
FeatureToggles.__sentinels = sentinels
68+
FeatureToggles.__sentinel_service_name = sentinel_service_name
69+
FeatureToggles.__redis_auth_enabled = redis_auth_enabled
70+
FeatureToggles.__redis_password = redis_password
5471
FeatureToggles.__cache = FeatureToggles.__get_cache()
5572
LOGGER.info(f'Initializing Feature toggles')
5673
else:
@@ -62,11 +79,16 @@ def __get_cache():
6279
Create redis connection
6380
"""
6481
if FeatureToggles.__cache is None:
65-
FeatureToggles.__cache = redis.Redis(
66-
host=FeatureToggles.__redis_host,
67-
port=FeatureToggles.__redis_port,
68-
db=FeatureToggles.__redis_db
69-
)
82+
if FeatureToggles.__sentinel_enabled:
83+
FeatureToggles.__cache = RedisConnector.get_sentinel_connection(
84+
FeatureToggles.__sentinels, FeatureToggles.__sentinel_service_name, FeatureToggles.__redis_db,
85+
FeatureToggles.__redis_auth_enabled, FeatureToggles.__redis_password
86+
)
87+
else:
88+
FeatureToggles.__cache = RedisConnector.get_non_sentinel_connection(
89+
FeatureToggles.__redis_host, FeatureToggles.__redis_port, FeatureToggles.__redis_db,
90+
FeatureToggles.__redis_auth_enabled, FeatureToggles.__redis_password
91+
)
7092

7193
return FeatureToggles.__cache
7294

@@ -113,7 +135,12 @@ def __get_unleash_client():
113135
environment=FeatureToggles.__environment,
114136
redis_host=FeatureToggles.__redis_host,
115137
redis_port=FeatureToggles.__redis_port,
116-
redis_db=FeatureToggles.__redis_db
138+
redis_db=FeatureToggles.__redis_db,
139+
sentinel_enabled=FeatureToggles.__sentinel_enabled,
140+
sentinels=FeatureToggles.__sentinels,
141+
sentinel_service_name= FeatureToggles.__sentinel_service_name,
142+
redis_auth_enabled=FeatureToggles.__redis_auth_enabled,
143+
redis_password=FeatureToggles.__redis_password
117144
)
118145
FeatureToggles.__client.initialize_client()
119146

@@ -248,6 +275,22 @@ def fetch_feature_toggles():
248275
feature_toggles = pickle.loads(
249276
FeatureToggles.__cache.get(consts.FEATURES_URL)
250277
)
278+
"""
279+
Sample output of feature_toggles
280+
[
281+
{
282+
"name": "devdanish.development.redis_auth",
283+
"strategies": [
284+
{
285+
"name": "EnableForPartners",
286+
"parameters": {
287+
"partner_names": "client1, client2"
288+
}
289+
}
290+
]
291+
}
292+
]
293+
"""
251294
if feature_toggles:
252295
for feature_toggle in feature_toggles:
253296
full_feature_name = feature_toggle['name']
@@ -267,7 +310,6 @@ def fetch_feature_toggles():
267310
# Strip CAS and ENV name from feature name
268311
active_cas_env_name = f'{cas_name}.{environment}.'
269312
full_feature_name = full_feature_name.replace(active_cas_env_name, '')
270-
full_feature_name = full_feature_name.replace(active_cas_env_name, '')
271313
if full_feature_name not in response:
272314
response[full_feature_name] = {}
273315
strategies = feature_toggle.get('strategies', [])

FeatureToggle/redis_utils.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from typing import Optional
2+
3+
import redis
4+
from redis.sentinel import Sentinel
5+
6+
7+
class RedisConnector:
8+
"""
9+
Utility Redis connector class to help with generating Redis sentinel and non-sentinel connection
10+
"""
11+
@staticmethod
12+
def get_sentinel_connection(sentinels: list, sentinel_service_name: str, redis_db: int,
13+
redis_auth_enabled: Optional[bool] = False, redis_password: Optional[str] = None):
14+
"""
15+
Generates the Redis sentinel connection
16+
:param sentinels:
17+
:param sentinel_service_name:
18+
:param redis_auth_enabled:
19+
:param redis_password:
20+
:param redis_db:
21+
:return: Redis<SentinelConnectionPool<service=service-name>
22+
"""
23+
if not all([sentinels, sentinel_service_name]):
24+
raise ValueError(
25+
"[get_sentinel_connection] Mandatory args for Redis Sentinel are missing."
26+
"Required Args: (sentinels, sentinel_service_name)"
27+
)
28+
if redis_auth_enabled and not redis_password:
29+
raise ValueError("[get_sentinel_connection] Redis Auth enabled but Redis Password not provided.")
30+
31+
if redis_auth_enabled and redis_password:
32+
sentinel = Sentinel(sentinels, sentinel_kwargs={"password": redis_password})
33+
sentinel_connection_pool = sentinel.master_for(sentinel_service_name, password=redis_password, db=redis_db)
34+
else:
35+
sentinel = Sentinel(sentinels)
36+
sentinel_connection_pool = sentinel.master_for(sentinel_service_name, db=redis_db)
37+
return sentinel_connection_pool
38+
39+
@staticmethod
40+
def get_non_sentinel_connection(redis_host: str, redis_port: int, redis_db: int,
41+
redis_auth_enabled: Optional[bool] = False,
42+
redis_password: Optional[str] = None):
43+
"""
44+
Generates the Redis non-sentinel connection
45+
:param redis_host:
46+
:param redis_port:
47+
:param redis_db:
48+
:param redis_auth_enabled:
49+
:param redis_password:
50+
:return: Redis<ConnectionPool<Connection<host=,port=,db=>>>
51+
"""
52+
if redis_auth_enabled and not redis_password:
53+
raise ValueError("[get_non_sentinel_connection] Redis Auth enabled but Redis Password not provided.")
54+
55+
if redis_auth_enabled and redis_password:
56+
non_sentinel_connection_pool = redis.Redis(
57+
host=redis_host,
58+
port=redis_port,
59+
db=redis_db,
60+
password=redis_password
61+
)
62+
else:
63+
non_sentinel_connection_pool = redis.Redis(
64+
host=redis_host,
65+
port=redis_port,
66+
db=redis_db
67+
)
68+
return non_sentinel_connection_pool

UnleashClient/__init__.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import redis
2-
3-
from typing import Dict, Callable
1+
from typing import Dict, Callable, Optional
42

53
from UnleashClient.periodic_tasks import fetch_and_load_features
6-
from UnleashClient.strategies import ApplicationHostname, Default, GradualRolloutRandom, \
7-
GradualRolloutSessionId, GradualRolloutUserId, UserWithId, RemoteAddress, FlexibleRollout, \
8-
EnableForDomains, EnableForBusinesses, EnableForPartners, EnableForExperts
4+
from UnleashClient.strategies import (
5+
ApplicationHostname, Default, GradualRolloutRandom, GradualRolloutSessionId, GradualRolloutUserId, UserWithId,
6+
RemoteAddress, FlexibleRollout, EnableForDomains, EnableForBusinesses, EnableForPartners, EnableForExperts
7+
)
98
from UnleashClient import constants as consts
109
from UnleashClient.strategies.EnableForTeamStrategy import EnableForTeams
1110
from UnleashClient.utils import LOGGER
@@ -24,8 +23,8 @@ def __init__(self,
2423
environment: str,
2524
cas_name: str,
2625
redis_host: str,
27-
redis_port: str,
28-
redis_db: str,
26+
redis_port: int,
27+
redis_db: int,
2928
instance_id: str = "unleash-client-python",
3029
refresh_interval: int = 15,
3130
metrics_interval: int = 60,
@@ -34,7 +33,13 @@ def __init__(self,
3433
custom_headers: dict = {},
3534
custom_options: dict = {},
3635
custom_strategies: dict = {},
37-
cache_directory: str = None) -> None:
36+
cache_directory: str = None,
37+
sentinel_enabled: bool = False,
38+
sentinels: Optional[list] = None,
39+
sentinel_service_name: Optional[str] = None,
40+
redis_auth_enabled: bool = False,
41+
redis_password: Optional[str] = None
42+
) -> None:
3843
"""
3944
A client for the Unleash feature toggle system.
4045
:param url: URL of the unleash server, required.
@@ -64,13 +69,14 @@ def __init__(self,
6469
"appName": self.unleash_app_name,
6570
"environment": self.unleash_environment
6671
}
72+
from FeatureToggle.redis_utils import RedisConnector
73+
if sentinel_enabled:
74+
self.cache = RedisConnector.get_sentinel_connection(sentinels, sentinel_service_name, redis_db,
75+
redis_auth_enabled, redis_password)
76+
else:
77+
self.cache = RedisConnector.get_non_sentinel_connection(redis_host, redis_port, redis_db,
78+
redis_auth_enabled, redis_password)
6779

68-
# Class objects
69-
self.cache = redis.Redis(
70-
host=redis_host,
71-
port=redis_port,
72-
db=redis_db
73-
)
7480
self.features = {} # type: Dict
7581

7682
# Mappings

0 commit comments

Comments
 (0)