From 05dda98dfa2f78f0c45b69de4ba4088ac9307d47 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Sat, 31 Aug 2024 16:51:40 -0700 Subject: [PATCH] crud tests --- nexus/auth/src/authz/oso_generic.rs | 3 + nexus/db-model/src/internet_gateway.rs | 12 +- nexus/db-queries/src/db/datastore/vpc.rs | 117 ++++++---- .../src/policy_test/resource_builder.rs | 3 + nexus/db-queries/src/policy_test/resources.rs | 29 ++- nexus/db-queries/tests/output/authz-roles.out | 126 +++++++++++ nexus/external-api/output/nexus_tags.txt | 6 +- nexus/external-api/src/lib.rs | 6 +- nexus/src/app/internet_gateway.rs | 25 ++- nexus/test-utils/src/resource_helpers.rs | 159 ++++++++++++++ nexus/tests/integration_tests/endpoints.rs | 10 +- .../integration_tests/internet_gateway.rs | 203 ++++++++++++++++++ nexus/tests/integration_tests/mod.rs | 1 + nexus/types/src/external_api/params.rs | 6 +- openapi/nexus.json | 25 +-- schema/crdb/dbinit.sql | 10 + schema/crdb/internet-gateway/up05.sql | 4 + schema/crdb/internet-gateway/up06.sql | 4 + 18 files changed, 674 insertions(+), 75 deletions(-) create mode 100644 nexus/tests/integration_tests/internet_gateway.rs create mode 100644 schema/crdb/internet-gateway/up05.sql create mode 100644 schema/crdb/internet-gateway/up06.sql diff --git a/nexus/auth/src/authz/oso_generic.rs b/nexus/auth/src/authz/oso_generic.rs index 383a06e985c..acd74b2167f 100644 --- a/nexus/auth/src/authz/oso_generic.rs +++ b/nexus/auth/src/authz/oso_generic.rs @@ -130,6 +130,9 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result { InstanceNetworkInterface::init(), Vpc::init(), VpcRouter::init(), + InternetGateway::init(), + InternetGatewayIpPool::init(), + InternetGatewayIpAddress::init(), RouterRoute::init(), VpcSubnet::init(), FloatingIp::init(), diff --git a/nexus/db-model/src/internet_gateway.rs b/nexus/db-model/src/internet_gateway.rs index 9c5389bfcec..d885f7e97a7 100644 --- a/nexus/db-model/src/internet_gateway.rs +++ b/nexus/db-model/src/internet_gateway.rs @@ -7,6 +7,7 @@ use db_macros::Resource; use ipnetwork::IpNetwork; use nexus_types::external_api::{params, views}; use nexus_types::identity::Resource; +use omicron_common::api::external::IdentityMetadataCreateParams; use uuid::Uuid; #[derive(Queryable, Insertable, Clone, Debug, Selectable, Resource)] @@ -50,13 +51,14 @@ pub struct InternetGatewayIpPool { impl InternetGatewayIpPool { pub fn new( - pool_id: Uuid, + id: Uuid, + ip_pool_id: Uuid, internet_gateway_id: Uuid, - params: params::InternetGatewayIpPoolCreate, + identity: IdentityMetadataCreateParams, ) -> Self { - let identity = - InternetGatewayIpPoolIdentity::new(pool_id, params.identity); - Self { identity, internet_gateway_id, ip_pool_id: params.ip_pool_id } + let identity = InternetGatewayIpPoolIdentity::new(id, identity); + //InternetGatewayIpPoolIdentity::new(pool_id, params.identity); + Self { identity, internet_gateway_id, ip_pool_id } } } diff --git a/nexus/db-queries/src/db/datastore/vpc.rs b/nexus/db-queries/src/db/datastore/vpc.rs index cbf70adec31..148b5f13150 100644 --- a/nexus/db-queries/src/db/datastore/vpc.rs +++ b/nexus/db-queries/src/db/datastore/vpc.rs @@ -1114,6 +1114,50 @@ impl DataStore { .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } + pub async fn internet_gateway_has_ip_pools( + &self, + opctx: &OpContext, + authz_igw: &authz::InternetGateway, + ) -> LookupResult { + opctx.authorize(authz::Action::ListChildren, authz_igw).await?; + + use db::schema::internet_gateway_ip_pool::dsl; + let result = dsl::internet_gateway_ip_pool + .filter(dsl::time_deleted.is_null()) + .filter(dsl::internet_gateway_id.eq(authz_igw.id())) + .select(InternetGatewayIpPool::as_select()) + .limit(1) + .load_async::( + &*self.pool_connection_authorized(opctx).await?, + ) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + Ok(!result.is_empty()) + } + + pub async fn internet_gateway_has_ip_addresses( + &self, + opctx: &OpContext, + authz_igw: &authz::InternetGateway, + ) -> LookupResult { + opctx.authorize(authz::Action::ListChildren, authz_igw).await?; + + use db::schema::internet_gateway_ip_address::dsl; + let result = dsl::internet_gateway_ip_address + .filter(dsl::time_deleted.is_null()) + .filter(dsl::internet_gateway_id.eq(authz_igw.id())) + .select(InternetGatewayIpAddress::as_select()) + .limit(1) + .load_async::( + &*self.pool_connection_authorized(opctx).await?, + ) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + Ok(!result.is_empty()) + } + pub async fn internet_gateway_list_ip_pools( &self, opctx: &OpContext, @@ -1301,44 +1345,45 @@ impl DataStore { ) -> DeleteResult { opctx.authorize(authz::Action::Delete, authz_igw).await?; - use db::schema::internet_gateway::dsl; - let now = Utc::now(); - diesel::update(dsl::internet_gateway) - .filter(dsl::time_deleted.is_null()) - .filter(dsl::id.eq(authz_igw.id())) - .set(dsl::time_deleted.eq(now)) - .execute_async(&*self.pool_connection_authorized(opctx).await?) - .await - .map_err(|e| { - public_error_from_diesel( - e, - ErrorHandler::NotFoundByResource(authz_igw), - ) - })?; - - // Delete ip pool associations - use db::schema::internet_gateway_ip_pool::dsl as pool; - let now = Utc::now(); - diesel::update(pool::internet_gateway_ip_pool) - .filter(pool::time_deleted.is_null()) - .filter(pool::internet_gateway_id.eq(authz_igw.id())) - .set(pool::time_deleted.eq(now)) - .execute_async(&*self.pool_connection_authorized(opctx).await?) - .await - .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + let conn = self.pool_connection_authorized(opctx).await?; - // Delete ip address associations - use db::schema::internet_gateway_ip_address::dsl as addr; - let now = Utc::now(); - diesel::update(addr::internet_gateway_ip_address) - .filter(addr::time_deleted.is_null()) - .filter(addr::internet_gateway_id.eq(authz_igw.id())) - .set(addr::time_deleted.eq(now)) - .execute_async(&*self.pool_connection_authorized(opctx).await?) + self.transaction_retry_wrapper("vpc_delete_internet_gateway") + .transaction(&conn, |conn| { + async move { + use db::schema::internet_gateway::dsl; + let now = Utc::now(); + diesel::update(dsl::internet_gateway) + .filter(dsl::time_deleted.is_null()) + .filter(dsl::id.eq(authz_igw.id())) + .set(dsl::time_deleted.eq(now)) + .execute_async(&conn) + .await?; + + // Delete ip pool associations + use db::schema::internet_gateway_ip_pool::dsl as pool; + let now = Utc::now(); + diesel::update(pool::internet_gateway_ip_pool) + .filter(pool::time_deleted.is_null()) + .filter(pool::internet_gateway_id.eq(authz_igw.id())) + .set(pool::time_deleted.eq(now)) + .execute_async(&conn) + .await?; + + // Delete ip address associations + use db::schema::internet_gateway_ip_address::dsl as addr; + let now = Utc::now(); + diesel::update(addr::internet_gateway_ip_address) + .filter(addr::time_deleted.is_null()) + .filter(addr::internet_gateway_id.eq(authz_igw.id())) + .set(addr::time_deleted.eq(now)) + .execute_async(&conn) + .await?; + + Ok(()) + } + }) .await - .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; - - Ok(()) + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } pub async fn vpc_update_router( diff --git a/nexus/db-queries/src/policy_test/resource_builder.rs b/nexus/db-queries/src/policy_test/resource_builder.rs index 3d09b2ab2d4..304b9703778 100644 --- a/nexus/db-queries/src/policy_test/resource_builder.rs +++ b/nexus/db-queries/src/policy_test/resource_builder.rs @@ -254,6 +254,9 @@ impl_dyn_authorized_resource_for_resource!(authz::IdentityProvider); impl_dyn_authorized_resource_for_resource!(authz::Image); impl_dyn_authorized_resource_for_resource!(authz::Instance); impl_dyn_authorized_resource_for_resource!(authz::InstanceNetworkInterface); +impl_dyn_authorized_resource_for_resource!(authz::InternetGateway); +impl_dyn_authorized_resource_for_resource!(authz::InternetGatewayIpAddress); +impl_dyn_authorized_resource_for_resource!(authz::InternetGatewayIpPool); impl_dyn_authorized_resource_for_resource!(authz::LoopbackAddress); impl_dyn_authorized_resource_for_resource!(authz::Rack); impl_dyn_authorized_resource_for_resource!(authz::PhysicalDisk); diff --git a/nexus/db-queries/src/policy_test/resources.rs b/nexus/db-queries/src/policy_test/resources.rs index 478fa169ffb..1a5ac77f534 100644 --- a/nexus/db-queries/src/policy_test/resources.rs +++ b/nexus/db-queries/src/policy_test/resources.rs @@ -318,7 +318,7 @@ async fn make_project( builder.new_resource(vpc1.clone()); // Test a resource nested two levels below Project builder.new_resource(authz::VpcSubnet::new( - vpc1, + vpc1.clone(), Uuid::new_v4(), LookupType::ByName(format!("{}-subnet1", vpc1_name)), )); @@ -342,6 +342,28 @@ async fn make_project( Uuid::new_v4(), LookupType::ByName(floating_ip_name), )); + + let igw_name = format!("{project_name}-igw1"); + let igw = authz::InternetGateway::new( + vpc1.clone(), + Uuid::new_v4(), + LookupType::ByName(igw_name.clone()), + ); + builder.new_resource(igw.clone()); + + let igw_ip_pool_name = format!("{igw_name}-pool1"); + builder.new_resource(authz::InternetGatewayIpPool::new( + igw.clone(), + Uuid::new_v4(), + LookupType::ByName(igw_ip_pool_name), + )); + + let igw_ip_address_name = format!("{igw_name}-address1"); + builder.new_resource(authz::InternetGatewayIpAddress::new( + igw.clone(), + Uuid::new_v4(), + LookupType::ByName(igw_ip_address_name), + )); } /// Returns the set of authz classes exempted from the coverage test @@ -378,6 +400,11 @@ pub fn exempted_authz_classes() -> BTreeSet { // to this list, modify `make_resources()` to test it instead. This // should be pretty straightforward in most cases. Adding a new // class to this list makes it harder to catch security flaws! + // + // NOTE: in order to add a resource to the aforementioned tests, you + // need to call the macro `impl_dyn_authorized_resource_for_resource!` + // for the type you are implementing the test for. See + // resource_builder.rs for examples. authz::IpPool::get_polar_class(), authz::VpcRouter::get_polar_class(), authz::RouterRoute::get_polar_class(), diff --git a/nexus/db-queries/tests/output/authz-roles.out b/nexus/db-queries/tests/output/authz-roles.out index 41a1ded3b4c..36403fa700b 100644 --- a/nexus/db-queries/tests/output/authz-roles.out +++ b/nexus/db-queries/tests/output/authz-roles.out @@ -404,6 +404,48 @@ resource: FloatingIp "silo1-proj1-fip1" silo1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! +resource: InternetGateway "silo1-proj1-igw1" + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-proj1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + +resource: InternetGatewayIpPool "silo1-proj1-igw1-pool1" + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-proj1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + +resource: InternetGatewayIpAddress "silo1-proj1-igw1-address1" + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-proj1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + resource: Project "silo1-proj2" USER Q R LC RP M MP CC D @@ -530,6 +572,48 @@ resource: FloatingIp "silo1-proj2-fip1" silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! +resource: InternetGateway "silo1-proj2-igw1" + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + +resource: InternetGatewayIpPool "silo1-proj2-igw1-pool1" + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + +resource: InternetGatewayIpAddress "silo1-proj2-igw1-address1" + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + resource: Silo "silo2" USER Q R LC RP M MP CC D @@ -824,6 +908,48 @@ resource: FloatingIp "silo2-proj1-fip1" silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! +resource: InternetGateway "silo2-proj1-igw1" + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + +resource: InternetGatewayIpPool "silo2-proj1-igw1-pool1" + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + +resource: InternetGatewayIpAddress "silo2-proj1-igw1-address1" + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + resource: Rack id "c037e882-8b6d-c8b5-bef4-97e848eb0a50" USER Q R LC RP M MP CC D diff --git a/nexus/external-api/output/nexus_tags.txt b/nexus/external-api/output/nexus_tags.txt index 75bd9a016bf..22ebae8e558 100644 --- a/nexus/external-api/output/nexus_tags.txt +++ b/nexus/external-api/output/nexus_tags.txt @@ -230,9 +230,9 @@ API operations found with tag "vpcs" OPERATION ID METHOD URL PATH internet_gateway_create POST /v1/internet-gateways internet_gateway_delete DELETE /v1/internet-gateways/{gateway} -internet_gateway_ip_address_create POST /v1/internet-gateway-ip-addrs -internet_gateway_ip_address_delete DELETE /v1/internet-gateway-ip-addrs/{address} -internet_gateway_ip_address_list GET /v1/internet-gateway-ip-addrs +internet_gateway_ip_address_create POST /v1/internet-gateway-ip-addresses +internet_gateway_ip_address_delete DELETE /v1/internet-gateway-ip-addresses/{address} +internet_gateway_ip_address_list GET /v1/internet-gateway-ip-addresses internet_gateway_ip_pool_create POST /v1/internet-gateway-ip-pools internet_gateway_ip_pool_delete DELETE /v1/internet-gateway-ip-pools/{pool} internet_gateway_ip_pool_list GET /v1/internet-gateway-ip-pools diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index bab876ae7cd..10f9bc0a2ac 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -2283,7 +2283,7 @@ pub trait NexusExternalApi { /// List addresses attached to an internet gateway. #[endpoint { method = GET, - path = "/v1/internet-gateway-ip-addrs", + path = "/v1/internet-gateway-ip-addresses", tags = ["vpcs"], }] async fn internet_gateway_ip_address_list( @@ -2299,7 +2299,7 @@ pub trait NexusExternalApi { /// Attach ip pool to internet gateway #[endpoint { method = POST, - path = "/v1/internet-gateway-ip-addrs", + path = "/v1/internet-gateway-ip-addresses", tags = ["vpcs"], }] async fn internet_gateway_ip_address_create( @@ -2311,7 +2311,7 @@ pub trait NexusExternalApi { /// Detach ip pool from internet gateway #[endpoint { method = DELETE, - path = "/v1/internet-gateway-ip-addrs/{address}", + path = "/v1/internet-gateway-ip-addresses/{address}", tags = ["vpcs"], }] async fn internet_gateway_ip_address_delete( diff --git a/nexus/src/app/internet_gateway.rs b/nexus/src/app/internet_gateway.rs index dd11084d1eb..8e615ebcfa3 100644 --- a/nexus/src/app/internet_gateway.rs +++ b/nexus/src/app/internet_gateway.rs @@ -97,11 +97,12 @@ impl super::Nexus { opctx: &OpContext, lookup: &lookup::InternetGateway<'_>, ) -> DeleteResult { - let (.., authz_router, _db_igw) = + let (.., authz_igw, _db_igw) = lookup.fetch_for(authz::Action::Delete).await?; + let out = self .db_datastore - .vpc_delete_internet_gateway(opctx, &authz_router) + .vpc_delete_internet_gateway(opctx, &authz_igw) .await?; self.vpc_needed_notify_sleds(); @@ -172,14 +173,28 @@ impl super::Nexus { lookup: &lookup::InternetGateway<'_>, params: ¶ms::InternetGatewayIpPoolCreate, ) -> CreateResult { - let (.., authz_igw, _db_pool) = + let (.., authz_igw, _) = lookup.fetch_for(authz::Action::CreateChild).await?; + let ip_pool_id = match params.ip_pool { + NameOrId::Id(id) => id, + NameOrId::Name(ref name) => { + let name = name.clone().into(); + LookupPath::new(opctx, &self.db_datastore) + .ip_pool_name(&name) + .fetch() + .await? + .0 + .id() + } + }; + let id = Uuid::new_v4(); let route = db::model::InternetGatewayIpPool::new( id, + ip_pool_id, authz_igw.id(), - params.clone(), + params.identity.clone(), ); let route = self .db_datastore @@ -274,7 +289,7 @@ impl super::Nexus { lookup: &lookup::InternetGateway<'_>, params: ¶ms::InternetGatewayIpAddressCreate, ) -> CreateResult { - let (.., authz_igw, _db_addr) = + let (.., authz_igw, _) = lookup.fetch_for(authz::Action::CreateChild).await?; let id = Uuid::new_v4(); diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index 14180459aba..0eb311e0ec5 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -22,6 +22,9 @@ use nexus_types::external_api::shared::IpRange; use nexus_types::external_api::views; use nexus_types::external_api::views::Certificate; use nexus_types::external_api::views::FloatingIp; +use nexus_types::external_api::views::InternetGateway; +use nexus_types::external_api::views::InternetGatewayIpAddress; +use nexus_types::external_api::views::InternetGatewayIpPool; use nexus_types::external_api::views::IpPool; use nexus_types::external_api::views::IpPoolRange; use nexus_types::external_api::views::User; @@ -35,6 +38,7 @@ use omicron_common::api::external::Error; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::Instance; use omicron_common::api::external::InstanceCpuCount; +use omicron_common::api::external::Name; use omicron_common::api::external::NameOrId; use omicron_common::api::external::RouteDestination; use omicron_common::api::external::RouteTarget; @@ -712,6 +716,161 @@ pub async fn create_route_with_error( .unwrap() } +pub async fn create_internet_gateway( + client: &ClientTestContext, + project_name: &str, + vpc_name: &str, + internet_gateway_name: &str, +) -> InternetGateway { + NexusRequest::objects_post( + &client, + format!( + "/v1/internet-gateways?project={}&vpc={}", + &project_name, &vpc_name + ) + .as_str(), + ¶ms::VpcRouterCreate { + identity: IdentityMetadataCreateParams { + name: internet_gateway_name.parse().unwrap(), + description: String::from("internet gateway description"), + }, + }, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap() +} + +pub async fn delete_internet_gateway( + client: &ClientTestContext, + project_name: &str, + vpc_name: &str, + internet_gateway_name: &str, +) { + NexusRequest::object_delete( + &client, + format!( + "/v1/internet-gateways/{}?project={}&vpc={}", + &internet_gateway_name, &project_name, &vpc_name + ) + .as_str(), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(); +} + +pub async fn attach_ip_pool_to_igw( + client: &ClientTestContext, + project_name: &str, + vpc_name: &str, + igw_name: &str, + ip_pool_name: &str, + attachment_name: &str, +) -> InternetGatewayIpPool { + let url = format!( + "/v1/internet-gateway-ip-pools?project={}&vpc={}&gateway={}", + project_name, vpc_name, igw_name, + ); + + let gateway: Name = igw_name.parse().unwrap(); + let ip_pool: Name = ip_pool_name.parse().unwrap(); + NexusRequest::objects_post( + &client, + url.as_str(), + ¶ms::InternetGatewayIpPoolCreate { + identity: IdentityMetadataCreateParams { + name: attachment_name.parse().unwrap(), + description: String::from("attached pool descriptoion"), + }, + gateway: NameOrId::Name(gateway), + ip_pool: NameOrId::Name(ip_pool), + }, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap() +} + +pub async fn detach_ip_pool_from_igw( + client: &ClientTestContext, + project_name: &str, + vpc_name: &str, + igw_name: &str, + ip_pool_name: &str, +) { + let url = format!( + "/v1/internet-gateway-ip-pools/{}?project={}&vpc={}&gateway={}", + ip_pool_name, project_name, vpc_name, igw_name, + ); + + NexusRequest::object_delete(&client, url.as_str()) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(); +} + +pub async fn attach_ip_address_to_igw( + client: &ClientTestContext, + project_name: &str, + vpc_name: &str, + igw_name: &str, + address: IpAddr, + attachment_name: &str, +) -> InternetGatewayIpAddress { + let url = format!( + "/v1/internet-gateway-ip-addresses?project={}&vpc={}&gateway={}", + project_name, vpc_name, igw_name, + ); + + let gateway: Name = igw_name.parse().unwrap(); + NexusRequest::objects_post( + &client, + url.as_str(), + ¶ms::InternetGatewayIpAddressCreate { + identity: IdentityMetadataCreateParams { + name: attachment_name.parse().unwrap(), + description: String::from("attached pool descriptoion"), + }, + gateway: NameOrId::Name(gateway), + address, + }, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap() +} + +pub async fn detach_ip_address_from_igw( + client: &ClientTestContext, + project_name: &str, + vpc_name: &str, + igw_name: &str, + attachment_name: &str, +) { + let url = format!( + "/v1/internet-gateway-ip-addresses/{}?project={}&vpc={}&gateway={}", + attachment_name, project_name, vpc_name, igw_name, + ); + + NexusRequest::object_delete(&client, url.as_str()) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(); +} + pub async fn assert_ip_pool_utilization( client: &ClientTestContext, pool_name: &str, diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 06d5dd16b71..830109ac116 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -282,8 +282,8 @@ pub static DEMO_INTERNET_GATEWAY_IP_POOL_CREATE: Lazy< name: DEMO_INTERNET_GATEWAY_NAME.clone(), description: String::from(""), }, - ip_pool_id: uuid::Uuid::new_v4(), - gateway_id: uuid::Uuid::new_v4(), + ip_pool: NameOrId::Id(uuid::Uuid::new_v4()), + gateway: NameOrId::Id(uuid::Uuid::new_v4()), }); pub static DEMO_INTERNET_GATEWAY_IP_ADDRESS_CREATE: Lazy< params::InternetGatewayIpAddressCreate, @@ -292,7 +292,7 @@ pub static DEMO_INTERNET_GATEWAY_IP_ADDRESS_CREATE: Lazy< name: DEMO_INTERNET_GATEWAY_NAME.clone(), description: String::from(""), }, - gateway_id: uuid::Uuid::new_v4(), + gateway: NameOrId::Id(uuid::Uuid::new_v4()), address: IpAddr::V4(Ipv4Addr::UNSPECIFIED), }); pub static DEMO_INTERNET_GATEWAY_IP_POOLS_URL: Lazy = Lazy::new(|| { @@ -303,7 +303,7 @@ pub static DEMO_INTERNET_GATEWAY_IP_POOLS_URL: Lazy = Lazy::new(|| { }); pub static DEMO_INTERNET_GATEWAY_IP_ADDRS_URL: Lazy = Lazy::new(|| { format!( - "/v1/internet-gateway-ip-addrs?project={}&vpc={}&gateway={}", + "/v1/internet-gateway-ip-addresses?project={}&vpc={}&gateway={}", *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_INTERNET_GATEWAY_NAME, ) }); @@ -322,7 +322,7 @@ pub static DEMO_INTERNET_GATEWAY_IP_POOL_URL: Lazy = Lazy::new(|| { }); pub static DEMO_INTERNET_GATEWAY_IP_ADDR_URL: Lazy = Lazy::new(|| { format!( - "/v1/internet-gateway-ip-addrs/{}?project={}&vpc={}&gateway={}", + "/v1/internet-gateway-ip-addresses/{}?project={}&vpc={}&gateway={}", *DEMO_INTERNET_GATEWAY_IP_ADDRESS_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, diff --git a/nexus/tests/integration_tests/internet_gateway.rs b/nexus/tests/integration_tests/internet_gateway.rs new file mode 100644 index 00000000000..8d31be1bb16 --- /dev/null +++ b/nexus/tests/integration_tests/internet_gateway.rs @@ -0,0 +1,203 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use dropshot::test_util::ClientTestContext; +use http::{Method, StatusCode}; +use nexus_test_utils::{ + http_testing::{AuthnMode, NexusRequest}, + resource_helpers::{ + attach_ip_address_to_igw, attach_ip_pool_to_igw, + create_internet_gateway, create_ip_pool, create_project, create_vpc, + delete_internet_gateway, detach_ip_address_from_igw, + detach_ip_pool_from_igw, objects_list_page_authz, + }, +}; +use nexus_test_utils_macros::nexus_test; +use nexus_types::external_api::views::{ + InternetGateway, InternetGatewayIpAddress, InternetGatewayIpPool, +}; + +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + +#[nexus_test] +async fn test_internet_gateway_basic_crud(ctx: &ControlPlaneTestContext) { + const PROJECT_NAME: &str = "delta-quadrant"; + const VPC_NAME: &str = "dominion"; + const IGW_NAME: &str = "wormhole"; + const IP_POOL_NAME: &str = "ds9"; + const IP_POOL_ATTACHMENT_NAME: &str = "runabout"; + const IP_ADDRESS_ATTACHMENT_NAME: &str = "defiant"; + const IP_ADDRESS_ATTACHMENT: &str = "198.51.100.47"; + + let c = &ctx.external_client; + + // create a project and vpc to test with + let _proj = create_project(&c, PROJECT_NAME).await; + let _vpc = create_vpc(&c, PROJECT_NAME, VPC_NAME).await; + let _pool = create_ip_pool(c, IP_POOL_NAME, None).await; + + // should start with zero gateways + let igws = list_internet_gateways(c, PROJECT_NAME, VPC_NAME).await; + assert_eq!(igws.len(), 0, "should start with zero internet gateways"); + + // check 404 response + let url = format!( + "/v1/internet-gateways/{}?project={}&vpc={}", + IGW_NAME, PROJECT_NAME, VPC_NAME + ); + let error: dropshot::HttpErrorResponseBody = NexusRequest::expect_failure( + c, + StatusCode::NOT_FOUND, + Method::GET, + &url, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + assert_eq!( + error.message, + format!("not found: internet-gateway with name \"{IGW_NAME}\"") + ); + + // create an internet gateway + let gw = create_internet_gateway(c, PROJECT_NAME, VPC_NAME, IGW_NAME).await; + let igws = list_internet_gateways(c, PROJECT_NAME, VPC_NAME).await; + assert_eq!(igws.len(), 1, "should now have one internet gateway"); + + // should be able to get the gateway just created + let same_gw: InternetGateway = NexusRequest::object_get(c, &url) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + assert_eq!(&gw.identity, &same_gw.identity); + + // a new igw should have zero ip pools + let igw_pools = + list_internet_gateway_ip_pools(c, PROJECT_NAME, VPC_NAME, IGW_NAME) + .await; + assert_eq!(igw_pools.len(), 0, "a new igw should have no pools"); + + // a new igw should have zero ip addresses + let igw_addrs = + list_internet_gateway_ip_addresses(c, PROJECT_NAME, VPC_NAME, IGW_NAME) + .await; + assert_eq!(igw_addrs.len(), 0, "a new igw should have no addresses"); + + // attach an ip pool + attach_ip_pool_to_igw( + c, + PROJECT_NAME, + VPC_NAME, + IGW_NAME, + IP_POOL_NAME, + IP_POOL_ATTACHMENT_NAME, + ) + .await; + let igw_pools = + list_internet_gateway_ip_pools(c, PROJECT_NAME, VPC_NAME, IGW_NAME) + .await; + assert_eq!(igw_pools.len(), 1, "should now have one attached ip pool"); + + // attach an ip address + attach_ip_address_to_igw( + c, + PROJECT_NAME, + VPC_NAME, + IGW_NAME, + IP_ADDRESS_ATTACHMENT.parse().unwrap(), + IP_ADDRESS_ATTACHMENT_NAME, + ) + .await; + let igw_pools = + list_internet_gateway_ip_addresses(c, PROJECT_NAME, VPC_NAME, IGW_NAME) + .await; + assert_eq!(igw_pools.len(), 1, "should now have one attached address"); + + // detach an ip pool + detach_ip_pool_from_igw( + c, + PROJECT_NAME, + VPC_NAME, + IGW_NAME, + IP_POOL_ATTACHMENT_NAME, + ) + .await; + let igw_addrs = + list_internet_gateway_ip_pools(c, PROJECT_NAME, VPC_NAME, IGW_NAME) + .await; + assert_eq!(igw_addrs.len(), 0, "should now have zero attached ip pool"); + + // detach an ip address + detach_ip_address_from_igw( + c, + PROJECT_NAME, + VPC_NAME, + IGW_NAME, + IP_ADDRESS_ATTACHMENT_NAME, + ) + .await; + let igw_addrs = + list_internet_gateway_ip_addresses(c, PROJECT_NAME, VPC_NAME, IGW_NAME) + .await; + assert_eq!( + igw_addrs.len(), + 0, + "should now have zero attached ip addresses" + ); + + // delete internet gateay + delete_internet_gateway(c, PROJECT_NAME, VPC_NAME, IGW_NAME).await; + let igws = list_internet_gateways(c, PROJECT_NAME, VPC_NAME).await; + assert_eq!(igws.len(), 0, "should now have zero internet gateways"); +} + +async fn list_internet_gateways( + client: &ClientTestContext, + project_name: &str, + vpc_name: &str, +) -> Vec { + let url = format!( + "/v1/internet-gateways?project={}&vpc={}", + project_name, vpc_name + ); + let out = objects_list_page_authz::(client, &url).await; + out.items +} + +async fn list_internet_gateway_ip_pools( + client: &ClientTestContext, + project_name: &str, + vpc_name: &str, + igw_name: &str, +) -> Vec { + let url = format!( + "/v1/internet-gateway-ip-pools?project={}&vpc={}&gateway={}", + project_name, vpc_name, igw_name, + ); + let out = + objects_list_page_authz::(client, &url).await; + out.items +} + +async fn list_internet_gateway_ip_addresses( + client: &ClientTestContext, + project_name: &str, + vpc_name: &str, + igw_name: &str, +) -> Vec { + let url = format!( + "/v1/internet-gateway-ip-addresses?project={}&vpc={}&gateway={}", + project_name, vpc_name, igw_name, + ); + let out = + objects_list_page_authz::(client, &url).await; + out.items +} diff --git a/nexus/tests/integration_tests/mod.rs b/nexus/tests/integration_tests/mod.rs index fdf14dbd079..99115b84883 100644 --- a/nexus/tests/integration_tests/mod.rs +++ b/nexus/tests/integration_tests/mod.rs @@ -19,6 +19,7 @@ mod host_phase1_updater; mod images; mod initialization; mod instances; +mod internet_gateway; mod ip_pools; mod metrics; mod oximeter; diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 645ab67f2a4..f4c0da4fc23 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -1311,15 +1311,15 @@ pub struct InternetGatewayCreate { pub struct InternetGatewayIpPoolCreate { #[serde(flatten)] pub identity: IdentityMetadataCreateParams, - pub gateway_id: Uuid, - pub ip_pool_id: Uuid, + pub gateway: NameOrId, + pub ip_pool: NameOrId, } #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct InternetGatewayIpAddressCreate { #[serde(flatten)] pub identity: IdentityMetadataCreateParams, - pub gateway_id: Uuid, + pub gateway: NameOrId, pub address: IpAddr, } diff --git a/openapi/nexus.json b/openapi/nexus.json index f259b4f4ebc..384c5fd5e47 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -2622,7 +2622,7 @@ } } }, - "/v1/internet-gateway-ip-addrs": { + "/v1/internet-gateway-ip-addresses": { "get": { "tags": [ "vpcs" @@ -2769,7 +2769,7 @@ } } }, - "/v1/internet-gateway-ip-addrs/{address}": { + "/v1/internet-gateway-ip-addresses/{address}": { "delete": { "tags": [ "vpcs" @@ -16447,9 +16447,8 @@ "description": { "type": "string" }, - "gateway_id": { - "type": "string", - "format": "uuid" + "gateway": { + "$ref": "#/components/schemas/NameOrId" }, "name": { "$ref": "#/components/schemas/Name" @@ -16458,7 +16457,7 @@ "required": [ "address", "description", - "gateway_id", + "gateway", "name" ] }, @@ -16542,13 +16541,11 @@ "description": { "type": "string" }, - "gateway_id": { - "type": "string", - "format": "uuid" + "gateway": { + "$ref": "#/components/schemas/NameOrId" }, - "ip_pool_id": { - "type": "string", - "format": "uuid" + "ip_pool": { + "$ref": "#/components/schemas/NameOrId" }, "name": { "$ref": "#/components/schemas/Name" @@ -16556,8 +16553,8 @@ }, "required": [ "description", - "gateway_id", - "ip_pool_id", + "gateway", + "ip_pool", "name" ] }, diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index daf56240568..eddfc9b895a 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -1749,6 +1749,11 @@ CREATE TABLE IF NOT EXISTS omicron.public.internet_gateway_ip_pool ( ip_pool_id UUID ); +CREATE UNIQUE INDEX IF NOT EXISTS lookup_internet_gateway_ip_pool_by_igw_id ON omicron.public.internet_gateway_ip_pool ( + internet_gateway_id +) WHERE + time_deleted IS NULL; + CREATE TABLE IF NOT EXISTS omicron.public.internet_gateway_ip_address ( id UUID PRIMARY KEY, name STRING(63) NOT NULL, @@ -1760,6 +1765,11 @@ CREATE TABLE IF NOT EXISTS omicron.public.internet_gateway_ip_address ( address INET ); +CREATE UNIQUE INDEX IF NOT EXISTS lookup_internet_gateway_ip_address_by_igw_id ON omicron.public.internet_gateway_ip_address ( + internet_gateway_id +) WHERE + time_deleted IS NULL; + /* * An IP Pool, a collection of zero or more IP ranges for external IPs. diff --git a/schema/crdb/internet-gateway/up05.sql b/schema/crdb/internet-gateway/up05.sql new file mode 100644 index 00000000000..f82cb8fa7a2 --- /dev/null +++ b/schema/crdb/internet-gateway/up05.sql @@ -0,0 +1,4 @@ +CREATE UNIQUE INDEX IF NOT EXISTS lookup_internet_gateway_ip_pool_by_igw_id ON omicron.public.internet_gateway_ip_pool ( + internet_gateway_id +) WHERE + time_deleted IS NULL; diff --git a/schema/crdb/internet-gateway/up06.sql b/schema/crdb/internet-gateway/up06.sql new file mode 100644 index 00000000000..4e37708f1d5 --- /dev/null +++ b/schema/crdb/internet-gateway/up06.sql @@ -0,0 +1,4 @@ +CREATE UNIQUE INDEX IF NOT EXISTS lookup_internet_gateway_ip_address_by_igw_id ON omicron.public.internet_gateway_ip_address ( + internet_gateway_id +) WHERE + time_deleted IS NULL;