-
Notifications
You must be signed in to change notification settings - Fork 13
New aws-secret argument resolver & uuid value provider #425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
New aws-secret argument resolver & uuid value provider #425
Conversation
|
||
def get_secret_value(self, secret_id: str) -> str | None: | ||
"""Return the secret value (or None if not found).""" | ||
return self._secret_value or None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is self._secret_value
defined or set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done, initialized them in init function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# Initialize instance attributes to prevent AttributeError
self._secret_value: Optional[str] = None
self.default: Optional[str] = None
"""Return the secret value (or a default if not found).""" | ||
return self._secret_value or self.default # type: ignore[no-any-return] | ||
|
||
def get_secret_value(self, secret_id: str) -> str | None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of the parameter secret_id
? It appears unused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup it was redundant function, removed it
cache_ttl: int = 300 # 5 minutes | ||
max_retries: int = 3 | ||
retry_delay: float = 1.0 | ||
# todo get region from environment variable or config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason that this TODO is unfinished?
Does it make sense to even have a default for region_name
? If we don't want to add the ability to set from an environment variable/config for this initial pass, I think it might make more sense to fail if the region name is not set explicitly by the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zprobst can provide better insights on this from our discussions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbristow nodestream is lacking a way to configure argument resolvers. We can push these things to environment variables but there is no "smart" way to configure these from say... a nodestream.yaml file. I suggested deferring configuration of these until we think through how we want to support that.
# Initialize caches | ||
secret_cache = SecretCache() | ||
json_cache = SecretCache() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these be initialized on module load? I know it's not a huge deal, but it seems a little aggressive to load these caches whenever someone loads any part of the nodestream library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we do this ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can I do this : Initialize the caches only when the AWSSecretResolver is first instantiated or used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbristow Moved the cache initialization to the first time they're actually needed
def retry_on_error(max_retries: int = 3, delay: float = 1.0) -> Callable[[F], F]: | ||
"""Decorator to retry a function on failure. | ||
|
||
Args: | ||
max_retries: Maximum number of retries | ||
delay: Delay between retries in seconds | ||
|
||
Returns: | ||
Decorated function that will retry on failure | ||
|
||
Example: | ||
@retry_on_error(max_retries=3, delay=1.0) | ||
def my_function(): | ||
# Function that may fail | ||
pass | ||
""" | ||
|
||
def decorator(func: F) -> F: | ||
@wraps(func) | ||
def wrapper(*args: Any, **kwargs: Any) -> Any: | ||
last_exception = None | ||
for attempt in range(max_retries): | ||
try: | ||
return func(*args, **kwargs) | ||
except Exception as e: | ||
last_exception = e | ||
if attempt < max_retries - 1: | ||
msg = ( | ||
f"Attempt {attempt + 1} failed for {func.__name__}, " | ||
f"retrying in {delay} seconds... Error: {str(e)}" | ||
) | ||
logger.warning(msg) | ||
time.sleep(delay) | ||
raise last_exception or Exception("Unknown error occurred") | ||
|
||
return cast(F, wrapper) | ||
|
||
return decorator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zprobst Are we still ok with one-off custom retrier logic proliferating through nodestream? Should we think about consolidating around a specific implementation for nodestream or a library like tenacity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think its about time we start to get serious about this issue. I am good with tenacity if you think its the best choice forward @jbristow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I default to tenacity because it's actively maintained and has a simple API for handling most cases, but still exposes granular config points so that you can do much more complicated things.
try: | ||
response = self._client.get_secret_value(SecretId=secret_name) | ||
if "SecretString" in response: | ||
return response["SecretString"] # type: ignore[no-any-return] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be safely & explicitly converted to a string rather than ignoring the mypy error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, type casted it to str
""" | ||
try: | ||
response = self._client.get_secret_value(SecretId=secret_name) | ||
if "SecretString" in response: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All tests still pass after deleting this line. Why is that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you show me how, I tried and i saw some tests failing. tests/unit/pipeline/argument_resolvers/test_aws_secret_resolver.py ..FF... [ 10%]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test are failing on a syntax error. Remove the indentation of the next line, and all tests pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its resolved :)
raise SecretDecodeError( | ||
f"Secret '{secret_name}' is not valid JSON: {e}" | ||
) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add tests ensuring that this raises the expected exception
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
except SecretResolverError as e: | ||
logger.error(f"Error resolving JSON secret '{secret_name}': {e}") | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we catching and not rethrowing here? Using a try-catch block as branch control is almost always a confusing and potentially bug-prone choice over if
statements.
Please eliminate this nested try block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup makes sense, removed nested try & catch, replaced with a simplified flow and if else , returning None
value, expiry = self._cache[key] | ||
if time.time() < expiry: | ||
logger.debug(f"Cache HIT: {key}") | ||
return value | ||
logger.debug(f"Cache EXPIRED: {key}") | ||
del self._cache[key] | ||
logger.debug(f"Cache MISS: {key}") | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All tests seem to avoid this inner section. I think we should have some unit tests that return a cache HIT and EXPIRED and check that the inner cache state has changed as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done , added test cases to check CACHE functions
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #425 +/- ##
==========================================
- Coverage 98.26% 98.02% -0.24%
==========================================
Files 152 154 +2
Lines 6171 6385 +214
==========================================
+ Hits 6064 6259 +195
- Misses 107 126 +19
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Adding a new Resolver to fetch AWS Secrets from Secrets Manager.
Usage :
Adding a New UUID Value Provide ,
Usage:
Supports both simple string input and structured configuration: