Skip to content

Commit 4796057

Browse files
authored
Fix duplicate entries in IP pools list (#4808)
Bugfix for #4261. Using `distinct` to eliminate dupes from the left outer join works, but it's better to just use the well-known name of the service IP pool to exclude it from whatever operations it needs to be excluded from. Potential followup related to #4762: if the ID was well-known to, we could do the `is_internal` check without having to hit the DB.
1 parent 146aa5f commit 4796057

File tree

3 files changed

+81
-75
lines changed

3 files changed

+81
-75
lines changed

nexus/db-queries/src/db/datastore/ip_pool.rs

Lines changed: 7 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ use crate::context::OpContext;
1010
use crate::db;
1111
use crate::db::collection_insert::AsyncInsertError;
1212
use crate::db::collection_insert::DatastoreCollection;
13+
use crate::db::datastore::SERVICE_IP_POOL_NAME;
1314
use crate::db::error::public_error_from_diesel;
1415
use crate::db::error::public_error_from_diesel_lookup;
1516
use crate::db::error::ErrorHandler;
16-
use crate::db::fixed_data::silo::INTERNAL_SILO_ID;
1717
use crate::db::identity::Resource;
18+
use crate::db::lookup::LookupPath;
1819
use crate::db::model::ExternalIp;
1920
use crate::db::model::IpKind;
2021
use crate::db::model::IpPool;
@@ -56,7 +57,6 @@ impl DataStore {
5657
pagparams: &PaginatedBy<'_>,
5758
) -> ListResultVec<IpPool> {
5859
use db::schema::ip_pool;
59-
use db::schema::ip_pool_resource;
6060

6161
opctx
6262
.authorize(authz::Action::ListChildren, &authz::IP_POOL_LIST)
@@ -71,14 +71,7 @@ impl DataStore {
7171
&pagparams.map_name(|n| Name::ref_cast(n)),
7272
),
7373
}
74-
.left_outer_join(ip_pool_resource::table)
75-
.filter(
76-
ip_pool_resource::resource_id
77-
.ne(*INTERNAL_SILO_ID)
78-
// resource_id is not nullable -- null here means the
79-
// pool has no entry in the join table
80-
.or(ip_pool_resource::resource_id.is_null()),
81-
)
74+
.filter(ip_pool::name.ne(SERVICE_IP_POOL_NAME))
8275
.filter(ip_pool::time_deleted.is_null())
8376
.select(IpPool::as_select())
8477
.get_results_async(&*self.pool_connection_authorized(opctx).await?)
@@ -225,48 +218,15 @@ impl DataStore {
225218
})
226219
}
227220

228-
/// Looks up an IP pool intended for internal services.
221+
/// Look up IP pool intended for internal services by its well-known name.
229222
///
230223
/// This method may require an index by Availability Zone in the future.
231224
pub async fn ip_pools_service_lookup(
232225
&self,
233226
opctx: &OpContext,
234227
) -> LookupResult<(authz::IpPool, IpPool)> {
235-
use db::schema::ip_pool;
236-
use db::schema::ip_pool_resource;
237-
238-
opctx
239-
.authorize(authz::Action::ListChildren, &authz::IP_POOL_LIST)
240-
.await?;
241-
242-
// Look up IP pool by its association with the internal silo.
243-
// We assume there is only one pool for that silo, or at least,
244-
// if there is more than one, it doesn't matter which one we pick.
245-
let (authz_pool, pool) = ip_pool::table
246-
.inner_join(ip_pool_resource::table)
247-
.filter(ip_pool::time_deleted.is_null())
248-
.filter(
249-
ip_pool_resource::resource_type
250-
.eq(IpPoolResourceType::Silo)
251-
.and(ip_pool_resource::resource_id.eq(*INTERNAL_SILO_ID)),
252-
)
253-
.select(IpPool::as_select())
254-
.get_result_async(&*self.pool_connection_authorized(opctx).await?)
255-
.await
256-
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
257-
.map(|ip_pool| {
258-
(
259-
authz::IpPool::new(
260-
authz::FLEET,
261-
ip_pool.id(),
262-
LookupType::ByCompositeId(
263-
"Service IP Pool".to_string(),
264-
),
265-
),
266-
ip_pool,
267-
)
268-
})?;
269-
Ok((authz_pool, pool))
228+
let name = SERVICE_IP_POOL_NAME.parse().unwrap();
229+
LookupPath::new(&opctx, self).ip_pool_name(&Name(name)).fetch().await
270230
}
271231

272232
/// Creates a new IP pool.
@@ -374,15 +334,10 @@ impl DataStore {
374334
authz_pool: &authz::IpPool,
375335
) -> LookupResult<bool> {
376336
use db::schema::ip_pool;
377-
use db::schema::ip_pool_resource;
378337

379338
ip_pool::table
380-
.inner_join(ip_pool_resource::table)
381339
.filter(ip_pool::id.eq(authz_pool.id()))
382-
.filter(
383-
ip_pool_resource::resource_type.eq(IpPoolResourceType::Silo),
384-
)
385-
.filter(ip_pool_resource::resource_id.eq(*INTERNAL_SILO_ID))
340+
.filter(ip_pool::name.eq(SERVICE_IP_POOL_NAME))
386341
.filter(ip_pool::time_deleted.is_null())
387342
.select(ip_pool::id)
388343
.first_async::<Uuid>(

nexus/tests/integration_tests/endpoints.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,7 @@ pub static VERIFY_ENDPOINTS: Lazy<Vec<VerifyEndpoint>> = Lazy::new(|| {
10481048
// IP Pool endpoint (Oxide services)
10491049
VerifyEndpoint {
10501050
url: &DEMO_IP_POOL_SERVICE_URL,
1051-
visibility: Visibility::Public,
1051+
visibility: Visibility::Protected,
10521052
unprivileged_access: UnprivilegedAccess::None,
10531053
allowed_methods: vec![
10541054
AllowedMethod::Get
@@ -1058,7 +1058,7 @@ pub static VERIFY_ENDPOINTS: Lazy<Vec<VerifyEndpoint>> = Lazy::new(|| {
10581058
// IP Pool ranges endpoint (Oxide services)
10591059
VerifyEndpoint {
10601060
url: &DEMO_IP_POOL_SERVICE_RANGES_URL,
1061-
visibility: Visibility::Public,
1061+
visibility: Visibility::Protected,
10621062
unprivileged_access: UnprivilegedAccess::None,
10631063
allowed_methods: vec![
10641064
AllowedMethod::Get
@@ -1068,7 +1068,7 @@ pub static VERIFY_ENDPOINTS: Lazy<Vec<VerifyEndpoint>> = Lazy::new(|| {
10681068
// IP Pool ranges/add endpoint (Oxide services)
10691069
VerifyEndpoint {
10701070
url: &DEMO_IP_POOL_SERVICE_RANGES_ADD_URL,
1071-
visibility: Visibility::Public,
1071+
visibility: Visibility::Protected,
10721072
unprivileged_access: UnprivilegedAccess::None,
10731073
allowed_methods: vec![
10741074
AllowedMethod::Post(
@@ -1080,7 +1080,7 @@ pub static VERIFY_ENDPOINTS: Lazy<Vec<VerifyEndpoint>> = Lazy::new(|| {
10801080
// IP Pool ranges/delete endpoint (Oxide services)
10811081
VerifyEndpoint {
10821082
url: &DEMO_IP_POOL_SERVICE_RANGES_DEL_URL,
1083-
visibility: Visibility::Public,
1083+
visibility: Visibility::Protected,
10841084
unprivileged_access: UnprivilegedAccess::None,
10851085
allowed_methods: vec![
10861086
AllowedMethod::Post(

nexus/tests/integration_tests/ip_pools.rs

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use nexus_test_utils::http_testing::RequestBuilder;
1717
use nexus_test_utils::resource_helpers::create_instance;
1818
use nexus_test_utils::resource_helpers::create_ip_pool;
1919
use nexus_test_utils::resource_helpers::create_project;
20+
use nexus_test_utils::resource_helpers::create_silo;
2021
use nexus_test_utils::resource_helpers::link_ip_pool;
2122
use nexus_test_utils::resource_helpers::object_create;
2223
use nexus_test_utils::resource_helpers::object_create_error;
@@ -36,13 +37,15 @@ use nexus_types::external_api::params::IpPoolUpdate;
3637
use nexus_types::external_api::shared::IpRange;
3738
use nexus_types::external_api::shared::Ipv4Range;
3839
use nexus_types::external_api::shared::Ipv6Range;
40+
use nexus_types::external_api::shared::SiloIdentityMode;
3941
use nexus_types::external_api::views::IpPool;
4042
use nexus_types::external_api::views::IpPoolRange;
4143
use nexus_types::external_api::views::IpPoolSilo;
4244
use nexus_types::external_api::views::Silo;
4345
use nexus_types::identity::Resource;
4446
use omicron_common::api::external::IdentityMetadataUpdateParams;
4547
use omicron_common::api::external::NameOrId;
48+
use omicron_common::api::external::SimpleIdentity;
4649
use omicron_common::api::external::{IdentityMetadataCreateParams, Name};
4750
use omicron_nexus::TestInterfaces;
4851
use sled_agent_client::TestInterfaces as SledTestInterfaces;
@@ -62,16 +65,7 @@ async fn test_ip_pool_basic_crud(cptestctx: &ControlPlaneTestContext) {
6265
let ip_pool_ranges_url = format!("{}/ranges", ip_pool_url);
6366
let ip_pool_add_range_url = format!("{}/add", ip_pool_ranges_url);
6467

65-
// Verify the list of IP pools is empty
66-
let ip_pools = NexusRequest::iter_collection_authn::<IpPool>(
67-
client,
68-
ip_pools_url,
69-
"",
70-
None,
71-
)
72-
.await
73-
.expect("Failed to list IP Pools")
74-
.all_items;
68+
let ip_pools = get_ip_pools(&client).await;
7569
assert_eq!(ip_pools.len(), 0, "Expected empty list of IP pools");
7670

7771
// Verify 404 if the pool doesn't exist yet, both for creating or deleting
@@ -102,15 +96,7 @@ async fn test_ip_pool_basic_crud(cptestctx: &ControlPlaneTestContext) {
10296
assert_eq!(created_pool.identity.name, pool_name);
10397
assert_eq!(created_pool.identity.description, description);
10498

105-
let list = NexusRequest::iter_collection_authn::<IpPool>(
106-
client,
107-
ip_pools_url,
108-
"",
109-
None,
110-
)
111-
.await
112-
.expect("Failed to list IP Pools")
113-
.all_items;
99+
let list = get_ip_pools(client).await;
114100
assert_eq!(list.len(), 1, "Expected exactly 1 IP pool");
115101
assert_pools_eq(&created_pool, &list[0]);
116102

@@ -212,6 +198,71 @@ async fn test_ip_pool_basic_crud(cptestctx: &ControlPlaneTestContext) {
212198
.expect("Expected to be able to delete an empty IP Pool");
213199
}
214200

201+
async fn get_ip_pools(client: &ClientTestContext) -> Vec<IpPool> {
202+
NexusRequest::iter_collection_authn::<IpPool>(
203+
client,
204+
"/v1/system/ip-pools",
205+
"",
206+
None,
207+
)
208+
.await
209+
.expect("Failed to list IP Pools")
210+
.all_items
211+
}
212+
213+
// this test exists primarily because of a bug in the initial implementation
214+
// where we included a duplicate of each pool in the list response for every
215+
// associated silo
216+
#[nexus_test]
217+
async fn test_ip_pool_list_dedupe(cptestctx: &ControlPlaneTestContext) {
218+
let client = &cptestctx.external_client;
219+
220+
let ip_pools = get_ip_pools(&client).await;
221+
assert_eq!(ip_pools.len(), 0);
222+
223+
let range1 = IpRange::V4(
224+
Ipv4Range::new(
225+
std::net::Ipv4Addr::new(10, 0, 0, 51),
226+
std::net::Ipv4Addr::new(10, 0, 0, 52),
227+
)
228+
.unwrap(),
229+
);
230+
let (pool1, ..) = create_ip_pool(client, "pool1", Some(range1)).await;
231+
let range2 = IpRange::V4(
232+
Ipv4Range::new(
233+
std::net::Ipv4Addr::new(10, 0, 0, 53),
234+
std::net::Ipv4Addr::new(10, 0, 0, 54),
235+
)
236+
.unwrap(),
237+
);
238+
let (pool2, ..) = create_ip_pool(client, "pool2", Some(range2)).await;
239+
240+
let ip_pools = get_ip_pools(&client).await;
241+
assert_eq!(ip_pools.len(), 2);
242+
assert_eq!(ip_pools[0].identity.id, pool1.id());
243+
assert_eq!(ip_pools[1].identity.id, pool2.id());
244+
245+
// create 3 silos and link
246+
let silo1 =
247+
create_silo(&client, "silo1", true, SiloIdentityMode::SamlJit).await;
248+
link_ip_pool(client, "pool1", &silo1.id(), false).await;
249+
// linking pool2 here only, just for variety
250+
link_ip_pool(client, "pool2", &silo1.id(), false).await;
251+
252+
let silo2 =
253+
create_silo(&client, "silo2", true, SiloIdentityMode::SamlJit).await;
254+
link_ip_pool(client, "pool1", &silo2.id(), true).await;
255+
256+
let silo3 =
257+
create_silo(&client, "silo3", true, SiloIdentityMode::SamlJit).await;
258+
link_ip_pool(client, "pool1", &silo3.id(), true).await;
259+
260+
let ip_pools = get_ip_pools(&client).await;
261+
assert_eq!(ip_pools.len(), 2);
262+
assert_eq!(ip_pools[0].identity.id, pool1.id());
263+
assert_eq!(ip_pools[1].identity.id, pool2.id());
264+
}
265+
215266
/// The internal IP pool, defined by its association with the internal silo,
216267
/// cannot be interacted with through the operator API. CRUD operations should
217268
/// all 404 except fetch by name or ID.

0 commit comments

Comments
 (0)