Skip to content

Commit 0f55bf4

Browse files
authored
ref(organizations): Use control silo endpoint for org listing (#3352)
### Description The organization listing endpoint (GET /organizations/) is now served from the control silo and returns every organization the authenticated user belongs to across all regions in a single paginated response. This replaces the previous per-region fan-out (which called /users/me/regions/ and then queried each region) with one call. Also aligns the Organization model with the current API response: drops requireEmailVerification (removed in getsentry/sentry#115003) and makes features optional (removed from the listing endpoint in #115007), so no longer fails to deserialize the live response. ### Issues * resolves: INFRENG-199 ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.
1 parent 1bf4117 commit 0f55bf4

4 files changed

Lines changed: 13 additions & 80 deletions

File tree

src/api/mod.rs

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -744,21 +744,18 @@ impl AuthenticatedApi<'_> {
744744
.send_into(dst)
745745
}
746746

747-
/// List all organizations associated with the authenticated token
748-
/// in the given `Region`. If no `Region` is provided, we assume
749-
/// we're issuing a request to a monolith deployment.
750-
pub fn list_organizations(&self, region: Option<&Region>) -> ApiResult<Vec<Organization>> {
747+
/// List all organizations associated with the authenticated token.
748+
///
749+
/// This hits the control silo organization listing, which returns every
750+
/// organization the authenticated user belongs to across all regions in a
751+
/// single (paginated) response. Self-hosted/monolith deployments serve the
752+
/// same endpoint and return all organizations as well.
753+
pub fn list_organizations(&self) -> ApiResult<Vec<Organization>> {
751754
let mut rv = vec![];
752755
let mut cursor = "".to_owned();
753756
loop {
754757
let current_path = &format!("/organizations/?cursor={}", QueryArg(&cursor));
755-
let resp = if let Some(rg) = region {
756-
self.api
757-
.request(Method::Get, current_path, Some(&rg.url))?
758-
.send()?
759-
} else {
760-
self.get(current_path)?
761-
};
758+
let resp = self.get(current_path)?;
762759

763760
if resp.status() == 404 || (resp.status() == 400 && !cursor.is_empty()) {
764761
if rv.is_empty() {
@@ -778,22 +775,6 @@ impl AuthenticatedApi<'_> {
778775
Ok(rv)
779776
}
780777

781-
pub fn list_available_regions(&self) -> ApiResult<Vec<Region>> {
782-
let resp = self.get("/users/me/regions/")?;
783-
if resp.status() == 404 {
784-
// This endpoint may not exist for self-hosted users, so
785-
// returning a default of [] seems appropriate.
786-
return Ok(vec![]);
787-
}
788-
789-
if resp.status() == 400 {
790-
return Err(ApiErrorKind::ResourceNotFound.into());
791-
}
792-
793-
let region_response = resp.convert::<RegionResponse>()?;
794-
Ok(region_response.regions)
795-
}
796-
797778
/// List all monitors associated with an organization
798779
pub fn list_organization_monitors(&self, org: &str) -> ApiResult<Vec<Monitor>> {
799780
let mut rv = vec![];
@@ -1960,9 +1941,9 @@ pub struct Organization {
19601941
pub is_early_adopter: bool,
19611942
#[serde(rename = "require2FA")]
19621943
pub require_2fa: bool,
1963-
#[serde(rename = "requireEmailVerification")]
1964-
#[expect(dead_code)]
1965-
pub require_email_verification: bool,
1944+
// features was removed from the org listing endpoint; this could be removed too
1945+
// Same with the get-organizations.json test fixture, the features fields could be removed
1946+
#[serde(default)]
19661947
#[expect(dead_code)]
19671948
pub features: Vec<String>,
19681949
}
@@ -2111,18 +2092,6 @@ impl fmt::Display for ProcessedEventTag {
21112092
}
21122093
}
21132094

2114-
#[derive(Clone, Debug, Deserialize)]
2115-
pub struct Region {
2116-
#[expect(dead_code)]
2117-
pub name: String,
2118-
pub url: String,
2119-
}
2120-
2121-
#[derive(Clone, Debug, Deserialize)]
2122-
pub struct RegionResponse {
2123-
pub regions: Vec<Region>,
2124-
}
2125-
21262095
/// Response structure for logs API
21272096
#[derive(Debug, Deserialize)]
21282097
struct LogsResponse {

src/commands/organizations/list.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use anyhow::Result;
22
use clap::{ArgMatches, Command};
3-
use log::debug;
43

54
use crate::api::{Api, Organization};
65
use crate::utils::formatting::Table;
@@ -13,21 +12,7 @@ pub fn execute(_matches: &ArgMatches) -> Result<()> {
1312
let api = Api::current();
1413
let authenticated_api = api.authenticated()?;
1514

16-
// Query regions available to the current CLI user
17-
let regions = authenticated_api.list_available_regions()?;
18-
19-
let mut organizations: Vec<Organization> = vec![];
20-
debug!("Available regions: {regions:?}");
21-
22-
// Self-hosted instances won't have a region instance or prefix, so we
23-
// need to check before fanning out.
24-
if !regions.is_empty() {
25-
for region in regions {
26-
organizations.append(&mut authenticated_api.list_organizations(Some(&region))?)
27-
}
28-
} else {
29-
organizations.append(&mut authenticated_api.list_organizations(None)?)
30-
}
15+
let mut organizations: Vec<Organization> = authenticated_api.list_organizations()?;
3116

3217
organizations.sort_by_key(|o| o.name.clone().to_lowercase());
3318

tests/integration/_responses/organizations/get-organizations.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
"dateCreated": "2020-09-14T17:28:14.933511Z",
1111
"isEarlyAdopter": true,
1212
"require2FA": false,
13-
"requireEmailVerification": false,
1413
"avatar": {
1514
"avatarType": "upload",
1615
"avatarUuid": "9fe0ee23944145b49c64924dfbca40fe"
@@ -98,7 +97,6 @@
9897
"dateCreated": "2019-12-18T13:20:09.250550Z",
9998
"isEarlyAdopter": false,
10099
"require2FA": false,
101-
"requireEmailVerification": false,
102100
"avatar": {
103101
"avatarType": "letter_avatar",
104102
"avatarUuid": null
@@ -147,7 +145,6 @@
147145
"dateCreated": "2018-04-03T17:51:37.660710Z",
148146
"isEarlyAdopter": false,
149147
"require2FA": false,
150-
"requireEmailVerification": false,
151148
"avatar": {
152149
"avatarType": "letter_avatar",
153150
"avatarUuid": null
@@ -198,7 +195,6 @@
198195
"dateCreated": "2015-03-02T23:55:20Z",
199196
"isEarlyAdopter": true,
200197
"require2FA": false,
201-
"requireEmailVerification": false,
202198
"avatar": {
203199
"avatarType": "upload",
204200
"avatarUuid": "24e94751da694e0fa57947e289048d7e"
@@ -292,7 +288,6 @@
292288
"dateCreated": "2014-12-15T04:06:24.263571Z",
293289
"isEarlyAdopter": true,
294290
"require2FA": false,
295-
"requireEmailVerification": false,
296291
"avatar": {
297292
"avatarType": "upload",
298293
"avatarUuid": "24f6f762f7a7473888b259c566da5adb"

tests/integration/organizations.rs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,11 @@ use crate::integration::{MockEndpointBuilder, TestManager};
22

33
#[test]
44
fn command_organizations() {
5-
let manager = TestManager::new();
6-
let region_response = format!(
7-
r#"{{
8-
"regions": [{{
9-
"name": "monolith",
10-
"url": "{}"
11-
}}]
12-
}}"#,
13-
manager.server_url(),
14-
);
15-
16-
manager
17-
// Mocks are for the organizations list command.
5+
TestManager::new()
186
.mock_endpoint(
197
MockEndpointBuilder::new("GET", "/api/0/organizations/?cursor=")
208
.with_response_file("organizations/get-organizations.json"),
219
)
22-
.mock_endpoint(
23-
MockEndpointBuilder::new("GET", "/api/0/users/me/regions/")
24-
.with_response_body(region_response),
25-
)
2610
.register_trycmd_test("organizations/*.trycmd")
2711
.with_default_token();
2812
}

0 commit comments

Comments
 (0)