Skip to content

Commit

Permalink
Merge pull request #50 from hellohaptik/develop
Browse files Browse the repository at this point in the history
Redis Auth sentinel support
  • Loading branch information
danish-wani-haptik authored May 10, 2024
2 parents 5090649 + 7e76fa6 commit 818190e
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 26 deletions.
64 changes: 53 additions & 11 deletions FeatureToggle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Python Imports
import redis

from redis.exceptions import LockError, BusyLoadingError, ConnectionError, RedisError
import pickle
from typing import Dict, Any, Optional
Expand All @@ -9,6 +9,7 @@
from UnleashClient import UnleashClient
from UnleashClient.utils import LOGGER
from FeatureToggle.utils import timed_lru_cache
from FeatureToggle.redis_utils import RedisConnector


def split_and_strip(parameters: str):
Expand All @@ -29,6 +30,11 @@ class FeatureToggles:
__environment = None
__cache = None
__enable_toggle_service = True
__sentinel_enabled = False
__sentinels = None
__sentinel_service_name = None
__redis_auth_enabled = False
__redis_password = None

@staticmethod
def initialize(url: str,
Expand All @@ -37,9 +43,15 @@ def initialize(url: str,
cas_name: str,
environment: str,
redis_host: str,
redis_port: str,
redis_db: str,
enable_toggle_service: bool = True) -> None:
redis_port: int,
redis_db: int,
enable_toggle_service: bool = True,
sentinel_enabled: bool = False,
sentinels: Optional[list] = None,
sentinel_service_name: Optional[str] = None,
redis_auth_enabled: bool = False,
redis_password: Optional[str] = None
) -> None:
""" Static access method. """
if FeatureToggles.__client is None:
FeatureToggles.__url = url
Expand All @@ -51,6 +63,11 @@ def initialize(url: str,
FeatureToggles.__redis_port = redis_port
FeatureToggles.__redis_db = redis_db
FeatureToggles.__enable_toggle_service = enable_toggle_service
FeatureToggles.__sentinel_enabled = sentinel_enabled
FeatureToggles.__sentinels = sentinels
FeatureToggles.__sentinel_service_name = sentinel_service_name
FeatureToggles.__redis_auth_enabled = redis_auth_enabled
FeatureToggles.__redis_password = redis_password
FeatureToggles.__cache = FeatureToggles.__get_cache()
LOGGER.info(f'Initializing Feature toggles')
else:
Expand All @@ -62,11 +79,16 @@ def __get_cache():
Create redis connection
"""
if FeatureToggles.__cache is None:
FeatureToggles.__cache = redis.Redis(
host=FeatureToggles.__redis_host,
port=FeatureToggles.__redis_port,
db=FeatureToggles.__redis_db
)
if FeatureToggles.__sentinel_enabled:
FeatureToggles.__cache = RedisConnector.get_sentinel_connection(
FeatureToggles.__sentinels, FeatureToggles.__sentinel_service_name, FeatureToggles.__redis_db,
FeatureToggles.__redis_auth_enabled, FeatureToggles.__redis_password
)
else:
FeatureToggles.__cache = RedisConnector.get_non_sentinel_connection(
FeatureToggles.__redis_host, FeatureToggles.__redis_port, FeatureToggles.__redis_db,
FeatureToggles.__redis_auth_enabled, FeatureToggles.__redis_password
)

return FeatureToggles.__cache

Expand Down Expand Up @@ -113,7 +135,12 @@ def __get_unleash_client():
environment=FeatureToggles.__environment,
redis_host=FeatureToggles.__redis_host,
redis_port=FeatureToggles.__redis_port,
redis_db=FeatureToggles.__redis_db
redis_db=FeatureToggles.__redis_db,
sentinel_enabled=FeatureToggles.__sentinel_enabled,
sentinels=FeatureToggles.__sentinels,
sentinel_service_name= FeatureToggles.__sentinel_service_name,
redis_auth_enabled=FeatureToggles.__redis_auth_enabled,
redis_password=FeatureToggles.__redis_password
)
FeatureToggles.__client.initialize_client()

Expand Down Expand Up @@ -248,6 +275,22 @@ def fetch_feature_toggles():
feature_toggles = pickle.loads(
FeatureToggles.__cache.get(consts.FEATURES_URL)
)
"""
Sample output of feature_toggles
[
{
"name": "devdanish.development.redis_auth",
"strategies": [
{
"name": "EnableForPartners",
"parameters": {
"partner_names": "client1, client2"
}
}
]
}
]
"""
if feature_toggles:
for feature_toggle in feature_toggles:
full_feature_name = feature_toggle['name']
Expand All @@ -267,7 +310,6 @@ def fetch_feature_toggles():
# Strip CAS and ENV name from feature name
active_cas_env_name = f'{cas_name}.{environment}.'
full_feature_name = full_feature_name.replace(active_cas_env_name, '')
full_feature_name = full_feature_name.replace(active_cas_env_name, '')
if full_feature_name not in response:
response[full_feature_name] = {}
strategies = feature_toggle.get('strategies', [])
Expand Down
68 changes: 68 additions & 0 deletions FeatureToggle/redis_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from typing import Optional

import redis
from redis.sentinel import Sentinel


class RedisConnector:
"""
Utility Redis connector class to help with generating Redis sentinel and non-sentinel connection
"""
@staticmethod
def get_sentinel_connection(sentinels: list, sentinel_service_name: str, redis_db: int,
redis_auth_enabled: Optional[bool] = False, redis_password: Optional[str] = None):
"""
Generates the Redis sentinel connection
:param sentinels:
:param sentinel_service_name:
:param redis_auth_enabled:
:param redis_password:
:param redis_db:
:return: Redis<SentinelConnectionPool<service=service-name>
"""
if not all([sentinels, sentinel_service_name]):
raise ValueError(
"[get_sentinel_connection] Mandatory args for Redis Sentinel are missing."
"Required Args: (sentinels, sentinel_service_name)"
)
if redis_auth_enabled and not redis_password:
raise ValueError("[get_sentinel_connection] Redis Auth enabled but Redis Password not provided.")

if redis_auth_enabled and redis_password:
sentinel = Sentinel(sentinels, sentinel_kwargs={"password": redis_password})
sentinel_connection_pool = sentinel.master_for(sentinel_service_name, password=redis_password, db=redis_db)
else:
sentinel = Sentinel(sentinels)
sentinel_connection_pool = sentinel.master_for(sentinel_service_name, db=redis_db)
return sentinel_connection_pool

@staticmethod
def get_non_sentinel_connection(redis_host: str, redis_port: int, redis_db: int,
redis_auth_enabled: Optional[bool] = False,
redis_password: Optional[str] = None):
"""
Generates the Redis non-sentinel connection
:param redis_host:
:param redis_port:
:param redis_db:
:param redis_auth_enabled:
:param redis_password:
:return: Redis<ConnectionPool<Connection<host=,port=,db=>>>
"""
if redis_auth_enabled and not redis_password:
raise ValueError("[get_non_sentinel_connection] Redis Auth enabled but Redis Password not provided.")

if redis_auth_enabled and redis_password:
non_sentinel_connection_pool = redis.Redis(
host=redis_host,
port=redis_port,
db=redis_db,
password=redis_password
)
else:
non_sentinel_connection_pool = redis.Redis(
host=redis_host,
port=redis_port,
db=redis_db
)
return non_sentinel_connection_pool
36 changes: 21 additions & 15 deletions UnleashClient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import redis

from typing import Dict, Callable
from typing import Dict, Callable, Optional

from UnleashClient.periodic_tasks import fetch_and_load_features
from UnleashClient.strategies import ApplicationHostname, Default, GradualRolloutRandom, \
GradualRolloutSessionId, GradualRolloutUserId, UserWithId, RemoteAddress, FlexibleRollout, \
EnableForDomains, EnableForBusinesses, EnableForPartners, EnableForExperts
from UnleashClient.strategies import (
ApplicationHostname, Default, GradualRolloutRandom, GradualRolloutSessionId, GradualRolloutUserId, UserWithId,
RemoteAddress, FlexibleRollout, EnableForDomains, EnableForBusinesses, EnableForPartners, EnableForExperts
)
from UnleashClient import constants as consts
from UnleashClient.strategies.EnableForTeamStrategy import EnableForTeams
from UnleashClient.utils import LOGGER
Expand All @@ -24,8 +23,8 @@ def __init__(self,
environment: str,
cas_name: str,
redis_host: str,
redis_port: str,
redis_db: str,
redis_port: int,
redis_db: int,
instance_id: str = "unleash-client-python",
refresh_interval: int = 15,
metrics_interval: int = 60,
Expand All @@ -34,7 +33,13 @@ def __init__(self,
custom_headers: dict = {},
custom_options: dict = {},
custom_strategies: dict = {},
cache_directory: str = None) -> None:
cache_directory: str = None,
sentinel_enabled: bool = False,
sentinels: Optional[list] = None,
sentinel_service_name: Optional[str] = None,
redis_auth_enabled: bool = False,
redis_password: Optional[str] = None
) -> None:
"""
A client for the Unleash feature toggle system.
:param url: URL of the unleash server, required.
Expand Down Expand Up @@ -64,13 +69,14 @@ def __init__(self,
"appName": self.unleash_app_name,
"environment": self.unleash_environment
}
from FeatureToggle.redis_utils import RedisConnector
if sentinel_enabled:
self.cache = RedisConnector.get_sentinel_connection(sentinels, sentinel_service_name, redis_db,
redis_auth_enabled, redis_password)
else:
self.cache = RedisConnector.get_non_sentinel_connection(redis_host, redis_port, redis_db,
redis_auth_enabled, redis_password)

# Class objects
self.cache = redis.Redis(
host=redis_host,
port=redis_port,
db=redis_db
)
self.features = {} # type: Dict

# Mappings
Expand Down

0 comments on commit 818190e

Please sign in to comment.