Skip to content

Commit fc7c2d3

Browse files
Merge pull request #446 from datadog-labs/feat/auto-apply-site-from-org-session
feat(auth): apply saved site automatically when --org is used
2 parents 9da4223 + 41963f9 commit fc7c2d3

14 files changed

Lines changed: 530 additions & 23 deletions

File tree

src/api.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ mod tests {
129129
app_key: Some("test-app".into()),
130130
access_token: None,
131131
site: "datadoghq.com".into(),
132+
site_explicit: false,
132133
org: None,
133134
output_format: OutputFormat::Json,
134135
auto_approve: false,
@@ -163,6 +164,7 @@ mod tests {
163164
app_key: Some("test-app".into()),
164165
access_token: None,
165166
site: "datadoghq.com".into(),
167+
site_explicit: false,
166168
org: None,
167169
output_format: OutputFormat::Json,
168170
auto_approve: false,
@@ -201,6 +203,7 @@ mod tests {
201203
app_key: Some("test-app".into()),
202204
access_token: None,
203205
site: "datadoghq.com".into(),
206+
site_explicit: false,
204207
org: None,
205208
output_format: OutputFormat::Json,
206209
auto_approve: false,
@@ -234,6 +237,7 @@ mod tests {
234237
app_key: Some("test-app".into()),
235238
access_token: None,
236239
site: "datadoghq.com".into(),
240+
site_explicit: false,
237241
org: None,
238242
output_format: OutputFormat::Json,
239243
auto_approve: false,
@@ -267,6 +271,7 @@ mod tests {
267271
app_key: Some("test-app".into()),
268272
access_token: None,
269273
site: "datadoghq.com".into(),
274+
site_explicit: false,
270275
org: None,
271276
output_format: OutputFormat::Json,
272277
auto_approve: false,
@@ -300,6 +305,7 @@ mod tests {
300305
app_key: Some("test-app".into()),
301306
access_token: None,
302307
site: "datadoghq.com".into(),
308+
site_explicit: false,
303309
org: None,
304310
output_format: OutputFormat::Json,
305311
auto_approve: false,
@@ -332,6 +338,7 @@ mod tests {
332338
app_key: Some("test-app".into()),
333339
access_token: None,
334340
site: "datadoghq.com".into(),
341+
site_explicit: false,
335342
org: None,
336343
output_format: OutputFormat::Json,
337344
auto_approve: false,
@@ -365,6 +372,7 @@ mod tests {
365372
app_key: None,
366373
access_token: Some("test-bearer-token".into()),
367374
site: "datadoghq.com".into(),
375+
site_explicit: false,
368376
org: None,
369377
output_format: OutputFormat::Json,
370378
auto_approve: false,
@@ -396,6 +404,7 @@ mod tests {
396404
app_key: None,
397405
access_token: None,
398406
site: "datadoghq.com".into(),
407+
site_explicit: false,
399408
org: None,
400409
output_format: OutputFormat::Json,
401410
auto_approve: false,
@@ -423,6 +432,7 @@ mod tests {
423432
app_key: Some("test-app".into()),
424433
access_token: None,
425434
site: "datadoghq.com".into(),
435+
site_explicit: false,
426436
org: None,
427437
output_format: OutputFormat::Json,
428438
auto_approve: false,
@@ -456,6 +466,7 @@ mod tests {
456466
app_key: Some("test-app".into()),
457467
access_token: None,
458468
site: "datadoghq.com".into(),
469+
site_explicit: false,
459470
org: None,
460471
output_format: OutputFormat::Json,
461472
auto_approve: false,

src/auth/storage.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,39 @@ pub fn remove_session(site: &str, org: Option<&str>) -> Result<()> {
900900
write_sessions(&sessions)
901901
}
902902

903+
/// Look up the site for a named org session. Returns None if no session exists
904+
/// for that org, or if multiple sessions share the same org name on different
905+
/// sites (ambiguous — caller must pass DD_SITE explicitly). On the ambiguous
906+
/// path, prints a one-line warning to stderr naming the conflicting sites so
907+
/// the user knows the auto-resolution gave up.
908+
#[cfg(not(target_arch = "wasm32"))]
909+
pub fn find_session_site(org: &str) -> Option<String> {
910+
let sessions = list_sessions().ok()?;
911+
let mut sites: Vec<String> = sessions
912+
.into_iter()
913+
.filter(|s| s.org.as_deref() == Some(org))
914+
.map(|s| s.site)
915+
.collect();
916+
sites.sort();
917+
sites.dedup();
918+
match sites.len() {
919+
0 => None,
920+
1 => sites.pop(),
921+
_ => {
922+
// The caller (Config::from_env / apply_org_override) handles the
923+
// resulting None by leaving cfg.site at whatever it was — which
924+
// may be a default, an env-set site, or a previously-resolved
925+
// org's site — so we do not promise "falling back to default" here.
926+
eprintln!(
927+
"Warning: org '{org}' has saved sessions on multiple sites ({}); \
928+
not auto-selecting one. Set DD_SITE to disambiguate.",
929+
sites.join(", ")
930+
);
931+
None
932+
}
933+
}
934+
}
935+
903936
#[cfg(not(target_arch = "wasm32"))]
904937
fn write_sessions(sessions: &[SessionEntry]) -> Result<()> {
905938
let path = match sessions_path() {
@@ -1280,6 +1313,63 @@ mod tests {
12801313
assert!(result.is_ok());
12811314
}
12821315

1316+
#[test]
1317+
fn test_find_session_site_unique_match() {
1318+
let _lock = crate::test_utils::ENV_LOCK.blocking_lock();
1319+
let tmp = TempDir::new("find_sess_unique");
1320+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
1321+
1322+
save_session("custom.datadoghq.com", Some("prod-child")).unwrap();
1323+
save_session("datadoghq.com", None).unwrap();
1324+
let result = find_session_site("prod-child");
1325+
std::env::remove_var("PUP_CONFIG_DIR");
1326+
1327+
assert_eq!(result.as_deref(), Some("custom.datadoghq.com"));
1328+
}
1329+
1330+
#[test]
1331+
fn test_find_session_site_no_match() {
1332+
let _lock = crate::test_utils::ENV_LOCK.blocking_lock();
1333+
let tmp = TempDir::new("find_sess_none");
1334+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
1335+
1336+
save_session("datadoghq.com", Some("prod-child")).unwrap();
1337+
let result = find_session_site("nonexistent");
1338+
std::env::remove_var("PUP_CONFIG_DIR");
1339+
1340+
assert!(result.is_none());
1341+
}
1342+
1343+
#[test]
1344+
fn test_find_session_site_ambiguous_returns_none() {
1345+
let _lock = crate::test_utils::ENV_LOCK.blocking_lock();
1346+
let tmp = TempDir::new("find_sess_amb");
1347+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
1348+
1349+
// Same org name registered against two different sites → caller must
1350+
// disambiguate via DD_SITE rather than us picking one.
1351+
save_session("datadoghq.com", Some("shared-name")).unwrap();
1352+
save_session("datadoghq.eu", Some("shared-name")).unwrap();
1353+
let result = find_session_site("shared-name");
1354+
std::env::remove_var("PUP_CONFIG_DIR");
1355+
1356+
assert!(result.is_none());
1357+
}
1358+
1359+
#[test]
1360+
fn test_find_session_site_skips_default_session() {
1361+
let _lock = crate::test_utils::ENV_LOCK.blocking_lock();
1362+
let tmp = TempDir::new("find_sess_default");
1363+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
1364+
1365+
// The unnamed (org=None) session must not match any --org lookup.
1366+
save_session("datadoghq.eu", None).unwrap();
1367+
let result = find_session_site("anything");
1368+
std::env::remove_var("PUP_CONFIG_DIR");
1369+
1370+
assert!(result.is_none());
1371+
}
1372+
12831373
// --- detect_backend ---------------------------------------------------------
12841374

12851375
// Exercises the FileStorage fallback when the auto-detect keychain probe fails,

src/client.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ pub fn make_dd_config(cfg: &Config) -> datadog_api_client::datadog::Configuratio
116116
// ap1, ap2, eu, gov). Server index 2 uses the same URL template but with
117117
// no enum restriction, so it works for any site including staging
118118
// (datad0g.com). Use index 2 for non-standard sites.
119+
//
120+
// The SDK populates `server_variables["site"]` from the DD_SITE env var
121+
// at Configuration::default() time. We override it with `cfg.site` so
122+
// programmatic site resolution (e.g. `--org` picking up a saved site
123+
// from the session registry) reaches the SDK without requiring the
124+
// user to also set DD_SITE.
119125
static STANDARD_SITES: &[&str] = &[
120126
"datadoghq.com",
121127
"us3.datadoghq.com",
@@ -125,10 +131,12 @@ pub fn make_dd_config(cfg: &Config) -> datadog_api_client::datadog::Configuratio
125131
"datadoghq.eu",
126132
"ddog-gov.com",
127133
];
128-
let site = std::env::var("DD_SITE").unwrap_or_default();
129-
if !site.is_empty() && !STANDARD_SITES.contains(&site.as_str()) {
134+
if !STANDARD_SITES.contains(&cfg.site.as_str()) {
130135
dd_cfg.server_index = 2;
131136
}
137+
dd_cfg
138+
.server_variables
139+
.insert("site".into(), cfg.site.clone());
132140
}
133141

134142
dd_cfg
@@ -1019,6 +1027,7 @@ mod tests {
10191027
app_key: Some("test".into()),
10201028
access_token: None,
10211029
site: "datadoghq.com".into(),
1030+
site_explicit: false,
10221031
org: None,
10231032
output_format: crate::config::OutputFormat::Json,
10241033
auto_approve: false,
@@ -1048,6 +1057,70 @@ mod tests {
10481057
assert_eq!(get_auth_type(&cfg), AuthType::None);
10491058
}
10501059

1060+
/// `make_dd_config` must propagate `cfg.site` into the SDK's `site`
1061+
/// server variable, otherwise programmatic site resolution (e.g.
1062+
/// `--org` picking up a saved staging site) silently routes API calls
1063+
/// to api.datadoghq.com.
1064+
#[test]
1065+
fn test_make_dd_config_uses_cfg_site_for_non_standard() {
1066+
let _guard = ENV_LOCK.blocking_lock();
1067+
std::env::remove_var("PUP_MOCK_SERVER");
1068+
std::env::remove_var("DD_SITE");
1069+
1070+
let mut cfg = test_cfg();
1071+
cfg.site = "datad0g.com".into();
1072+
1073+
let dd_cfg = make_dd_config(&cfg);
1074+
1075+
assert_eq!(dd_cfg.server_index, 2);
1076+
assert_eq!(
1077+
dd_cfg.server_variables.get("site").map(String::as_str),
1078+
Some("datad0g.com")
1079+
);
1080+
}
1081+
1082+
#[test]
1083+
fn test_make_dd_config_uses_cfg_site_for_standard() {
1084+
let _guard = ENV_LOCK.blocking_lock();
1085+
std::env::remove_var("PUP_MOCK_SERVER");
1086+
std::env::remove_var("DD_SITE");
1087+
1088+
let mut cfg = test_cfg();
1089+
cfg.site = "datadoghq.eu".into();
1090+
1091+
let dd_cfg = make_dd_config(&cfg);
1092+
1093+
assert_eq!(dd_cfg.server_index, 0);
1094+
assert_eq!(
1095+
dd_cfg.server_variables.get("site").map(String::as_str),
1096+
Some("datadoghq.eu")
1097+
);
1098+
}
1099+
1100+
/// `cfg.site` (e.g. resolved from a saved org session) must override any
1101+
/// stale `DD_SITE` env var the user happens to have set in their shell —
1102+
/// otherwise `pup --org staging-org` would silently route to the env's
1103+
/// site instead of the org's saved site.
1104+
#[test]
1105+
fn test_make_dd_config_cfg_site_overrides_env_dd_site() {
1106+
let _guard = ENV_LOCK.blocking_lock();
1107+
std::env::remove_var("PUP_MOCK_SERVER");
1108+
std::env::set_var("DD_SITE", "datadoghq.com");
1109+
1110+
let mut cfg = test_cfg();
1111+
cfg.site = "datad0g.com".into();
1112+
1113+
let dd_cfg = make_dd_config(&cfg);
1114+
1115+
std::env::remove_var("DD_SITE");
1116+
1117+
assert_eq!(dd_cfg.server_index, 2);
1118+
assert_eq!(
1119+
dd_cfg.server_variables.get("site").map(String::as_str),
1120+
Some("datad0g.com")
1121+
);
1122+
}
1123+
10511124
#[test]
10521125
fn test_auth_type_display() {
10531126
assert_eq!(AuthType::OAuth.to_string(), "OAuth2 Bearer Token");

src/commands/auth.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ mod tests {
393393
app_key: None,
394394
access_token: None,
395395
site: "datadoghq.com".into(),
396+
site_explicit: false,
396397
org: None,
397398
output_format: OutputFormat::Json,
398399
auto_approve: false,

src/commands/cost_ccm.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,7 @@ mod tests {
606606
app_key: None,
607607
access_token: None,
608608
site: "datadoghq.com".into(),
609+
site_explicit: false,
609610
org: None,
610611
output_format: OutputFormat::Json,
611612
auto_approve: false,

src/commands/debugger.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ mod tests {
699699
app_key: Some("test".into()),
700700
access_token: None,
701701
site: "datadoghq.com".into(),
702+
site_explicit: false,
702703
org: None,
703704
output_format: OutputFormat::Json,
704705
auto_approve: false,

src/commands/events.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ mod tests {
138138
app_key: None,
139139
access_token: Some("token".into()),
140140
site: "datadoghq.com".into(),
141+
site_explicit: false,
141142
org: None,
142143
output_format: OutputFormat::Json,
143144
auto_approve: false,

src/commands/llm_obs.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ mod tests {
440440
app_key: None,
441441
access_token: None,
442442
site: "datadoghq.com".into(),
443+
site_explicit: false,
443444
org: None,
444445
output_format: OutputFormat::Json,
445446
auto_approve: false,
@@ -851,6 +852,7 @@ mod tests {
851852
app_key: None,
852853
access_token: None,
853854
site: "datadoghq.com".into(),
855+
site_explicit: false,
854856
org: None,
855857
output_format: OutputFormat::Json,
856858
auto_approve: false,
@@ -936,6 +938,7 @@ mod tests {
936938
app_key: None,
937939
access_token: None,
938940
site: "datadoghq.com".into(),
941+
site_explicit: false,
939942
org: None,
940943
output_format: OutputFormat::Json,
941944
auto_approve: false,
@@ -1083,6 +1086,7 @@ mod tests {
10831086
app_key: None,
10841087
access_token: None,
10851088
site: "datadoghq.com".into(),
1089+
site_explicit: false,
10861090
org: None,
10871091
output_format: OutputFormat::Json,
10881092
auto_approve: false,
@@ -1515,6 +1519,7 @@ mod tests {
15151519
app_key: None,
15161520
access_token: None,
15171521
site: "datadoghq.com".into(),
1522+
site_explicit: false,
15181523
org: None,
15191524
output_format: OutputFormat::Json,
15201525
auto_approve: false,

src/commands/logs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,7 @@ mod tests {
677677
app_key: None,
678678
access_token: Some("token".into()),
679679
site: "datadoghq.com".into(),
680+
site_explicit: false,
680681
org: None,
681682
output_format: OutputFormat::Json,
682683
auto_approve: false,

0 commit comments

Comments
 (0)