Skip to content
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

Small IP pools fixes #4007

Merged
merged 3 commits into from
Sep 1, 2023
Merged
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
6 changes: 5 additions & 1 deletion nexus/db-model/src/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ impl IpPool {

impl From<IpPool> for views::IpPool {
fn from(pool: IpPool) -> Self {
Self { identity: pool.identity(), silo_id: pool.silo_id }
Self {
identity: pool.identity(),
silo_id: pool.silo_id,
is_default: pool.is_default,
}
}
}

Expand Down
29 changes: 25 additions & 4 deletions nexus/db-queries/src/db/datastore/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
//! [`DataStore`] methods on [`ExternalIp`]s.

use super::DataStore;
use crate::authz;
use crate::authz::ApiResource;
use crate::context::OpContext;
use crate::db;
use crate::db::error::public_error_from_diesel_pool;
use crate::db::error::ErrorHandler;
use crate::db::lookup::LookupPath;
use crate::db::model::ExternalIp;
use crate::db::model::IncompleteExternalIp;
use crate::db::model::IpKind;
Expand Down Expand Up @@ -52,11 +55,29 @@ impl DataStore {
instance_id: Uuid,
pool_name: Option<Name>,
) -> CreateResult<ExternalIp> {
// If we have a pool name, look up the pool by name and return it
// as long as its scopes don't conflict with the current scope.
// Otherwise, not found.
let pool = match pool_name {
Some(name) => self.ip_pools_fetch(&opctx, &name).await?,
Some(name) => {
let (.., authz_pool, pool) = LookupPath::new(opctx, &self)
.ip_pool_name(&name)
// any authenticated user can CreateChild on an IP pool. this is
// meant to represent allocating an IP
.fetch_for(authz::Action::CreateChild)
.await?;

// If the named pool conflicts with user's current scope, i.e.,
// if it has a silo and it's different from the current silo,
// then as far as IP allocation is concerned, that pool doesn't
// exist. If the pool has no silo, it's fleet-scoped and can
// always be used.
let authz_silo_id = opctx.authn.silo_required()?.id();
if let Some(pool_silo_id) = pool.silo_id {
if pool_silo_id != authz_silo_id {
return Err(authz_pool.not_found());
}
}

pool
}
// If no name given, use the default logic
None => self.ip_pools_fetch_default(&opctx).await?,
};
Expand Down
46 changes: 6 additions & 40 deletions nexus/db-queries/src/db/datastore/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use super::DataStore;
use crate::authz;
use crate::authz::ApiResource;
use crate::context::OpContext;
use crate::db;
use crate::db::collection_insert::AsyncInsertError;
Expand All @@ -16,7 +15,6 @@ use crate::db::error::public_error_from_diesel_pool;
use crate::db::error::ErrorHandler;
use crate::db::fixed_data::silo::INTERNAL_SILO_ID;
use crate::db::identity::Resource;
use crate::db::lookup::LookupPath;
use crate::db::model::IpPool;
use crate::db::model::IpPoolRange;
use crate::db::model::IpPoolUpdate;
Expand Down Expand Up @@ -111,32 +109,6 @@ impl DataStore {
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

/// Looks up an IP pool by name if it does not conflict with your current scope.
pub(crate) async fn ip_pools_fetch(
&self,
opctx: &OpContext,
name: &Name,
) -> LookupResult<IpPool> {
let authz_silo_id = opctx.authn.silo_required()?.id();

let (.., authz_pool, pool) = LookupPath::new(opctx, &self)
.ip_pool_name(&name)
// any authenticated user can CreateChild on an IP pool. this is
// meant to represent allocating an IP
.fetch_for(authz::Action::CreateChild)
.await?;

// You can't look up a pool by name if it conflicts with your current
// scope, i.e., if it has a silo it is different from your current silo
if let Some(pool_silo_id) = pool.silo_id {
if pool_silo_id != authz_silo_id {
return Err(authz_pool.not_found());
}
}

Ok(pool)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This did not make sense as a function here. All the logic was related to the calling context.

/// Looks up an IP pool intended for internal services.
///
/// This method may require an index by Availability Zone in the future.
Expand Down Expand Up @@ -465,7 +437,6 @@ mod test {
use crate::db::datastore::datastore_test;
use crate::db::model::IpPool;
use assert_matches::assert_matches;
use nexus_db_model::Name;
use nexus_test_utils::db::test_setup_database;
use nexus_types::identity::Resource;
use omicron_common::api::external::{Error, IdentityMetadataCreateParams};
Expand Down Expand Up @@ -510,12 +481,13 @@ mod test {
name: "non-default-for-silo".parse().unwrap(),
description: "".to_string(),
};
let _ = datastore
datastore
.ip_pool_create(
&opctx,
IpPool::new(&identity, Some(silo_id), /*default= */ false),
)
.await;
.await
.expect("Failed to create silo non-default IP pool");

// because that one was not a default, when we ask for the silo default
// pool, we still get the fleet default
Expand All @@ -530,9 +502,10 @@ mod test {
name: "default-for-silo".parse().unwrap(),
description: "".to_string(),
};
let _ = datastore
datastore
.ip_pool_create(&opctx, IpPool::new(&identity, Some(silo_id), true))
.await;
.await
.expect("Failed to create silo default IP pool");

// now when we ask for the default pool, we get the one we just made
let ip_pool = datastore
Expand All @@ -541,13 +514,6 @@ mod test {
.expect("Failed to get silo's default IP pool");
assert_eq!(ip_pool.name().as_str(), "default-for-silo");

// if we ask for the fleet default by name, we can still get that one
let ip_pool = datastore
.ip_pools_fetch(&opctx, &Name("default".parse().unwrap()))
.await
.expect("Failed to get fleet default IP pool");
assert_eq!(ip_pool.id(), fleet_default_pool.id());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't do this anymore because the method is gone, but this integration test covers it.

// we can still specify other pool even though we now have a silo default
create_instance_with_pool(client, "other-pool-inst-2", Some("other-pool"))
.await;
let ip = fetch_instance_ephemeral_ip(client, "other-pool-inst-2").await;
assert!(
ip.ip >= other_pool_range.first_address()
&& ip.ip <= other_pool_range.last_address(),
"Expected ephemeral IP to come from the other pool"
);

// and we can't create a second default pool for the silo
let identity = IdentityMetadataCreateParams {
name: "second-default-for-silo".parse().unwrap(),
Expand Down
6 changes: 4 additions & 2 deletions nexus/tests/integration_tests/ip_pools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ async fn test_ip_pool_basic_crud(cptestctx: &ControlPlaneTestContext) {
assert_eq!(ip_pools.len(), 1, "Expected to see default IP pool");

assert_eq!(ip_pools[0].identity.name, "default",);
assert_eq!(ip_pools[0].silo_id, None);
assert!(ip_pools[0].is_default);

// Verify 404 if the pool doesn't exist yet, both for creating or deleting
let error: HttpErrorResponseBody = NexusRequest::expect_failure(
Expand Down Expand Up @@ -288,9 +290,9 @@ async fn test_ip_pool_with_silo(cptestctx: &ControlPlaneTestContext) {
};
let created_pool = create_pool(client, &params).await;
assert_eq!(created_pool.identity.name, "p0");
assert!(created_pool.silo_id.is_some());

let silo_id = created_pool.silo_id.unwrap();
let silo_id =
created_pool.silo_id.expect("Expected pool to have a silo_id");

// now we'll create another IP pool using that silo ID
let params = IpPoolCreate {
Expand Down
2 changes: 1 addition & 1 deletion nexus/types/src/external_api/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ pub struct IpPoolCreate {
/// silo can draw from that pool.
pub silo: Option<NameOrId>,

/// Whether the IP pool is considered a default pool for its scope (fleet,
/// Whether the IP pool is considered a default pool for its scope (fleet
/// or silo). If a pool is marked default and is associated with a silo,
/// instances created in that silo will draw IPs from that pool unless
/// another pool is specified at instance create time.
Expand Down
1 change: 1 addition & 0 deletions nexus/types/src/external_api/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ pub struct IpPool {
#[serde(flatten)]
pub identity: IdentityMetadata,
pub silo_id: Option<Uuid>,
pub is_default: bool,
}

#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema)]
Expand Down
6 changes: 5 additions & 1 deletion openapi/nexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -10133,6 +10133,9 @@
"type": "string",
"format": "uuid"
},
"is_default": {
"type": "boolean"
},
"name": {
"description": "unique, mutable, user-controlled identifier for each resource",
"allOf": [
Expand Down Expand Up @@ -10160,6 +10163,7 @@
"required": [
"description",
"id",
"is_default",
"name",
"time_created",
"time_modified"
Expand All @@ -10173,7 +10177,7 @@
"type": "string"
},
"is_default": {
"description": "Whether the IP pool is considered a default pool for its scope (fleet, or silo). If a pool is marked default and is associated with a silo, instances created in that silo will draw IPs from that pool unless another pool is specified at instance create time.",
"description": "Whether the IP pool is considered a default pool for its scope (fleet or silo). If a pool is marked default and is associated with a silo, instances created in that silo will draw IPs from that pool unless another pool is specified at instance create time.",
"default": false,
"type": "boolean"
},
Expand Down