-
Notifications
You must be signed in to change notification settings - Fork 1.8k
out_azure_logs_ingestion: implement Managed Identity support #10867
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: master
Are you sure you want to change the base?
out_azure_logs_ingestion: implement Managed Identity support #10867
Conversation
WalkthroughAdds Managed Identity (MSI) authentication to the out_azure_logs_ingestion plugin, introduces an Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant CFG as Config
participant PLG as Azure Logs Ingestion Plugin
participant AUTH as Auth Layer
participant IMDS as Azure IMDS (MSI)
participant AAD as Azure AD (OAuth2)
CFG->>PLG: provide auth_type + credentials
PLG->>AUTH: request access token
alt managed_identity (MSI)
AUTH->>IMDS: GET /metadata/identity/oauth2/token\nHeader: Metadata: true
IMDS-->>AUTH: 200 {access_token, expires_in}
AUTH->>AUTH: parse JSON, cache token, set expiry
else service_principal (OAuth2)
AUTH->>AAD: POST /{tenant}/oauth2/v2.0/token\nclient_id/client_secret
AAD-->>AUTH: 200 {access_token, expires_in}
AUTH->>AUTH: parse response, cache token, set expiry
end
AUTH-->>PLG: return access_token
note right of AUTH: On expiry, refresh via the selected path
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Example output config with system managed identity
Example output config with user managed identity
Example output config with default service principal (should still work as before)
|
Debug logs from test:
|
Documentation PR fluent/fluent-bit-docs#2062 |
403c41f
to
62f72e4
Compare
This change is based on the existing approach to Managed Identity authentication used for the out_azure_kusto plugin. Signed-off-by: Stefano Boriero <stefano.boriero@seqera.io>
62f72e4
to
e9418b1
Compare
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.
Actionable comments posted: 0
🧹 Nitpick comments (5)
plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c (3)
58-82
: Managed Identity UX: default to system-assigned if client_id is absent; don’t hard-require itRequiring client_id for MSI complicates system-assigned usage. Defaulting to system-assigned when client_id is unset matches common patterns and reduces config friction.
Apply:
-else if (strcasecmp(ctx->auth_type_str, "managed_identity") == 0) { - /* Check if client_id indicates system-assigned or user-assigned managed identity */ - if (!ctx->client_id) { - flb_plg_error(ins, "When using managed_identity auth, client_id must be set to 'system' for system-assigned or the managed identity client ID"); - flb_az_li_ctx_destroy(ctx); - return NULL; - } - - if (strcasecmp(ctx->client_id, "system") == 0) { - ctx->auth_type = FLB_AZ_LI_AUTH_MANAGED_IDENTITY_SYSTEM; - } else { - ctx->auth_type = FLB_AZ_LI_AUTH_MANAGED_IDENTITY_USER; - } -} +else if (strcasecmp(ctx->auth_type_str, "managed_identity") == 0) { + /* Default to system-assigned if client_id is unset or equals "system" */ + if (!ctx->client_id || strcasecmp(ctx->client_id, "system") == 0) { + ctx->auth_type = FLB_AZ_LI_AUTH_MANAGED_IDENTITY_SYSTEM; + } + else { + ctx->auth_type = FLB_AZ_LI_AUTH_MANAGED_IDENTITY_USER; + } +}
84-86
: Error message lists unsupported optionMessage mentions 'workload_identity' but there’s no handling for it here. Remove to avoid confusion.
- flb_plg_error(ins, "Invalid auth_type '%s'. Valid options are: 'service_principal', 'managed_identity', or 'workload_identity'", + flb_plg_error(ins, "Invalid auth_type '%s'. Valid options are: 'service_principal' or 'managed_identity'", ctx->auth_type_str);
188-213
: Destroy the mutex in ctx_destroyYou init token_mutex but never destroy it. Add pthread_mutex_destroy to prevent leaks.
if (ctx->u_dce) { flb_upstream_destroy(ctx->u_dce); } - flb_free(ctx); + pthread_mutex_destroy(&ctx->token_mutex); + flb_free(ctx);plugins/out_azure_logs_ingestion/azure_logs_ingestion.c (2)
190-229
: Service principal payload: minor hardeningIf any of the required SP fields are missing, payload_append will proceed but token request will fail later. Consider early validation in this path to emit a precise error.
+ if (!ctx->client_id || !ctx->client_secret) { + flb_plg_error(ctx->ins, "service_principal auth requires client_id and client_secret"); + goto token_cleanup; + }
412-417
: Config doc: clarify client_id dual-useDoc string is good; recommend adding that client_id is reused for MSI (either 'system' or the user-assigned MI client ID) to avoid confusion with the earlier client_id description.
- "Set the authentication type: 'service_principal' or 'managed_identity'. " - "For managed_identity, use 'system' as client_id for system-assigned identity, or specify the managed identity's client ID" + "Set authentication: 'service_principal' or 'managed_identity'. " + "For managed_identity, set client_id to 'system' (system-assigned) or to the user-assigned MI client ID."
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
plugins/out_azure_logs_ingestion/CMakeLists.txt
(1 hunks)plugins/out_azure_logs_ingestion/azure_logs_ingestion.c
(3 hunks)plugins/out_azure_logs_ingestion/azure_logs_ingestion.h
(2 hunks)plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c
(3 hunks)plugins/out_azure_logs_ingestion/azure_logs_ingestion_msiauth.c
(1 hunks)plugins/out_azure_logs_ingestion/azure_logs_ingestion_msiauth.h
(1 hunks)tests/runtime/CMakeLists.txt
(1 hunks)tests/runtime/out_azure_logs_ingestion.c
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
- plugins/out_azure_logs_ingestion/CMakeLists.txt
- plugins/out_azure_logs_ingestion/azure_logs_ingestion_msiauth.h
- tests/runtime/out_azure_logs_ingestion.c
- plugins/out_azure_logs_ingestion/azure_logs_ingestion_msiauth.c
- plugins/out_azure_logs_ingestion/azure_logs_ingestion.h
- tests/runtime/CMakeLists.txt
🧰 Additional context used
🧬 Code graph analysis (2)
plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c (1)
src/flb_sds.c (2)
flb_sds_create_size
(92-95)flb_sds_snprintf
(405-428)
plugins/out_azure_logs_ingestion/azure_logs_ingestion.c (2)
src/flb_oauth2.c (4)
flb_oauth2_token_expired
(542-556)flb_oauth2_payload_clear
(258-271)flb_oauth2_payload_append
(274-316)flb_oauth2_token_get
(448-531)plugins/out_azure_logs_ingestion/azure_logs_ingestion_msiauth.c (1)
flb_azure_li_msiauth_token_get
(30-99)
🔇 Additional comments (4)
plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c (2)
28-28
: Include looks correctHeader inclusion aligns with new MSI helpers.
108-132
: Verify SDS allocation includes space for NULAfter inspecting flb_sds_create_size and flb_sds_snprintf:
- flb_sds_create_size(len) calls sds_alloc(len), and SDS’s sds_alloc always allocates
len + 1
bytes internally to store the NUL terminator.- flb_sds_snprintf uses the provided
size
argument (returned by flb_sds_alloc) as the maximum including the NUL.Thus,
sizeof(FLB_AZ_LI_MSIAUTH_URL_TEMPLATE) - 1
correctly covers the template characters, and SDS already accounts for the extra byte for'\0'
. No off-by-one here.Resolve as-is.
plugins/out_azure_logs_ingestion/azure_logs_ingestion.c (2)
34-34
: Include looks correctMatches new MSI helper usage.
175-188
: Token refresh path: solid MSI branchClear separation between MSI and SP flows; logs are appropriate and mutex-protected.
Signed-off-by: Stefano Boriero <stefano.boriero@seqera.io>
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.
Actionable comments posted: 4
🧹 Nitpick comments (1)
plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c (1)
69-82
: Make system-assigned MSI the default when client_id is omittedThis reduces config friction and matches IMDS behavior; “client_id” is only required for user-assigned MI.
- else if (strcasecmp(ctx->auth_type_str, "managed_identity") == 0) { - /* Check if client_id indicates system-assigned or user-assigned managed identity */ - if (!ctx->client_id) { - flb_plg_error(ins, "When using managed_identity auth, client_id must be set to 'system' for system-assigned or the managed identity client ID"); - flb_az_li_ctx_destroy(ctx); - return NULL; - } - - if (strcasecmp(ctx->client_id, "system") == 0) { - ctx->auth_type = FLB_AZ_LI_AUTH_MANAGED_IDENTITY_SYSTEM; - } else { - ctx->auth_type = FLB_AZ_LI_AUTH_MANAGED_IDENTITY_USER; - } - } + else if (strcasecmp(ctx->auth_type_str, "managed_identity") == 0) { + /* system-assigned by default; user-assigned when client_id is provided */ + if (!ctx->client_id || strcasecmp(ctx->client_id, "system") == 0) { + ctx->auth_type = FLB_AZ_LI_AUTH_MANAGED_IDENTITY_SYSTEM; + } + else { + ctx->auth_type = FLB_AZ_LI_AUTH_MANAGED_IDENTITY_USER; + } + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c
(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c (1)
src/flb_sds.c (2)
flb_sds_create_size
(92-95)flb_sds_snprintf
(405-428)
🔇 Additional comments (1)
plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c (1)
28-28
: Include looks correct for MSI supportBrings in the MSI URL template used below; no issues.
/* Auth method validation and setup */ | ||
if (strcasecmp(ctx->auth_type_str, "service_principal") == 0) { | ||
ctx->auth_type = FLB_AZ_LI_AUTH_SERVICE_PRINCIPAL; | ||
|
||
/* Verify required parameters for Service Principal auth */ | ||
if (!ctx->tenant_id || !ctx->client_id || !ctx->client_secret) { | ||
flb_plg_error(ins, "When using service_principal auth, tenant_id, client_id, and client_secret are required"); | ||
flb_az_li_ctx_destroy(ctx); | ||
return NULL; | ||
} | ||
} |
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.
🛠️ Refactor suggestion
Defensive default for auth_type_str and avoid stale ins->context on early returns
- Add a NULL/empty guard so strcasecmp never derefs NULL if the config map fails to populate the default.
- Today ins->context is set before validation; on any “return NULL” below, the instance may keep a dangling pointer. Set the context only after successful init (see snippet).
Apply this minimal guard:
- /* Auth method validation and setup */
- if (strcasecmp(ctx->auth_type_str, "service_principal") == 0) {
+ /* Auth method validation and setup */
+ if (!ctx->auth_type_str || ctx->auth_type_str[0] == '\0') {
+ ctx->auth_type_str = "service_principal";
+ }
+ if (strcasecmp(ctx->auth_type_str, "service_principal") == 0) {
Outside this hunk, move the context assignment to the end (right before returning ctx) instead of early:
/* remove this early assignment */
// flb_output_set_context(ins, ctx);
/* ... after successful setup (e.g., after flb_output_upstream_set) */
flb_output_set_context(ins, ctx);
return ctx;
🤖 Prompt for AI Agents
In plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c around lines 58
to 68, add a NULL/empty guard before calling strcasecmp on ctx->auth_type_str
(e.g., treat NULL or empty as a default string or skip comparison) so strcasecmp
never dereferences NULL, and remove the early flb_output_set_context(ins, ctx)
assignment so ins->context is not set on partial failures; instead move
flb_output_set_context(ins, ctx) to the very end of the initialization sequence
(after all validation and flb_output_upstream_set succeed) and only then return
ctx.
/* Allocate and set auth url based on authentication method */ | ||
if (ctx->auth_type == FLB_AZ_LI_AUTH_MANAGED_IDENTITY_SYSTEM) { | ||
/* System-assigned managed identity */ | ||
ctx->auth_url = flb_sds_create_size(sizeof(FLB_AZ_LI_MSIAUTH_URL_TEMPLATE) - 1); | ||
if (!ctx->auth_url) { | ||
flb_errno(); | ||
flb_az_li_ctx_destroy(ctx); | ||
return NULL; | ||
} | ||
flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url), | ||
FLB_AZ_LI_MSIAUTH_URL_TEMPLATE, "", ""); | ||
} |
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.
🛠️ Refactor suggestion
Check flb_sds_snprintf return; bail out on failure
Pre-sizing is fine, but flb_sds_snprintf can fail (returns -1). Handle it to avoid using an uninitialized URL.
- flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url),
- FLB_AZ_LI_MSIAUTH_URL_TEMPLATE, "", "");
+ ret = flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url),
+ FLB_AZ_LI_MSIAUTH_URL_TEMPLATE, "", "");
+ if (ret < 0) {
+ flb_plg_error(ins, "failed composing MSI auth_url (system-assigned)");
+ flb_az_li_ctx_destroy(ctx);
+ return NULL;
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
/* Allocate and set auth url based on authentication method */ | |
if (ctx->auth_type == FLB_AZ_LI_AUTH_MANAGED_IDENTITY_SYSTEM) { | |
/* System-assigned managed identity */ | |
ctx->auth_url = flb_sds_create_size(sizeof(FLB_AZ_LI_MSIAUTH_URL_TEMPLATE) - 1); | |
if (!ctx->auth_url) { | |
flb_errno(); | |
flb_az_li_ctx_destroy(ctx); | |
return NULL; | |
} | |
flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url), | |
FLB_AZ_LI_MSIAUTH_URL_TEMPLATE, "", ""); | |
} | |
/* Allocate and set auth url based on authentication method */ | |
if (ctx->auth_type == FLB_AZ_LI_AUTH_MANAGED_IDENTITY_SYSTEM) { | |
/* System-assigned managed identity */ | |
ctx->auth_url = flb_sds_create_size(sizeof(FLB_AZ_LI_MSIAUTH_URL_TEMPLATE) - 1); | |
if (!ctx->auth_url) { | |
flb_errno(); | |
flb_az_li_ctx_destroy(ctx); | |
return NULL; | |
} | |
- flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url), | |
ret = flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url), | |
FLB_AZ_LI_MSIAUTH_URL_TEMPLATE, "", ""); | |
if (ret < 0) { | |
flb_plg_error(ins, "failed composing MSI auth_url (system-assigned)"); | |
flb_az_li_ctx_destroy(ctx); | |
return NULL; | |
} | |
} |
🤖 Prompt for AI Agents
In plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c around lines 108
to 119, the call to flb_sds_snprintf when building ctx->auth_url can fail
(returns -1) and the code currently ignores that; update the code to check the
return value of flb_sds_snprintf, and if it returns a negative value free the
allocated SDS (or call flb_az_li_ctx_destroy(ctx) if that frees it), log the
error (e.g., flb_errno() or a process logger), and return NULL to avoid using an
uninitialized auth_url.
else if (ctx->auth_type == FLB_AZ_LI_AUTH_MANAGED_IDENTITY_USER) { | ||
/* User-assigned managed identity */ | ||
ctx->auth_url = flb_sds_create_size(sizeof(FLB_AZ_LI_MSIAUTH_URL_TEMPLATE) - 1 + | ||
sizeof("&client_id=") - 1 + | ||
flb_sds_len(ctx->client_id)); | ||
if (!ctx->auth_url) { | ||
flb_errno(); | ||
flb_az_li_ctx_destroy(ctx); | ||
return NULL; | ||
} | ||
flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url), | ||
FLB_AZ_LI_MSIAUTH_URL_TEMPLATE, "&client_id=", ctx->client_id); | ||
} |
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.
💡 Verification agent
🧩 Analysis chain
Validate/encode client_id for user-assigned MSI and check snprintf return
- If client_id can ever be non-GUID (e.g., resource ID or contains special chars), it must be URL-encoded before embedding in the query string.
- Also check flb_sds_snprintf return.
- flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url),
- FLB_AZ_LI_MSIAUTH_URL_TEMPLATE, "&client_id=", ctx->client_id);
+ ret = flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url),
+ FLB_AZ_LI_MSIAUTH_URL_TEMPLATE, "&client_id=", ctx->client_id);
+ if (ret < 0) {
+ flb_plg_error(ins, "failed composing MSI auth_url (user-assigned)");
+ flb_az_li_ctx_destroy(ctx);
+ return NULL;
+ }
If client_id is not guaranteed to be a bare GUID, confirm and we can add a URL-encode step before formatting it into the URL.
🏁 Script executed:
#!/bin/bash
# Search for MSI auth URL template definition
echo "== FLB_AZ_LI_MSIAUTH_URL_TEMPLATE definition =="
rg -n "FLB_AZ_LI_MSIAUTH_URL_TEMPLATE" -C3 plugins/out_azure_logs_ingestion
echo ""
# Locate client_id declaration, assignment, and config mapping
echo "== client_id occurrences in azure_logs_ingestion_conf.c =="
rg -n "client_id" -C5 plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c
echo ""
# Inspect flb_sds_snprintf signature to confirm its return semantics
echo "== flb_sds_snprintf signature and context =="
rg -n "flb_sds_snprintf" -C3 .
Length of output: 36494
Check snprintf return and validate/URL-encode client_id
- In azure_logs_ingestion_conf.c (around lines 130–132), capture the return value of
flb_sds_snprintf(…)
into aret
variable; ifret < 0
, log an error, destroy the context and return NULL. client_id
must be a bare GUID (safe in URLs). If you plan to accept arbitrary resource IDs or other characters, apply URL-encoding or enforce GUID-only input before embedding.
else { | ||
/* Service principal authentication */ | ||
ctx->auth_url = flb_sds_create_size(sizeof(FLB_AZ_LI_AUTH_URL_TMPLT) - 1 + | ||
flb_sds_len(ctx->tenant_id)); | ||
if (!ctx->auth_url) { | ||
flb_errno(); | ||
flb_az_li_ctx_destroy(ctx); | ||
return NULL; | ||
} | ||
flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url), | ||
FLB_AZ_LI_AUTH_URL_TMPLT, ctx->tenant_id); | ||
} |
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.
🛠️ Refactor suggestion
Also check snprintf return for service principal auth_url
Same robustness as MSI paths.
- flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url),
- FLB_AZ_LI_AUTH_URL_TMPLT, ctx->tenant_id);
+ ret = flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url),
+ FLB_AZ_LI_AUTH_URL_TMPLT, ctx->tenant_id);
+ if (ret < 0) {
+ flb_plg_error(ins, "failed composing SP auth_url");
+ flb_az_li_ctx_destroy(ctx);
+ return NULL;
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
else { | |
/* Service principal authentication */ | |
ctx->auth_url = flb_sds_create_size(sizeof(FLB_AZ_LI_AUTH_URL_TMPLT) - 1 + | |
flb_sds_len(ctx->tenant_id)); | |
if (!ctx->auth_url) { | |
flb_errno(); | |
flb_az_li_ctx_destroy(ctx); | |
return NULL; | |
} | |
flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url), | |
FLB_AZ_LI_AUTH_URL_TMPLT, ctx->tenant_id); | |
} | |
else { | |
/* Service principal authentication */ | |
ctx->auth_url = flb_sds_create_size(sizeof(FLB_AZ_LI_AUTH_URL_TMPLT) - 1 + | |
flb_sds_len(ctx->tenant_id)); | |
if (!ctx->auth_url) { | |
flb_errno(); | |
flb_az_li_ctx_destroy(ctx); | |
return NULL; | |
} | |
- flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url), | |
ret = flb_sds_snprintf(&ctx->auth_url, flb_sds_alloc(ctx->auth_url), | |
FLB_AZ_LI_AUTH_URL_TMPLT, ctx->tenant_id); | |
if (ret < 0) { | |
flb_plg_error(ins, "failed composing SP auth_url"); | |
flb_az_li_ctx_destroy(ctx); | |
return NULL; | |
} | |
} |
🤖 Prompt for AI Agents
In plugins/out_azure_logs_ingestion/azure_logs_ingestion_conf.c around lines 133
to 144, the flb_sds_snprintf call that builds ctx->auth_url for
service-principal auth is not checked for failure/overflow; mirror the MSI-path
robustness by capturing the flb_sds_snprintf return value, verify it succeeded
(check for negative return or size >= flb_sds_alloc(ctx->auth_url) as
appropriate), and on failure call flb_errno(), clean up (destroy/free ctx and
ctx->auth_url), and return NULL so you don't proceed with a malformed auth_url.
This change is based on the existing approach to Managed Identity authentication used for the out_azure_kusto plugin.
Addresses #10777
Enter
[N/A]
in the box, if an item is not applicable to your change.Testing
Before we can approve your change; please submit the following in a comment:
If this is a change to packaging of containers or native binaries then please confirm it works for all targets.
ok-package-test
label to test for all targets (requires maintainer to do).Documentation
Backporting
Fluent Bit is licensed under Apache 2.0, by submitting this pull request I understand that this code will be released under the terms of that license.
Summary by CodeRabbit
New Features
Tests
Chores