Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 6 additions & 22 deletions bottlecap/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions bottlecap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ libdd-trace-normalization = { git = "https://github.com/DataDog/libdatadog", rev
libdd-trace-obfuscation = { git = "https://github.com/DataDog/libdatadog", rev = "48da0d82cb32b43d4cdece35b794c9bcbc275a03", default-features = false }
libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog", rev = "48da0d82cb32b43d4cdece35b794c9bcbc275a03", default-features = false }
datadog-opentelemetry = { git = "https://github.com/DataDog/dd-trace-rs", rev = "f51cefc4ad24bec81b38fb2f36b1ed93f21ae913", default-features = false }
dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "bb4dedeee20b949db3143c05e5a779b843a8a484", default-features = false }
datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "bb4dedeee20b949db3143c05e5a779b843a8a484", default-features = false }
datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "bb4dedeee20b949db3143c05e5a779b843a8a484", default-features = false }
dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "8ce37eb029410b7cf30847376772e3af6baa5f5c", default-features = false }
datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "8ce37eb029410b7cf30847376772e3af6baa5f5c", default-features = false }
datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "8ce37eb029410b7cf30847376772e3af6baa5f5c", default-features = false }
libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] }

[dev-dependencies]
Expand Down
81 changes: 54 additions & 27 deletions bottlecap/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use datadog_agent_config::{
deserialize_optional_duration_from_microseconds as deser_dur_micros,
deserialize_optional_duration_from_seconds as deser_dur_secs,
deserialize_optional_duration_from_seconds_ignore_zero as deser_dur_secs_ignore_zero,
deserialize_optional_string as deser_opt_str, deserialize_string_or_int as deser_str_or_int,
deserialize_optional_string as deser_opt_str,
flush_strategy::FlushStrategy as UpstreamFlushStrategy,
};

Expand All @@ -68,7 +68,6 @@ pub struct LambdaConfig {
pub lambda_extension_compute_stats: bool,
pub span_dedup_timeout: Option<Duration>,
pub api_key_secret_reload_interval: Option<Duration>,
pub dd_org_uuid: String,
pub serverless_appsec_enabled: bool,
pub appsec_rules: Option<String>,
pub appsec_waf_timeout: Duration,
Expand Down Expand Up @@ -98,7 +97,6 @@ impl Default for LambdaConfig {
lambda_extension_compute_stats: false,
span_dedup_timeout: None,
api_key_secret_reload_interval: None,
dd_org_uuid: String::new(),
serverless_appsec_enabled: false,
appsec_rules: None,
appsec_waf_timeout: Duration::from_millis(5),
Expand Down Expand Up @@ -126,11 +124,17 @@ pub struct LambdaConfigSource {
#[serde(deserialize_with = "deser_opt_str")]
pub api_key_ssm_arn: Option<String>,

/// `DD_SERVERLESS_LOGS_ENABLED` — primary toggle for Lambda log shipping.
/// `DD_SERVERLESS_LOGS_ENABLED` — Lambda-specific log toggle, kept for
/// backwards compatibility. Defaults to `true` (the Lambda extension's
/// historical behavior).
#[serde(deserialize_with = "deser_opt_bool")]
pub serverless_logs_enabled: Option<bool>,
/// `DD_LOGS_ENABLED` — alias for `serverless_logs_enabled`; OR-merged so
/// either being `true` turns logs on. See `merge_from` below.
/// `DD_LOGS_ENABLED` — deserialized here a second time (the canonical
/// `Config::logs_enabled` upstream field is also populated by the upstream
/// env/yaml parsing) because lambda's default for logs is `true` while
/// upstream's is `false`. Keeping the alias as `Option<bool>` lets
/// `merge_from` detect "was it explicitly set?" and OR-merge it into
/// `serverless_logs_enabled` — that is the field lambda call sites read.
#[serde(deserialize_with = "deser_opt_bool")]
pub logs_enabled: Option<bool>,

Expand All @@ -154,12 +158,6 @@ pub struct LambdaConfigSource {
#[serde(deserialize_with = "deser_dur_secs_ignore_zero")]
pub api_key_secret_reload_interval: Option<Duration>,

/// `DD_ORG_UUID` — when set, delegated auth is auto-enabled. The source
/// field is `org_uuid` (matching the env var) and merges into the
/// `dd_org_uuid` config field.
#[serde(deserialize_with = "deser_str_or_int")]
pub org_uuid: Option<String>,

#[serde(deserialize_with = "deser_opt_bool")]
pub serverless_appsec_enabled: Option<bool>,
#[serde(deserialize_with = "deser_opt_str")]
Expand Down Expand Up @@ -210,17 +208,20 @@ impl DatadogConfigExtension for LambdaConfig {
option: [span_dedup_timeout, api_key_secret_reload_interval, appsec_rules],
);

// OR-merge serverless_logs_enabled with the logs_enabled alias. Either
// env var set to `true` enables logs; if both are absent the default
// (true) is preserved.
// Preserve legacy OR-merge semantics: when either env var is
// explicitly set, the resolved value is the OR of the two (unset
// counts as false for the OR). When neither is set, the default
// (true) is preserved. This invariant — in particular that setting
// only DD_LOGS_ENABLED=false disables logs — predates upstream
// owning `logs_enabled` and must be kept. The duplicate parse of
// DD_LOGS_ENABLED (once upstream, once here via the alias) is
// intentional: upstream populates `config.logs_enabled` for any
// non-lambda consumer, while this branch keeps the lambda contract.
if source.serverless_logs_enabled.is_some() || source.logs_enabled.is_some() {
self.serverless_logs_enabled = source.serverless_logs_enabled.unwrap_or(false)
|| source.logs_enabled.unwrap_or(false);
}

// org_uuid (source) → dd_org_uuid (config)
datadog_agent_config::merge_string!(self, dd_org_uuid, source, org_uuid);

// lambda_customer_metrics_exclude_tags (source) → custom_metrics_exclude_tags (config)
if !source.lambda_customer_metrics_exclude_tags.is_empty() {
self.custom_metrics_exclude_tags
Expand Down Expand Up @@ -316,6 +317,14 @@ mod lambda_config_tests {
}

// ---- serverless_logs_enabled with OR-merge alias ----
//
// The legacy contract: DD_SERVERLESS_LOGS_ENABLED and DD_LOGS_ENABLED are
// OR-merged into config.ext.serverless_logs_enabled. The default (true)
// is preserved iff neither env var was explicitly set; otherwise the
// resolved value is the OR of the two (unset counts as false). The
// upstream `Config::logs_enabled` field is also populated independently
// (sourced by the upstream env/yaml parsing), but lambda call sites that
// gate on log shipping continue to use serverless_logs_enabled.

#[test]
fn serverless_logs_enabled_defaults_true() {
Expand Down Expand Up @@ -343,14 +352,26 @@ mod lambda_config_tests {
}

#[test]
fn logs_enabled_alias_only() {
fn logs_enabled_alias_only_true() {
let config = load(|jail| {
jail.set_env("DD_LOGS_ENABLED", "true");
Ok(())
});
assert!(config.ext.serverless_logs_enabled);
}

#[test]
fn logs_enabled_alias_only_false_overrides_default() {
// Setting only DD_LOGS_ENABLED=false must disable logs, overriding
// the default-true. This is the legacy behavior that the alias-only
// entry into the OR-merge guards.
let config = load(|jail| {
jail.set_env("DD_LOGS_ENABLED", "false");
Ok(())
});
assert!(!config.ext.serverless_logs_enabled);
}

#[test]
fn serverless_logs_disabled_when_both_false() {
let config = load(|jail| {
Expand All @@ -370,6 +391,18 @@ mod lambda_config_tests {
assert!(!config.ext.serverless_logs_enabled);
}

#[test]
fn dd_logs_enabled_also_populates_upstream_field() {
// The upstream Config::logs_enabled field is wired through the
// upstream env parsing independently of the lambda alias. Lambda
// doesn't read this field, but other consumers of the same crate do.
let config = load(|jail| {
jail.set_env("DD_LOGS_ENABLED", "true");
Ok(())
});
assert!(config.logs_enabled);
}

// ---- FlushStrategy ----

#[test]
Expand Down Expand Up @@ -598,10 +631,7 @@ mod lambda_config_tests {
jail.set_env("DD_ORG_UUID", "00000000-1111-2222-3333-444444444444");
Ok(())
});
assert_eq!(
config.ext.dd_org_uuid,
"00000000-1111-2222-3333-444444444444"
);
assert_eq!(config.dd_org_uuid, "00000000-1111-2222-3333-444444444444");
}

#[test]
Expand All @@ -615,10 +645,7 @@ mod lambda_config_tests {
)?;
Ok(())
});
assert_eq!(
config.ext.dd_org_uuid,
"00000000-1111-2222-3333-444444444444"
);
assert_eq!(config.dd_org_uuid, "00000000-1111-2222-3333-444444444444");
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions bottlecap/src/secrets/decrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub async fn resolve_secrets(
let api_key_candidate = if !config.ext.api_key_secret_arn.is_empty()
|| !config.ext.kms_api_key.is_empty()
|| !config.ext.api_key_ssm_arn.is_empty()
|| !config.ext.dd_org_uuid.is_empty()
|| !config.dd_org_uuid.is_empty()
{
let before_decrypt = Instant::now();

Expand All @@ -48,7 +48,7 @@ pub async fn resolve_secrets(

let aws_credentials = get_aws_credentials(&client).await?;

let decrypted_key = if !config.ext.dd_org_uuid.is_empty() {
let decrypted_key = if !config.dd_org_uuid.is_empty() {
delegated_auth::get_delegated_api_key(
&config,
&aws_config,
Expand Down
4 changes: 2 additions & 2 deletions bottlecap/src/secrets/delegated_auth/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const INTAKE_KEY_ENDPOINT: &str = "/api/v2/intake-key";
/// 3. Returns the managed API key
///
/// # Arguments
/// * `config` - The extension configuration containing site and `org_uuid`
/// * `config` - The agent configuration containing `site` and `dd_org_uuid`
/// * `aws_config` - The AWS configuration containing region
/// * `client` - A pre-built `reqwest::Client` to use for the request. The client is built
/// with `create_reqwest_client_builder()` which respects proxy configuration via
Expand All @@ -35,7 +35,7 @@ pub async fn get_delegated_api_key(
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
debug!("Attempting to get API key via delegated auth");

let proof = generate_auth_proof(aws_credentials, &aws_config.region, &config.ext.dd_org_uuid)?;
let proof = generate_auth_proof(aws_credentials, &aws_config.region, &config.dd_org_uuid)?;
Comment thread
duncanista marked this conversation as resolved.

let url = get_api_endpoint(&config.site);
debug!("Requesting delegated API key from: {}", url);
Expand Down
Loading