From 8d51dbb8a05ea6e45d86bed5409b2423efc9100a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 13 Dec 2022 10:08:34 -0500 Subject: [PATCH 01/45] Revamp selectors (again), convert organizations --- nexus/db-model/src/name.rs | 2 + nexus/src/app/instance.rs | 60 +-- nexus/src/app/organization.rs | 76 ++- nexus/src/app/project.rs | 40 +- nexus/src/external_api/http_entrypoints.rs | 512 ++++++++++++++++----- nexus/types/src/external_api/params.rs | 39 +- 6 files changed, 472 insertions(+), 257 deletions(-) diff --git a/nexus/db-model/src/name.rs b/nexus/db-model/src/name.rs index 96603530333..11d3d6574c6 100644 --- a/nexus/db-model/src/name.rs +++ b/nexus/db-model/src/name.rs @@ -37,6 +37,8 @@ use serde::{Deserialize, Serialize}; #[display("{0}")] pub struct Name(pub external::Name); +// impl Into for Name {} + NewtypeFrom! { () pub struct Name(external::Name); } NewtypeDeref! { () pub struct Name(external::Name); } diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index ec4f0f574ac..bab5e77cbd2 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -62,56 +62,26 @@ impl super::Nexus { instance_selector: &'a params::InstanceSelector, ) -> LookupResult> { match instance_selector { - params::InstanceSelector { instance: NameOrId::Id(id), .. } => { - // TODO: 400 if project or organization are present + params::InstanceSelector(NameOrId::Id(id), ..) => { let instance = LookupPath::new(opctx, &self.db_datastore).instance_id(*id); Ok(instance) } - params::InstanceSelector { - instance: NameOrId::Name(instance_name), - project: Some(NameOrId::Id(project_id)), - .. - } => { - // TODO: 400 if organization is present - let instance = LookupPath::new(opctx, &self.db_datastore) - .project_id(*project_id) - .instance_name(Name::ref_cast(instance_name)); - Ok(instance) - } - params::InstanceSelector { - instance: NameOrId::Name(instance_name), - project: Some(NameOrId::Name(project_name)), - organization: Some(NameOrId::Id(organization_id)), - } => { - let instance = LookupPath::new(opctx, &self.db_datastore) - .organization_id(*organization_id) - .project_name(Name::ref_cast(project_name)) - .instance_name(Name::ref_cast(instance_name)); - Ok(instance) - } - params::InstanceSelector { - instance: NameOrId::Name(instance_name), - project: Some(NameOrId::Name(project_name)), - organization: Some(NameOrId::Name(organization_name)), - } => { - let instance = LookupPath::new(opctx, &self.db_datastore) - .organization_name(Name::ref_cast(organization_name)) - .project_name(Name::ref_cast(project_name)) - .instance_name(Name::ref_cast(instance_name)); - Ok(instance) + params::InstanceSelector( + NameOrId::Name(name), + project_selector, + ) => { + if let Some(project) = project_selector { + let instance = self + .project_lookup(opctx, project)? + .instance_name(Name::ref_cast(name)); + Ok(instance) + } else { + Err(Error::InvalidRequest { + message: "Unable to resolve instance by name without instance".to_string(), + }) + } } - // TODO: Add a better error message - _ => Err(Error::InvalidRequest { - message: " - Unable to resolve instance. Expected one of - - instance: Uuid - - instance: Name, project: Uuid - - instance: Name, project: Name, organization: Uuid - - instance: Name, project: Name, organization: Name - " - .to_string(), - }), } } diff --git a/nexus/src/app/organization.rs b/nexus/src/app/organization.rs index d7783d2a7a6..a1fae4997db 100644 --- a/nexus/src/app/organization.rs +++ b/nexus/src/app/organization.rs @@ -7,6 +7,7 @@ use crate::authz; use crate::context::OpContext; use crate::db; +use crate::db::lookup; use crate::db::lookup::LookupPath; use crate::db::model::Name; use crate::external_api::params; @@ -18,10 +19,30 @@ use omicron_common::api::external::DeleteResult; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; +use ref_cast::RefCast; use uuid::Uuid; impl super::Nexus { + pub fn organization_lookup<'a>( + &'a self, + opctx: &'a OpContext, + organization_selector: &'a params::OrganizationSelector, + ) -> LookupResult> { + match organization_selector { + params::OrganizationSelector(NameOrId::Id(id)) => { + let organization = LookupPath::new(opctx, &self.db_datastore) + .organization_id(*id); + Ok(organization) + } + params::OrganizationSelector(NameOrId::Name(name)) => { + let organization = LookupPath::new(opctx, &self.db_datastore) + .organization_name(Name::ref_cast(name)); + Ok(organization) + } + } + } pub async fn organization_create( &self, opctx: &OpContext, @@ -30,30 +51,6 @@ impl super::Nexus { self.db_datastore.organization_create(opctx, new_organization).await } - pub async fn organization_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - ) -> LookupResult { - let (.., db_organization) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .fetch() - .await?; - Ok(db_organization) - } - - pub async fn organization_fetch_by_id( - &self, - opctx: &OpContext, - organization_id: &Uuid, - ) -> LookupResult { - let (.., db_organization) = LookupPath::new(opctx, &self.db_datastore) - .organization_id(*organization_id) - .fetch() - .await?; - Ok(db_organization) - } - pub async fn organizations_list_by_name( &self, opctx: &OpContext, @@ -73,27 +70,20 @@ impl super::Nexus { pub async fn organization_delete( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, ) -> DeleteResult { - let (.., authz_org, db_org) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .fetch() - .await?; + let (.., authz_org, db_org) = organization_lookup.fetch().await?; self.db_datastore.organization_delete(opctx, &authz_org, &db_org).await } pub async fn organization_update( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, new_params: ¶ms::OrganizationUpdate, ) -> UpdateResult { let (.., authz_organization) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::Modify) - .await?; + organization_lookup.lookup_for(authz::Action::Modify).await?; self.db_datastore .organization_update( opctx, @@ -108,12 +98,10 @@ impl super::Nexus { pub async fn organization_fetch_policy( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, ) -> LookupResult> { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::ReadPolicy) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::ReadPolicy).await?; let role_assignments = self .db_datastore .role_assignment_fetch_visible(opctx, &authz_org) @@ -128,13 +116,11 @@ impl super::Nexus { pub async fn organization_update_policy( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, policy: &shared::Policy, ) -> UpdateResult> { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::ModifyPolicy) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::ModifyPolicy).await?; let role_assignments = self .db_datastore diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index 07c4c73ceec..0166bde2a6d 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -33,39 +33,23 @@ impl super::Nexus { project_selector: &'a params::ProjectSelector, ) -> LookupResult> { match project_selector { - params::ProjectSelector { project: NameOrId::Id(id), .. } => { - // TODO: 400 if organization is present + params::ProjectSelector(NameOrId::Id(id), ..) => { let project = LookupPath::new(opctx, &self.db_datastore).project_id(*id); Ok(project) } - params::ProjectSelector { - project: NameOrId::Name(project_name), - organization: Some(NameOrId::Id(organization_id)), - } => { - let project = LookupPath::new(opctx, &self.db_datastore) - .organization_id(*organization_id) - .project_name(Name::ref_cast(project_name)); - Ok(project) - } - params::ProjectSelector { - project: NameOrId::Name(project_name), - organization: Some(NameOrId::Name(organization_name)), - } => { - let project = LookupPath::new(opctx, &self.db_datastore) - .organization_name(Name::ref_cast(organization_name)) - .project_name(Name::ref_cast(project_name)); - Ok(project) + params::ProjectSelector(NameOrId::Name(name), org_selector) => { + if let Some(org) = org_selector { + let project = self + .organization_lookup(opctx, org)? + .project_name(Name::ref_cast(name)); + Ok(project) + } else { + Err(Error::InvalidRequest { + message: "Unable to resolve project by name without organization".to_string(), + }) + } } - _ => Err(Error::InvalidRequest { - message: " - Unable to resolve project. Expected one of - - project: Uuid - - project: Name, organization: Uuid - - project: Name, organization: Name - " - .to_string(), - }), } } pub async fn project_create( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 50b3435f5c8..6d342177715 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -98,6 +98,14 @@ pub fn external_api() -> NexusApiDescription { api.register(organization_policy_view)?; api.register(organization_policy_update)?; + api.register(organization_list_v1)?; + api.register(organization_create_v1)?; + api.register(organization_view_v1)?; + api.register(organization_delete_v1)?; + api.register(organization_update_v1)?; + api.register(organization_policy_view_v1)?; + api.register(organization_policy_update_v1)?; + api.register(project_list)?; api.register(project_create)?; api.register(project_view)?; @@ -918,10 +926,55 @@ async fn local_idp_user_set_password( } /// List organizations +#[endpoint { + method = GET, + path = "/v1/organizations", + tags = ["organizations"] +}] +async fn organization_list_v1( + rqctx: Arc>>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let query = query_params.into_inner(); + let params = ScanByNameOrId::from_query(&query)?; + let field = pagination_field_for_scan_params(params); + + let organizations = match field { + PagField::Id => { + let page_selector = data_page_params_nameid_id(&rqctx, &query)?; + nexus.organizations_list_by_id(&opctx, &page_selector).await? + } + + PagField::Name => { + let page_selector = + data_page_params_nameid_name(&rqctx, &query)? + .map_name(|n| Name::ref_cast(n)); + nexus.organizations_list_by_name(&opctx, &page_selector).await? + } + } + .into_iter() + .map(|p| p.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + organizations, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// List organizations +/// Use `/v1/organizations` instead #[endpoint { method = GET, path = "/organizations", tags = ["organizations"], + deprecated = true }] async fn organization_list( rqctx: Arc>>, @@ -961,10 +1014,34 @@ async fn organization_list( } /// Create an organization +#[endpoint { + method = POST, + path = "/v1/organizations", + tags = ["organizations"], +}] +async fn organization_create_v1( + rqctx: Arc>>, + new_organization: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization = nexus + .organization_create(&opctx, &new_organization.into_inner()) + .await?; + Ok(HttpResponseCreated(organization.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Create an organization +/// Use `POST /v1/organizations` instead #[endpoint { method = POST, path = "/organizations", tags = ["organizations"], + deprecated = true }] async fn organization_create( rqctx: Arc>>, @@ -982,6 +1059,37 @@ async fn organization_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +struct OrganizationLookupPathParam { + organization: NameOrId, +} + +#[endpoint { + method = GET, + path = "/v1/organizations/{organization}", + tags = ["organizations"], +}] +async fn organization_view_v1( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let (.., organization) = nexus + .organization_lookup( + &opctx, + ¶ms::OrganizationSelector(path.organization), + )? + .fetch() + .await?; + Ok(HttpResponseOk(organization.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Organization requests #[derive(Deserialize, JsonSchema)] struct OrganizationPathParam { @@ -990,10 +1098,12 @@ struct OrganizationPathParam { } /// Fetch an organization +/// Use `GET /v1/organizations/{organization}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}", tags = ["organizations"], + deprecated = true }] async fn organization_view( rqctx: Arc>>, @@ -1005,18 +1115,27 @@ async fn organization_view( let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization = - nexus.organization_fetch(&opctx, &organization_name).await?; + let (.., organization) = nexus + .organization_lookup( + &opctx, + ¶ms::OrganizationSelector(NameOrId::Name( + path.organization_name.into(), + )), + )? + .fetch() + .await?; Ok(HttpResponseOk(organization.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Fetch an organization by id +/// Use `GET /v1/organizations/{organization}` instead #[endpoint { method = GET, path = "/by-id/organizations/{id}", tags = ["organizations"], + deprecated = true }] async fn organization_view_by_id( rqctx: Arc>>, @@ -1025,20 +1144,51 @@ async fn organization_view_by_id( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization = nexus.organization_fetch_by_id(&opctx, id).await?; + let (.., organization) = nexus + .organization_lookup( + &opctx, + ¶ms::OrganizationSelector(NameOrId::Id(path.id.into())), + )? + .fetch() + .await?; Ok(HttpResponseOk(organization.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = DELETE, + path = "/v1/organizations/{organization}", + tags = ["organizations"], +}] +async fn organization_delete_v1( + rqctx: Arc>>, + path_params: Path, +) -> Result { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(params.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + nexus.organization_delete(&opctx, &organization_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete an organization +/// Use `DELETE /v1/organizations/{organization}` instead #[endpoint { method = DELETE, path = "/organizations/{organization_name}", tags = ["organizations"], + deprecated = true }] async fn organization_delete( rqctx: Arc>>, @@ -1047,25 +1197,62 @@ async fn organization_delete( let apictx = rqctx.context(); let nexus = &apictx.nexus; let params = path_params.into_inner(); - let organization_name = ¶ms.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus.organization_delete(&opctx, &organization_name).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(params.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + nexus.organization_delete(&opctx, &organization_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = PUT, + path = "/v1/organizations/{organization}", + tags = ["organizations"], +}] +async fn organization_update_v1( + rqctx: Arc>>, + path_params: Path, + updated_organization: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(params.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let new_organization = nexus + .organization_update( + &opctx, + &organization_lookup, + &updated_organization.into_inner(), + ) + .await?; + Ok(HttpResponseOk(new_organization.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Update an organization // TODO-correctness: Is it valid for PUT to accept application/json that's a // subset of what the resource actually represents? If not, is that a problem? // (HTTP may require that this be idempotent.) If so, can we get around that // having this be a slightly different content-type (e.g., // "application/json-patch")? We should see what other APIs do. +/// Use `PUT /v1/organizations/{organization}` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}", tags = ["organizations"], + deprecated = true }] async fn organization_update( rqctx: Arc>>, @@ -1078,10 +1265,15 @@ async fn organization_update( let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; let new_organization = nexus .organization_update( &opctx, - &organization_name, + &organization_lookup, &updated_organization.into_inner(), ) .await?; @@ -1090,11 +1282,40 @@ async fn organization_update( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = GET, + path = "/v1/organizations/{organization}/policy", + tags = ["organizations"], +}] +async fn organization_policy_view_v1( + rqctx: Arc>>, + path_params: Path, +) -> Result>, HttpError> +{ + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(params.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let policy = nexus + .organization_fetch_policy(&opctx, &organization_lookup) + .await?; + Ok(HttpResponseOk(policy.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Fetch an organization's IAM policy +/// Use `GET /v1/organizations/{organization}/policy` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/policy", tags = ["organizations"], + deprecated = true }] async fn organization_policy_view( rqctx: Arc>>, @@ -1104,22 +1325,65 @@ async fn organization_policy_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let policy = - nexus.organization_fetch_policy(&opctx, organization_name).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let policy = nexus + .organization_fetch_policy(&opctx, &organization_lookup) + .await?; + Ok(HttpResponseOk(policy)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +#[endpoint { + method = PUT, + path = "/v1/organizations/{organization}/policy", + tags = ["organizations"], +}] +async fn organization_policy_update_v1( + rqctx: Arc>>, + path_params: Path, + new_policy: TypedBody>, +) -> Result>, HttpError> +{ + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let new_policy = new_policy.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(params.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let nasgns = new_policy.role_assignments.len(); + // This should have been validated during parsing. + bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); + let policy = nexus + .organization_update_policy( + &opctx, + &organization_lookup, + &new_policy, + ) + .await?; Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Update an organization's IAM policy +/// Use `PUT /v1/organizations/{organization}/policy` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}/policy", tags = ["organizations"], + deprecated = true }] async fn organization_policy_update( rqctx: Arc>>, @@ -1131,15 +1395,22 @@ async fn organization_policy_update( let nexus = &apictx.nexus; let path = path_params.into_inner(); let new_policy = new_policy.into_inner(); - let organization_name = &path.organization_name; - let handler = async { let nasgns = new_policy.role_assignments.len(); // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus - .organization_update_policy(&opctx, organization_name, &new_policy) + .organization_update_policy( + &opctx, + &organization_lookup, + &new_policy, + ) .await?; Ok(HttpResponseOk(policy)) }; @@ -2014,8 +2285,8 @@ async fn disk_metrics_list( struct InstanceListQueryParams { #[serde(flatten)] pagination: PaginatedByName, - #[serde(flatten)] - selector: params::ProjectSelector, + project: NameOrId, + organization: Option, } #[endpoint { @@ -2032,7 +2303,9 @@ async fn instance_list_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_project = nexus.project_lookup(&opctx, &query.selector)?; + let project_selector = + ¶ms::ProjectSelector::new(query.organization, query.project); + let authz_project = nexus.project_lookup(&opctx, project_selector)?; let instances = nexus .project_list_instances( &opctx, @@ -2068,12 +2341,10 @@ async fn instance_list( let nexus = &apictx.nexus; let query = query_params.into_inner(); let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let project_selector = params::ProjectSelector { - project: NameOrId::Name(project_name.clone().into()), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; @@ -2099,8 +2370,8 @@ async fn instance_list( #[derive(Deserialize, JsonSchema)] struct InstanceCreateParams { - #[serde(flatten)] - selector: params::ProjectSelector, + organization: Option, + project: NameOrId, } #[endpoint { @@ -2119,7 +2390,9 @@ async fn instance_create_v1( let new_instance_params = &new_instance.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_lookup = nexus.project_lookup(&opctx, &query.selector)?; + let project_selector = + ¶ms::ProjectSelector::new(query.organization, query.project); + let project_lookup = nexus.project_lookup(&opctx, project_selector)?; let instance = nexus .project_create_instance( &opctx, @@ -2153,13 +2426,11 @@ async fn instance_create( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; let new_instance_params = &new_instance.into_inner(); - let project_selector = params::ProjectSelector { - project: NameOrId::Name(project_name.clone().into()), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; @@ -2189,8 +2460,8 @@ struct InstanceLookupPathParam { #[derive(Deserialize, JsonSchema)] struct InstanceQueryParams { - #[serde(flatten)] - selector: Option, + organization: Option, + project: Option, } #[endpoint { @@ -2209,11 +2480,15 @@ async fn instance_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); - let instance_selector = + // let instance_selector = params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); + let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; - let (.., instance) = instance_selector.fetch().await?; + let (.., instance) = instance_lookup.fetch().await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2240,14 +2515,11 @@ async fn instance_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2277,11 +2549,7 @@ async fn instance_view_by_id( let (.., instance) = nexus .instance_lookup( &opctx, - ¶ms::InstanceSelector { - instance: NameOrId::Id(*id), - project: None, - organization: None, - }, + ¶ms::InstanceSelector::new(None, None, NameOrId::Id(*id)), )? .fetch() .await?; @@ -2304,8 +2572,11 @@ async fn instance_delete_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2329,14 +2600,11 @@ async fn instance_delete( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2364,8 +2632,11 @@ async fn instance_migrate_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let migrate_instance_params = migrate_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2397,15 +2668,12 @@ async fn instance_migrate( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; let migrate_instance_params = migrate_params.into_inner(); - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2436,8 +2704,11 @@ async fn instance_reboot_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2461,14 +2732,11 @@ async fn instance_reboot( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2494,8 +2762,11 @@ async fn instance_start_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2519,14 +2790,11 @@ async fn instance_start( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2551,8 +2819,11 @@ async fn instance_stop_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2576,14 +2847,11 @@ async fn instance_stop( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2596,8 +2864,8 @@ async fn instance_stop( #[derive(Deserialize, JsonSchema)] pub struct InstanceSerialConsoleParams { - #[serde(flatten)] - selector: Option, + organization: Option, + project: Option, #[serde(flatten)] pub console_params: params::InstanceSerialConsoleRequest, @@ -2617,8 +2885,11 @@ async fn instance_serial_console_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2648,14 +2919,11 @@ async fn instance_serial_console( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2687,8 +2955,11 @@ async fn instance_serial_console_stream_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; nexus.instance_serial_console_stream(conn, &instance_lookup).await?; Ok(()) @@ -2708,15 +2979,12 @@ async fn instance_serial_console_stream( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; nexus.instance_serial_console_stream(conn, &instance_lookup).await?; Ok(()) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index dd57a3935dc..bf47b856ed0 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -18,31 +18,36 @@ use serde::{ use std::{net::IpAddr, str::FromStr}; use uuid::Uuid; -#[derive(Deserialize, JsonSchema)] -pub struct ProjectSelector { - pub project: NameOrId, - pub organization: Option, -} +pub struct OrganizationSelector(pub NameOrId); + +pub struct ProjectSelector(pub NameOrId, pub Option); -#[derive(Deserialize, JsonSchema)] -pub struct InstanceSelector { - pub instance: NameOrId, - pub project: Option, - pub organization: Option, +impl ProjectSelector { + pub fn new( + organization: Option, + project: NameOrId, + ) -> ProjectSelector { + ProjectSelector(project, organization.map(|o| OrganizationSelector(o))) + } } +pub struct InstanceSelector(pub NameOrId, pub Option); + impl InstanceSelector { pub fn new( + organization: Option, + project: Option, instance: NameOrId, - project_selector: &Option, ) -> InstanceSelector { - InstanceSelector { + InstanceSelector( instance, - organization: project_selector - .as_ref() - .and_then(|s| s.organization.clone()), - project: project_selector.as_ref().map(|s| s.project.clone()), - } + project.map(|p| { + ProjectSelector( + p, + organization.map(|o| OrganizationSelector(o)), + ) + }), + ) } } From 129011eec752278a49ca0ffd4265dea3ddfdab03 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 13 Dec 2022 16:58:51 -0500 Subject: [PATCH 02/45] Add project APIs, update routes --- nexus/src/app/project.rs | 99 +-- nexus/src/app/vpc.rs | 11 +- nexus/src/external_api/http_entrypoints.rs | 377 ++++++++++-- nexus/tests/output/nexus_tags.txt | 14 + openapi/nexus.json | 685 ++++++++++++++++++++- 5 files changed, 1043 insertions(+), 143 deletions(-) diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index 0166bde2a6d..5051bfb62c6 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -14,6 +14,7 @@ use crate::external_api::params; use crate::external_api::shared; use anyhow::Context; use nexus_defaults as defaults; +use nexus_types::identity::Resource; use omicron_common::api::external::CreateResult; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::DeleteResult; @@ -55,13 +56,11 @@ impl super::Nexus { pub async fn project_create( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, new_project: ¶ms::ProjectCreate, ) -> CreateResult { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::CreateChild) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::CreateChild).await?; // Create a project. let db_project = @@ -70,6 +69,8 @@ impl super::Nexus { .db_datastore .project_create(opctx, &authz_org, db_project) .await?; + let project_lookup = LookupPath::new(opctx, &self.db_datastore) + .project_id(db_project.id()); // TODO: We probably want to have "project creation" and "default VPC // creation" co-located within a saga for atomicity. @@ -77,14 +78,10 @@ impl super::Nexus { // Until then, we just perform the operations sequentially. // Create a default VPC associated with the project. - // TODO-correctness We need to be using the project_id we just created. - // project_create() should return authz::Project and we should use that - // here. let _ = self .project_create_vpc( opctx, - &organization_name, - &new_project.identity.name.clone().into(), + &project_lookup, ¶ms::VpcCreate { identity: IdentityMetadataCreateParams { name: "default".parse().unwrap(), @@ -102,42 +99,14 @@ impl super::Nexus { Ok(db_project) } - pub async fn project_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - ) -> LookupResult { - let (.., db_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .fetch() - .await?; - Ok(db_project) - } - - pub async fn project_fetch_by_id( - &self, - opctx: &OpContext, - project_id: &Uuid, - ) -> LookupResult { - let (.., db_project) = LookupPath::new(opctx, &self.db_datastore) - .project_id(*project_id) - .fetch() - .await?; - Ok(db_project) - } - pub async fn projects_list_by_name( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::ListChildren) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore .projects_list_by_name(opctx, &authz_org, pagparams) .await @@ -146,13 +115,11 @@ impl super::Nexus { pub async fn projects_list_by_id( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::ListChildren) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore .projects_list_by_id(opctx, &authz_org, pagparams) .await @@ -161,15 +128,11 @@ impl super::Nexus { pub async fn project_update( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, new_params: ¶ms::ProjectUpdate, ) -> UpdateResult { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::Modify) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::Modify).await?; self.db_datastore .project_update(opctx, &authz_project, new_params.clone().into()) .await @@ -178,14 +141,10 @@ impl super::Nexus { pub async fn project_delete( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, ) -> DeleteResult { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::Delete) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::Delete).await?; self.db_datastore.project_delete(opctx, &authz_project).await } @@ -194,14 +153,10 @@ impl super::Nexus { pub async fn project_fetch_policy( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, ) -> LookupResult> { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::ReadPolicy) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ReadPolicy).await?; let role_assignments = self .db_datastore .role_assignment_fetch_visible(opctx, &authz_project) @@ -216,15 +171,11 @@ impl super::Nexus { pub async fn project_update_policy( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, policy: &shared::Policy, ) -> UpdateResult> { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::ModifyPolicy) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ModifyPolicy).await?; let role_assignments = self .db_datastore diff --git a/nexus/src/app/vpc.rs b/nexus/src/app/vpc.rs index 121e73d05e3..c4544d323cb 100644 --- a/nexus/src/app/vpc.rs +++ b/nexus/src/app/vpc.rs @@ -9,6 +9,7 @@ use crate::context::OpContext; use crate::db; use crate::db::identity::Asset; use crate::db::identity::Resource; +use crate::db::lookup; use crate::db::lookup::LookupPath; use crate::db::model::Name; use crate::db::model::VpcRouterKind; @@ -45,15 +46,11 @@ impl super::Nexus { pub async fn project_create_vpc( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, params: ¶ms::VpcCreate, ) -> CreateResult { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::CreateChild) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::CreateChild).await?; let vpc_id = Uuid::new_v4(); let system_router_id = Uuid::new_v4(); let default_route_id = Uuid::new_v4(); diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 6d342177715..db945cfae84 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -115,6 +115,14 @@ pub fn external_api() -> NexusApiDescription { api.register(project_policy_view)?; api.register(project_policy_update)?; + api.register(project_list_v1)?; + api.register(project_create_v1)?; + api.register(project_view_v1)?; + api.register(project_delete_v1)?; + api.register(project_update_v1)?; + api.register(project_policy_view_v1)?; + api.register(project_policy_update_v1)?; + // Customer-Accessible IP Pools API api.register(ip_pool_list)?; api.register(ip_pool_create)?; @@ -1112,7 +1120,6 @@ async fn organization_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let (.., organization) = nexus @@ -1262,7 +1269,6 @@ async fn organization_update( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = ¶ms::OrganizationSelector( @@ -1417,11 +1423,79 @@ async fn organization_policy_update( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +pub struct ProjectListQueryParams { + pub organization: NameOrId, + #[serde(flatten)] + pagination: PaginatedByNameOrId, +} + +/// List projects +#[endpoint { + method = GET, + path = "/v1/projects", + tags = ["projects"], +}] +async fn project_list_v1( + rqctx: Arc>>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(query.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let params = ScanByNameOrId::from_query(&query.pagination)?; + let field = pagination_field_for_scan_params(params); + let projects = match field { + PagField::Id => { + let page_selector = + data_page_params_nameid_id(&rqctx, &query.pagination)?; + nexus + .projects_list_by_id( + &opctx, + &organization_lookup, + &page_selector, + ) + .await? + } + + PagField::Name => { + let page_selector = + data_page_params_nameid_name(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)); + nexus + .projects_list_by_name( + &opctx, + &organization_lookup, + &page_selector, + ) + .await? + } + } + .into_iter() + .map(|p| p.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query.pagination, + projects, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// List projects +/// Use `GET /v1/projects` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects", tags = ["projects"], + deprecated = true, }] async fn project_list( rqctx: Arc>>, @@ -1432,10 +1506,14 @@ async fn project_list( let nexus = &apictx.nexus; let query = query_params.into_inner(); let path = path_params.into_inner(); - let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; let params = ScanByNameOrId::from_query(&query)?; let field = pagination_field_for_scan_params(params); let projects = match field { @@ -1444,7 +1522,7 @@ async fn project_list( nexus .projects_list_by_id( &opctx, - &organization_name, + &organization_lookup, &page_selector, ) .await? @@ -1457,7 +1535,7 @@ async fn project_list( nexus .projects_list_by_name( &opctx, - &organization_name, + &organization_lookup, &page_selector, ) .await? @@ -1475,11 +1553,54 @@ async fn project_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +pub struct ProjectLookupPathParams { + pub project: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ProjectCreateParams { + pub organization: NameOrId, +} + +#[endpoint { + method = POST, + path = "/v1/projects", + tags = ["projects"], +}] +async fn project_create_v1( + rqctx: Arc>>, + query_params: Query, + new_project: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + params::OrganizationSelector(query.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let project = nexus + .project_create( + &opctx, + &organization_lookup, + &new_project.into_inner(), + ) + .await?; + Ok(HttpResponseCreated(project.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a project +/// Use `POST /v1/projects` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects", tags = ["projects"], + deprecated = true }] async fn project_create( rqctx: Arc>>, @@ -1488,14 +1609,18 @@ async fn project_create( ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; - let params = path_params.into_inner(); - let organization_name = ¶ms.organization_name; + let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; let project = nexus .project_create( &opctx, - &organization_name, + &organization_lookup, &new_project.into_inner(), ) .await?; @@ -1504,6 +1629,36 @@ async fn project_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +pub struct ProjectQueryParams { + pub organization: Option, +} + +#[endpoint { + method = GET, + path = "/v1/projects/{project}", + tags = ["projects"], +}] +async fn project_view_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = + params::ProjectSelector::new(query.organization, path.project); + let (.., project) = + nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; + Ok(HttpResponseOk(project.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Project requests #[derive(Deserialize, JsonSchema)] struct ProjectPathParam { @@ -1514,10 +1669,12 @@ struct ProjectPathParam { } /// Fetch a project +/// Use `GET /v1/projects/{project}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}", tags = ["projects"], + deprecated = true }] async fn project_view( rqctx: Arc>>, @@ -1526,23 +1683,26 @@ async fn project_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project = nexus - .project_fetch(&opctx, &organization_name, &project_name) - .await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let (.., project) = + nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; Ok(HttpResponseOk(project.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Fetch a project by id +/// Use `GET /v1/projects/{project}` instead #[endpoint { method = GET, path = "/by-id/projects/{id}", tags = ["projects"], + deprecated = true }] async fn project_view_by_id( rqctx: Arc>>, @@ -1551,20 +1711,50 @@ async fn project_view_by_id( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project = nexus.project_fetch_by_id(&opctx, id).await?; + let project_selector = + params::ProjectSelector::new(None, NameOrId::Id(path.id)); + let (.., project) = + nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; Ok(HttpResponseOk(project.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Delete a project +#[endpoint { + method = DELETE, + path = "/v1/projects/{project}", + tags = ["projects"], +}] +async fn project_delete_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = + params::ProjectSelector::new(query.organization, path.project); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + nexus.project_delete(&opctx, &project_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Delete a project +/// Use `DELETE /v1/projects/{project}` instead #[endpoint { method = DELETE, path = "/organizations/{organization_name}/projects/{project_name}", tags = ["projects"], + deprecated = true }] async fn project_delete( rqctx: Arc>>, @@ -1572,27 +1762,62 @@ async fn project_delete( ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; - let params = path_params.into_inner(); - let organization_name = ¶ms.organization_name; - let project_name = ¶ms.project_name; + let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus.project_delete(&opctx, &organization_name, &project_name).await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + nexus.project_delete(&opctx, &project_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Update a project +#[endpoint { + method = PUT, + path = "/v1/projects/{project}", + tags = ["projects"], +}] +async fn project_update_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, + updated_project: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let updated_project = updated_project.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = + params::ProjectSelector::new(query.organization, path.project); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let project = nexus + .project_update(&opctx, &project_lookup, &updated_project) + .await?; + Ok(HttpResponseOk(project.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Update a project // TODO-correctness: Is it valid for PUT to accept application/json that's a // subset of what the resource actually represents? If not, is that a problem? // (HTTP may require that this be idempotent.) If so, can we get around that // having this be a slightly different content-type (e.g., // "application/json-patch")? We should see what other APIs do. +/// Use `PUT /v1/projects/{project}` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}/projects/{project_name}", tags = ["projects"], + deprecated = true }] async fn project_update( rqctx: Arc>>, @@ -1602,28 +1827,59 @@ async fn project_update( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let newproject = nexus + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let new_project = nexus .project_update( &opctx, - &organization_name, - &project_name, + &project_lookup, &updated_project.into_inner(), ) .await?; - Ok(HttpResponseOk(newproject.into())) + Ok(HttpResponseOk(new_project.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Fetch a project's IAM policy +#[endpoint { + method = GET, + path = "/v1/projects/{project}/policy", + tags = ["projects"], +}] +async fn project_policy_view_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = + params::ProjectSelector::new(query.organization, path.project); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let policy = + nexus.project_fetch_policy(&opctx, &project_lookup).await?; + Ok(HttpResponseOk(policy.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Fetch a project's IAM policy +/// Use `GET /v1/projects/{project}/policy` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/policy", tags = ["projects"], + deprecated = true }] async fn project_policy_view( rqctx: Arc>>, @@ -1632,15 +1888,43 @@ async fn project_policy_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let policy = + nexus.project_fetch_policy(&opctx, &project_lookup).await?; + Ok(HttpResponseOk(policy)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} +/// Update a project's IAM policy +#[endpoint { + method = PUT, + path = "/v1/projects/{project}/policy", + tags = ["projects"], +}] +async fn project_policy_update_v1( + rqctx: Arc>>, + path_params: Path, + new_policy: TypedBody>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let new_policy = new_policy.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let policy = nexus - .project_fetch_policy(&opctx, organization_name, project_name) + let project_selector = params::ProjectSelector::new(None, path.project); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + nexus + .project_update_policy(&opctx, &project_lookup, &new_policy) .await?; - Ok(HttpResponseOk(policy)) + Ok(HttpResponseOk(new_policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -1660,21 +1944,18 @@ async fn project_policy_update( let nexus = &apictx.nexus; let path = path_params.into_inner(); let new_policy = new_policy.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let handler = async { let nasgns = new_policy.role_assignments.len(); // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = nexus - .project_update_policy( - &opctx, - organization_name, - project_name, - &new_policy, - ) + .project_update_policy(&opctx, &project_lookup, &new_policy) .await?; Ok(HttpResponseOk(policy)) }; @@ -2305,11 +2586,11 @@ async fn instance_list_v1( let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = ¶ms::ProjectSelector::new(query.organization, query.project); - let authz_project = nexus.project_lookup(&opctx, project_selector)?; + let project_lookup = nexus.project_lookup(&opctx, project_selector)?; let instances = nexus .project_list_instances( &opctx, - &authz_project, + &project_lookup, &data_page_params_for(&rqctx, &query.pagination)? .map_name(|n| Name::ref_cast(n)), ) @@ -3946,18 +4227,16 @@ async fn vpc_create( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; let new_vpc_params = &new_vpc.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let vpc = nexus - .project_create_vpc( - &opctx, - &organization_name, - &project_name, - &new_vpc_params, - ) + .project_create_vpc(&opctx, &project_lookup, &new_vpc_params) .await?; Ok(HttpResponseCreated(vpc.into())) }; diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 3828fb5bfb7..5d6714c6b12 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -72,13 +72,20 @@ timeseries_schema_get /timeseries/schema API operations found with tag "organizations" OPERATION ID URL PATH organization_create /organizations +organization_create_v1 /v1/organizations organization_delete /organizations/{organization_name} +organization_delete_v1 /v1/organizations/{organization} organization_list /organizations +organization_list_v1 /v1/organizations organization_policy_update /organizations/{organization_name}/policy +organization_policy_update_v1 /v1/organizations/{organization}/policy organization_policy_view /organizations/{organization_name}/policy +organization_policy_view_v1 /v1/organizations/{organization}/policy organization_update /organizations/{organization_name} +organization_update_v1 /v1/organizations/{organization} organization_view /organizations/{organization_name} organization_view_by_id /by-id/organizations/{id} +organization_view_v1 /v1/organizations/{organization} API operations found with tag "policy" OPERATION ID URL PATH @@ -88,13 +95,20 @@ system_policy_view /system/policy API operations found with tag "projects" OPERATION ID URL PATH project_create /organizations/{organization_name}/projects +project_create_v1 /v1/projects project_delete /organizations/{organization_name}/projects/{project_name} +project_delete_v1 /v1/projects/{project} project_list /organizations/{organization_name}/projects +project_list_v1 /v1/projects project_policy_update /organizations/{organization_name}/projects/{project_name}/policy +project_policy_update_v1 /v1/projects/{project}/policy project_policy_view /organizations/{organization_name}/projects/{project_name}/policy +project_policy_view_v1 /v1/projects/{project}/policy project_update /organizations/{organization_name}/projects/{project_name} +project_update_v1 /v1/projects/{project} project_view /organizations/{organization_name}/projects/{project_name} project_view_by_id /by-id/projects/{id} +project_view_v1 /v1/projects/{project} API operations found with tag "roles" OPERATION ID URL PATH diff --git a/openapi/nexus.json b/openapi/nexus.json index e6a4b225d1d..0981635a1ae 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -172,6 +172,7 @@ "organizations" ], "summary": "Fetch an organization by id", + "description": "Use `GET /v1/organizations/{organization}` instead", "operationId": "organization_view_by_id", "parameters": [ { @@ -202,7 +203,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/by-id/projects/{id}": { @@ -211,6 +213,7 @@ "projects" ], "summary": "Fetch a project by id", + "description": "Use `GET /v1/projects/{project}` instead", "operationId": "project_view_by_id", "parameters": [ { @@ -241,7 +244,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/by-id/snapshots/{id}": { @@ -805,6 +809,7 @@ "organizations" ], "summary": "List organizations", + "description": "Use `/v1/organizations` instead", "operationId": "organization_list", "parameters": [ { @@ -856,6 +861,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -863,6 +869,7 @@ "organizations" ], "summary": "Create an organization", + "description": "Use `POST /v1/organizations` instead", "operationId": "organization_create", "requestBody": { "content": { @@ -891,7 +898,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}": { @@ -900,6 +908,7 @@ "organizations" ], "summary": "Fetch an organization", + "description": "Use `GET /v1/organizations/{organization}` instead", "operationId": "organization_view", "parameters": [ { @@ -930,13 +939,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "organizations" ], "summary": "Update an organization", + "description": "Use `PUT /v1/organizations/{organization}` instead", "operationId": "organization_update", "parameters": [ { @@ -977,13 +988,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "organizations" ], "summary": "Delete an organization", + "description": "Use `DELETE /v1/organizations/{organization}` instead", "operationId": "organization_delete", "parameters": [ { @@ -1007,7 +1020,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/policy": { @@ -1016,6 +1030,7 @@ "organizations" ], "summary": "Fetch an organization's IAM policy", + "description": "Use `GET /v1/organizations/{organization}/policy` instead", "operationId": "organization_policy_view", "parameters": [ { @@ -1046,13 +1061,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "organizations" ], "summary": "Update an organization's IAM policy", + "description": "Use `PUT /v1/organizations/{organization}/policy` instead", "operationId": "organization_policy_update", "parameters": [ { @@ -1093,7 +1110,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects": { @@ -1102,6 +1120,7 @@ "projects" ], "summary": "List projects", + "description": "Use `GET /v1/projects` instead", "operationId": "project_list", "parameters": [ { @@ -1163,6 +1182,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -1170,6 +1190,7 @@ "projects" ], "summary": "Create a project", + "description": "Use `POST /v1/projects` instead", "operationId": "project_create", "parameters": [ { @@ -1210,7 +1231,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}": { @@ -1219,6 +1241,7 @@ "projects" ], "summary": "Fetch a project", + "description": "Use `GET /v1/projects/{project}` instead", "operationId": "project_view", "parameters": [ { @@ -1259,13 +1282,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "projects" ], "summary": "Update a project", + "description": "Use `PUT /v1/projects/{project}` instead", "operationId": "project_update", "parameters": [ { @@ -1316,13 +1341,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "projects" ], "summary": "Delete a project", + "description": "Use `DELETE /v1/projects/{project}` instead", "operationId": "project_delete", "parameters": [ { @@ -1356,7 +1383,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/disks": { @@ -3193,6 +3221,7 @@ "projects" ], "summary": "Fetch a project's IAM policy", + "description": "Use `GET /v1/projects/{project}/policy` instead", "operationId": "project_policy_view", "parameters": [ { @@ -3233,7 +3262,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ @@ -8245,6 +8275,635 @@ } } } + }, + "/v1/organizations": { + "get": { + "tags": [ + "organizations" + ], + "summary": "List organizations", + "operationId": "organization_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + }, + "style": "form" + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "organizations" + ], + "summary": "Create an organization", + "operationId": "organization_create_v1", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/organizations/{organization}": { + "get": { + "tags": [ + "organizations" + ], + "operationId": "organization_view_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "organizations" + ], + "operationId": "organization_update_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "organizations" + ], + "operationId": "organization_delete_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/organizations/{organization}/policy": { + "get": { + "tags": [ + "organizations" + ], + "operationId": "organization_policy_view_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "organizations" + ], + "operationId": "organization_policy_update_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects": { + "get": { + "tags": [ + "projects" + ], + "summary": "List projects", + "operationId": "project_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + }, + "style": "form" + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "projects" + ], + "operationId": "project_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects/{project}": { + "get": { + "tags": [ + "projects" + ], + "operationId": "project_view_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "projects" + ], + "summary": "Update a project", + "operationId": "project_update_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "projects" + ], + "summary": "Delete a project", + "operationId": "project_delete_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects/{project}/policy": { + "get": { + "tags": [ + "projects" + ], + "summary": "Fetch a project's IAM policy", + "operationId": "project_policy_view_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "projects" + ], + "summary": "Update a project's IAM policy", + "operationId": "project_policy_update_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { From c29c67d556243fb29387a7275bd112b5eda09532 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 13 Dec 2022 23:19:33 -0500 Subject: [PATCH 03/45] Fix clippy failures --- nexus/src/external_api/http_entrypoints.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index db945cfae84..e14a557e921 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1156,7 +1156,7 @@ async fn organization_view_by_id( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(NameOrId::Id(path.id.into())), + ¶ms::OrganizationSelector(NameOrId::Id(path.id)), )? .fetch() .await?; @@ -1310,7 +1310,7 @@ async fn organization_policy_view_v1( let policy = nexus .organization_fetch_policy(&opctx, &organization_lookup) .await?; - Ok(HttpResponseOk(policy.into())) + Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -1868,7 +1868,7 @@ async fn project_policy_view_v1( let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = nexus.project_fetch_policy(&opctx, &project_lookup).await?; - Ok(HttpResponseOk(policy.into())) + Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } From 599e6eb9989cd1e6e43f511162cf5e7726e2a499 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 13 Dec 2022 23:34:02 -0500 Subject: [PATCH 04/45] Require less conversions when going from NameOrId to name --- common/src/api/external/mod.rs | 12 ++ nexus/db-model/src/name.rs | 6 +- nexus/src/external_api/http_entrypoints.rs | 121 ++++++++++----------- 3 files changed, 73 insertions(+), 66 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 7ae77794494..6d988c61397 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -288,6 +288,18 @@ impl TryFrom for NameOrId { } } +impl From for NameOrId { + fn from(name: Name) -> Self { + NameOrId::Name(name) + } +} + +impl From for NameOrId { + fn from(id: Uuid) -> Self { + NameOrId::Id(id) + } +} + impl JsonSchema for NameOrId { fn schema_name() -> String { "NameOrId".to_string() diff --git a/nexus/db-model/src/name.rs b/nexus/db-model/src/name.rs index 11d3d6574c6..74ff715c8dd 100644 --- a/nexus/db-model/src/name.rs +++ b/nexus/db-model/src/name.rs @@ -37,7 +37,11 @@ use serde::{Deserialize, Serialize}; #[display("{0}")] pub struct Name(pub external::Name); -// impl Into for Name {} +impl From for external::NameOrId { + fn from(name: Name) -> Self { + Self::Name(name.0) + } +} NewtypeFrom! { () pub struct Name(external::Name); } NewtypeDeref! { () pub struct Name(external::Name); } diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index e14a557e921..9191651e14f 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1125,9 +1125,7 @@ async fn organization_view( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(NameOrId::Name( - path.organization_name.into(), - )), + ¶ms::OrganizationSelector(path.organization_name.into()), )? .fetch() .await?; @@ -1156,7 +1154,7 @@ async fn organization_view_by_id( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(NameOrId::Id(path.id)), + ¶ms::OrganizationSelector(path.id.into()), )? .fetch() .await?; @@ -1206,9 +1204,8 @@ async fn organization_delete( let params = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(params.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(params.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; nexus.organization_delete(&opctx, &organization_lookup).await?; @@ -1271,9 +1268,8 @@ async fn organization_update( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let new_organization = nexus @@ -1334,9 +1330,8 @@ async fn organization_policy_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1406,9 +1401,8 @@ async fn organization_policy_update( // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1509,9 +1503,8 @@ async fn project_list( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let params = ScanByNameOrId::from_query(&query)?; @@ -1612,9 +1605,8 @@ async fn project_create( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let project = nexus @@ -1686,8 +1678,8 @@ async fn project_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let (.., project) = nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; @@ -1714,7 +1706,7 @@ async fn project_view_by_id( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = - params::ProjectSelector::new(None, NameOrId::Id(path.id)); + params::ProjectSelector::new(None, path.id.into()); let (.., project) = nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; Ok(HttpResponseOk(project.into())) @@ -1766,8 +1758,8 @@ async fn project_delete( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; nexus.project_delete(&opctx, &project_lookup).await?; @@ -1830,8 +1822,8 @@ async fn project_update( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let new_project = nexus @@ -1891,8 +1883,8 @@ async fn project_policy_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = @@ -1950,8 +1942,8 @@ async fn project_policy_update( bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = nexus @@ -2623,8 +2615,8 @@ async fn instance_list( let query = query_params.into_inner(); let path = path_params.into_inner(); let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2709,8 +2701,8 @@ async fn instance_create( let path = path_params.into_inner(); let new_instance_params = &new_instance.into_inner(); let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2797,9 +2789,9 @@ async fn instance_view( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2824,13 +2816,12 @@ async fn instance_view_by_id( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let (.., instance) = nexus .instance_lookup( &opctx, - ¶ms::InstanceSelector::new(None, None, NameOrId::Id(*id)), + ¶ms::InstanceSelector::new(None, None, path.id.into()), )? .fetch() .await?; @@ -2882,9 +2873,9 @@ async fn instance_delete( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2951,9 +2942,9 @@ async fn instance_migrate( let path = path_params.into_inner(); let migrate_instance_params = migrate_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3014,9 +3005,9 @@ async fn instance_reboot( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3072,9 +3063,9 @@ async fn instance_start( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3129,9 +3120,9 @@ async fn instance_stop( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3201,9 +3192,9 @@ async fn instance_serial_console( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3262,9 +3253,9 @@ async fn instance_serial_console_stream( let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; nexus.instance_serial_console_stream(conn, &instance_lookup).await?; @@ -4231,8 +4222,8 @@ async fn vpc_create( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let vpc = nexus From 114d39a18eef6678b02612df4a343a45e160a01d Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 15:22:53 -0500 Subject: [PATCH 05/45] Revamp selectors (again x2), convert projects --- nexus/src/app/instance.rs | 32 +-- nexus/src/app/organization.rs | 6 +- nexus/src/app/project.rs | 27 +- nexus/src/external_api/http_entrypoints.rs | 299 ++++++++------------- nexus/test-utils/src/resource_helpers.rs | 2 +- nexus/tests/integration_tests/instances.rs | 2 +- nexus/types/src/external_api/params.rs | 112 ++++++-- 7 files changed, 248 insertions(+), 232 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index bab5e77cbd2..5f30d3dc54e 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -62,25 +62,27 @@ impl super::Nexus { instance_selector: &'a params::InstanceSelector, ) -> LookupResult> { match instance_selector { - params::InstanceSelector(NameOrId::Id(id), ..) => { + params::InstanceSelector { + project_selector: None, + instance: NameOrId::Id(id), + } => { let instance = LookupPath::new(opctx, &self.db_datastore).instance_id(*id); Ok(instance) } - params::InstanceSelector( - NameOrId::Name(name), - project_selector, - ) => { - if let Some(project) = project_selector { - let instance = self - .project_lookup(opctx, project)? - .instance_name(Name::ref_cast(name)); - Ok(instance) - } else { - Err(Error::InvalidRequest { - message: "Unable to resolve instance by name without instance".to_string(), - }) - } + params::InstanceSelector { + project_selector: Some(project_selector), + instance: NameOrId::Name(name), + } => { + let instance = self + .project_lookup(opctx, project_selector)? + .instance_name(Name::ref_cast(name)); + Ok(instance) + } + _ => { + return Err(Error::invalid_request( + "instance should either be UUID or project should be specified", + )); } } } diff --git a/nexus/src/app/organization.rs b/nexus/src/app/organization.rs index a1fae4997db..5c0d7cfdb06 100644 --- a/nexus/src/app/organization.rs +++ b/nexus/src/app/organization.rs @@ -31,12 +31,14 @@ impl super::Nexus { organization_selector: &'a params::OrganizationSelector, ) -> LookupResult> { match organization_selector { - params::OrganizationSelector(NameOrId::Id(id)) => { + params::OrganizationSelector { organization: NameOrId::Id(id) } => { let organization = LookupPath::new(opctx, &self.db_datastore) .organization_id(*id); Ok(organization) } - params::OrganizationSelector(NameOrId::Name(name)) => { + params::OrganizationSelector { + organization: NameOrId::Name(name), + } => { let organization = LookupPath::new(opctx, &self.db_datastore) .organization_name(Name::ref_cast(name)); Ok(organization) diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index 5051bfb62c6..0a20c5cc1cb 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -34,23 +34,26 @@ impl super::Nexus { project_selector: &'a params::ProjectSelector, ) -> LookupResult> { match project_selector { - params::ProjectSelector(NameOrId::Id(id), ..) => { + params::ProjectSelector { + project: NameOrId::Id(id), + organization_selector: None, + } => { let project = LookupPath::new(opctx, &self.db_datastore).project_id(*id); Ok(project) } - params::ProjectSelector(NameOrId::Name(name), org_selector) => { - if let Some(org) = org_selector { - let project = self - .organization_lookup(opctx, org)? - .project_name(Name::ref_cast(name)); - Ok(project) - } else { - Err(Error::InvalidRequest { - message: "Unable to resolve project by name without organization".to_string(), - }) - } + params::ProjectSelector { + project: NameOrId::Name(name), + organization_selector: Some(organization_selector), + } => { + let project = self + .organization_lookup(opctx, organization_selector)? + .project_name(Name::ref_cast(name)); + Ok(project) } + _ => Err(Error::invalid_request( + "project should either be UUID or organization should be specified" + )), } } pub async fn project_create( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 9191651e14f..aaaeed90945 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -60,7 +60,6 @@ use omicron_common::api::external::Disk; use omicron_common::api::external::Error; use omicron_common::api::external::Instance; use omicron_common::api::external::InternalContext; -use omicron_common::api::external::NameOrId; use omicron_common::api::external::NetworkInterface; use omicron_common::api::external::RouterRoute; use omicron_common::api::external::RouterRouteCreateParams; @@ -1067,11 +1066,6 @@ async fn organization_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -struct OrganizationLookupPathParam { - organization: NameOrId, -} - #[endpoint { method = GET, path = "/v1/organizations/{organization}", @@ -1079,7 +1073,7 @@ struct OrganizationLookupPathParam { }] async fn organization_view_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1089,7 +1083,9 @@ async fn organization_view_v1( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(path.organization), + ¶ms::OrganizationSelector { + organization: path.organization, + }, )? .fetch() .await?; @@ -1125,7 +1121,9 @@ async fn organization_view( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(path.organization_name.into()), + ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }, )? .fetch() .await?; @@ -1154,7 +1152,7 @@ async fn organization_view_by_id( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(path.id.into()), + ¶ms::OrganizationSelector { organization: path.id.into() }, )? .fetch() .await?; @@ -1170,7 +1168,7 @@ async fn organization_view_by_id( }] async fn organization_delete_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1178,7 +1176,7 @@ async fn organization_delete_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - ¶ms::OrganizationSelector(params.organization); + ¶ms::OrganizationSelector { organization: params.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; nexus.organization_delete(&opctx, &organization_lookup).await?; @@ -1204,8 +1202,9 @@ async fn organization_delete( let params = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(params.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: params.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; nexus.organization_delete(&opctx, &organization_lookup).await?; @@ -1221,7 +1220,7 @@ async fn organization_delete( }] async fn organization_update_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, updated_organization: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -1230,7 +1229,7 @@ async fn organization_update_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - ¶ms::OrganizationSelector(params.organization); + ¶ms::OrganizationSelector { organization: params.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let new_organization = nexus @@ -1268,8 +1267,9 @@ async fn organization_update( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let new_organization = nexus @@ -1291,7 +1291,7 @@ async fn organization_update( }] async fn organization_policy_view_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, ) -> Result>, HttpError> { let apictx = rqctx.context(); @@ -1300,7 +1300,7 @@ async fn organization_policy_view_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - ¶ms::OrganizationSelector(params.organization); + ¶ms::OrganizationSelector { organization: params.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1330,8 +1330,9 @@ async fn organization_policy_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1349,7 +1350,7 @@ async fn organization_policy_view( }] async fn organization_policy_update_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, new_policy: TypedBody>, ) -> Result>, HttpError> { @@ -1360,7 +1361,7 @@ async fn organization_policy_update_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - ¶ms::OrganizationSelector(params.organization); + ¶ms::OrganizationSelector { organization: params.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let nasgns = new_policy.role_assignments.len(); @@ -1401,8 +1402,9 @@ async fn organization_policy_update( // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1417,13 +1419,6 @@ async fn organization_policy_update( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -pub struct ProjectListQueryParams { - pub organization: NameOrId, - #[serde(flatten)] - pagination: PaginatedByNameOrId, -} - /// List projects #[endpoint { method = GET, @@ -1432,17 +1427,15 @@ pub struct ProjectListQueryParams { }] async fn project_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(query.organization); let organization_lookup = - nexus.organization_lookup(&opctx, &organization_selector)?; + nexus.organization_lookup(&opctx, &query.organization)?; let params = ScanByNameOrId::from_query(&query.pagination)?; let field = pagination_field_for_scan_params(params); let projects = match field { @@ -1503,8 +1496,9 @@ async fn project_list( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let params = ScanByNameOrId::from_query(&query)?; @@ -1546,16 +1540,6 @@ async fn project_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -pub struct ProjectLookupPathParams { - pub project: NameOrId, -} - -#[derive(Deserialize, JsonSchema)] -pub struct ProjectCreateParams { - pub organization: NameOrId, -} - #[endpoint { method = POST, path = "/v1/projects", @@ -1563,7 +1547,7 @@ pub struct ProjectCreateParams { }] async fn project_create_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, new_project: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -1572,7 +1556,7 @@ async fn project_create_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - params::OrganizationSelector(query.organization); + params::OrganizationSelector { organization: query.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let project = nexus @@ -1605,8 +1589,9 @@ async fn project_create( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let project = nexus @@ -1621,11 +1606,6 @@ async fn project_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -pub struct ProjectQueryParams { - pub organization: Option, -} - #[endpoint { method = GET, path = "/v1/projects/{project}", @@ -1633,8 +1613,8 @@ pub struct ProjectQueryParams { }] async fn project_view_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1642,8 +1622,10 @@ async fn project_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - params::ProjectSelector::new(query.organization, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let (.., project) = nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; Ok(HttpResponseOk(project.into())) @@ -1722,8 +1704,8 @@ async fn project_view_by_id( }] async fn project_delete_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1731,8 +1713,10 @@ async fn project_delete_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - params::ProjectSelector::new(query.organization, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; nexus.project_delete(&opctx, &project_lookup).await?; Ok(HttpResponseDeleted()) @@ -1776,8 +1760,8 @@ async fn project_delete( }] async fn project_update_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, updated_project: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -1787,8 +1771,10 @@ async fn project_update_v1( let updated_project = updated_project.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - params::ProjectSelector::new(query.organization, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let project = nexus .project_update(&opctx, &project_lookup, &updated_project) @@ -1846,8 +1832,8 @@ async fn project_update( }] async fn project_policy_view_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1855,8 +1841,10 @@ async fn project_policy_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - params::ProjectSelector::new(query.organization, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = nexus.project_fetch_policy(&opctx, &project_lookup).await?; @@ -1902,7 +1890,7 @@ async fn project_policy_view( }] async fn project_policy_update_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, new_policy: TypedBody>, ) -> Result>, HttpError> { let apictx = rqctx.context(); @@ -2554,14 +2542,6 @@ async fn disk_metrics_list( // Instances -#[derive(Deserialize, JsonSchema)] -struct InstanceListQueryParams { - #[serde(flatten)] - pagination: PaginatedByName, - project: NameOrId, - organization: Option, -} - #[endpoint { method = GET, path = "/v1/instances", @@ -2569,16 +2549,15 @@ struct InstanceListQueryParams { }] async fn instance_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - ¶ms::ProjectSelector::new(query.organization, query.project); - let project_lookup = nexus.project_lookup(&opctx, project_selector)?; + let project_lookup = + nexus.project_lookup(&opctx, &query.project_selector)?; let instances = nexus .project_list_instances( &opctx, @@ -2641,12 +2620,6 @@ async fn instance_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -struct InstanceCreateParams { - organization: Option, - project: NameOrId, -} - #[endpoint { method = POST, path = "/v1/instances", @@ -2654,18 +2627,16 @@ struct InstanceCreateParams { }] async fn instance_create_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, new_instance: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; - let query = query_params.into_inner(); + let project_selector = query_params.into_inner(); let new_instance_params = &new_instance.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - ¶ms::ProjectSelector::new(query.organization, query.project); - let project_lookup = nexus.project_lookup(&opctx, project_selector)?; + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let instance = nexus .project_create_instance( &opctx, @@ -2719,24 +2690,6 @@ async fn instance_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Path parameters for Instance requests -#[derive(Deserialize, JsonSchema)] -struct InstanceLookupPathParam { - /// If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - /// - `project_id` - /// - `project_name`, `organization_id` - /// - `project_name`, `organization_name` - /// - /// If Id is used the above qualifiers are will be ignored - instance: NameOrId, -} - -#[derive(Deserialize, JsonSchema)] -struct InstanceQueryParams { - organization: Option, - project: Option, -} - #[endpoint { method = GET, path = "/v1/instances/{instance}", @@ -2744,8 +2697,8 @@ struct InstanceQueryParams { }] async fn instance_view_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -2753,12 +2706,10 @@ async fn instance_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - // let instance_selector = params::InstanceSelector::new(path.instance, &query.selector); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; let (.., instance) = instance_lookup.fetch().await?; @@ -2837,18 +2788,17 @@ async fn instance_view_by_id( }] async fn instance_delete_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2895,8 +2845,8 @@ async fn instance_delete( }] async fn instance_migrate_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, migrate_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -2904,11 +2854,10 @@ async fn instance_migrate_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let migrate_instance_params = migrate_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2969,18 +2918,17 @@ async fn instance_migrate( }] async fn instance_reboot_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3027,18 +2975,17 @@ async fn instance_reboot( }] async fn instance_start_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3084,18 +3031,17 @@ async fn instance_start( }] async fn instance_stop_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3134,15 +3080,6 @@ async fn instance_stop( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -pub struct InstanceSerialConsoleParams { - organization: Option, - project: Option, - - #[serde(flatten)] - pub console_params: params::InstanceSerialConsoleRequest, -} - #[endpoint { method = GET, path = "/v1/instances/{instance}/serial-console", @@ -3150,18 +3087,17 @@ pub struct InstanceSerialConsoleParams { }] async fn instance_serial_console_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3219,19 +3155,18 @@ async fn instance_serial_console( async fn instance_serial_console_stream_v1( rqctx: Arc>>, conn: WebsocketConnection, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> WebsocketChannelResult { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; nexus.instance_serial_console_stream(conn, &instance_lookup).await?; Ok(()) diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index da344d40657..ddc8e7374a8 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -77,7 +77,7 @@ pub async fn create_ip_pool( client: &ClientTestContext, pool_name: &str, ip_range: Option, - project_path: Option, + project_path: Option, ) -> (IpPool, IpPoolRange) { let ip_range = ip_range.unwrap_or_else(|| { use std::net::Ipv4Addr; diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 594c9573640..25389545710 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -2880,7 +2880,7 @@ async fn test_instance_ephemeral_ip_from_correct_project( // Create two IP pools. // // The first is restricted to the "restricted" project, the second unrestricted. - let project_path = params::ProjectPath { + let project_path = params::OldProjectPath { organization: Name::try_from(ORGANIZATION_NAME.to_string()).unwrap(), project: Name::try_from("restricted".to_string()).unwrap(), }; diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index bf47b856ed0..121fc291de6 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -7,6 +7,7 @@ use crate::external_api::shared; use chrono::{DateTime, Utc}; use omicron_common::api::external::{ + http_pagination::{PaginatedByName, PaginatedByNameOrId}, ByteCount, IdentityMetadataCreateParams, IdentityMetadataUpdateParams, InstanceCpuCount, Ipv4Net, Ipv6Net, Name, NameOrId, }; @@ -18,39 +19,112 @@ use serde::{ use std::{net::IpAddr, str::FromStr}; use uuid::Uuid; -pub struct OrganizationSelector(pub NameOrId); +#[derive(Deserialize, JsonSchema)] +pub struct OrganizationPath { + pub organization: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ProjectPath { + pub project: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +pub struct InstancePath { + pub instance: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +pub struct OrganizationSelector { + pub organization: NameOrId, +} + +impl From for OrganizationSelector { + fn from(name: Name) -> Self { + OrganizationSelector { organization: name.into() } + } +} -pub struct ProjectSelector(pub NameOrId, pub Option); +#[derive(Deserialize, JsonSchema)] +pub struct OptionalOrganizationSelector { + #[serde(flatten)] + pub organization_selector: Option, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ProjectSelector { + #[serde(flatten)] + pub organization_selector: Option, + pub project: NameOrId, +} +// TODO-v1: delete this post migration impl ProjectSelector { - pub fn new( - organization: Option, - project: NameOrId, - ) -> ProjectSelector { - ProjectSelector(project, organization.map(|o| OrganizationSelector(o))) + pub fn new(organization: Option, project: NameOrId) -> Self { + ProjectSelector { + organization_selector: organization + .map(|o| OrganizationSelector { organization: o }), + project, + } } } -pub struct InstanceSelector(pub NameOrId, pub Option); +#[derive(Deserialize, JsonSchema)] +pub struct ProjectList { + #[serde(flatten)] + pub pagination: PaginatedByNameOrId, + #[serde(flatten)] + pub organization: OrganizationSelector, +} + +#[derive(Deserialize, JsonSchema)] +pub struct OptionalProjectSelector { + #[serde(flatten)] + pub project_selector: Option, +} + +#[derive(Deserialize, JsonSchema)] +pub struct InstanceSelector { + #[serde(flatten)] + pub project_selector: Option, + pub instance: NameOrId, +} +// TODO-v1: delete this post migration impl InstanceSelector { pub fn new( organization: Option, project: Option, instance: NameOrId, - ) -> InstanceSelector { - InstanceSelector( + ) -> Self { + InstanceSelector { + project_selector: if let Some(p) = project { + Some(ProjectSelector::new(organization, p)) + } else { + None + }, instance, - project.map(|p| { - ProjectSelector( - p, - organization.map(|o| OrganizationSelector(o)), - ) - }), - ) + } } } +#[derive(Deserialize, JsonSchema)] +pub struct InstanceList { + #[serde(flatten)] + pub pagination: PaginatedByName, + #[serde(flatten)] + pub project_selector: ProjectSelector, +} + +#[derive(Deserialize, JsonSchema)] +pub struct InstanceSerialConsole { + #[serde(flatten)] + pub project_selector: Option, + + #[serde(flatten)] + pub console_params: InstanceSerialConsoleRequest, +} + // Silos /// Create-time parameters for a [`Silo`](crate::external_api::views::Silo) @@ -516,7 +590,7 @@ pub struct NetworkInterfaceUpdate { // Type used to identify a Project in request bodies, where one may not have // the path in the request URL. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -pub struct ProjectPath { +pub struct OldProjectPath { pub organization: Name, pub project: Name, } @@ -529,7 +603,7 @@ pub struct IpPoolCreate { #[serde(flatten)] pub identity: IdentityMetadataCreateParams, #[serde(flatten)] - pub project: Option, + pub project: Option, } /// Parameters for updating an IP Pool From 6ae7da34ec220af06e4f7fa9b4c58183cb373a44 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 15:31:46 -0500 Subject: [PATCH 06/45] Update nexus.json --- openapi/nexus.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openapi/nexus.json b/openapi/nexus.json index 0981635a1ae..df5124b7963 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7839,7 +7839,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -7891,7 +7890,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -7938,7 +7936,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8002,7 +7999,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8040,7 +8036,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8130,7 +8125,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8194,7 +8188,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8248,7 +8241,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" From 63eddd75767e28cc53ac780f3ec7b3dc1615c557 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 16:21:00 -0500 Subject: [PATCH 07/45] Skip deprecated endpoints in auth check; update test to assert contents --- nexus/tests/integration_tests/endpoints.rs | 47 ++++++------------- nexus/tests/integration_tests/unauthorized.rs | 6 +-- .../unauthorized_coverage.rs | 27 ++++------- .../output/uncovered-authz-endpoints.txt | 30 ++++++++---- 4 files changed, 49 insertions(+), 61 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 6051eeaf7a0..d2dfe67a283 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -87,11 +87,10 @@ lazy_static! { // Organization used for testing pub static ref DEMO_ORG_NAME: Name = "demo-org".parse().unwrap(); pub static ref DEMO_ORG_URL: String = - format!("/organizations/{}", *DEMO_ORG_NAME); + format!("/v1/organizations/{}", *DEMO_ORG_NAME); pub static ref DEMO_ORG_POLICY_URL: String = - format!("{}/policy", *DEMO_ORG_URL); - pub static ref DEMO_ORG_PROJECTS_URL: String = - format!("{}/projects", *DEMO_ORG_URL); + format!("/v1/organizations/{}/policy", *DEMO_ORG_NAME); + pub static ref DEMO_ORG_PROJECTS_URL: String = format!("/v1/projects?organization={}", *DEMO_ORG_NAME); pub static ref DEMO_ORG_CREATE: params::OrganizationCreate = params::OrganizationCreate { identity: IdentityMetadataCreateParams { @@ -103,20 +102,20 @@ lazy_static! { // Project used for testing pub static ref DEMO_PROJECT_NAME: Name = "demo-project".parse().unwrap(); pub static ref DEMO_PROJECT_URL: String = - format!("{}/{}", *DEMO_ORG_PROJECTS_URL, *DEMO_PROJECT_NAME); + format!("/v1/projects/{}?organization={}", *DEMO_PROJECT_NAME, *DEMO_ORG_NAME); pub static ref DEMO_PROJECT_SELECTOR: String = format!("?organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_POLICY_URL: String = - format!("{}/policy", *DEMO_PROJECT_URL); + format!("/v1/projects/{}/policy?organization={}", *DEMO_PROJECT_NAME, *DEMO_ORG_NAME); pub static ref DEMO_PROJECT_URL_DISKS: String = - format!("{}/disks", *DEMO_PROJECT_URL); + format!("/organizations/{}/projects/{}/disks", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_IMAGES: String = - format!("{}/images", *DEMO_PROJECT_URL); + format!("/organizations/{}/projects/{}/images", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_INSTANCES: String = format!("/v1/instances?organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_SNAPSHOTS: String = - format!("{}/snapshots", *DEMO_PROJECT_URL); + format!("/organizations/{}/projects/{}/snapshots", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_VPCS: String = - format!("{}/vpcs", *DEMO_PROJECT_URL); + format!("/organizations/{}/projects/{}/vpcs", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_CREATE: params::ProjectCreate = params::ProjectCreate { identity: IdentityMetadataCreateParams { @@ -128,13 +127,13 @@ lazy_static! { // VPC used for testing pub static ref DEMO_VPC_NAME: Name = "demo-vpc".parse().unwrap(); pub static ref DEMO_VPC_URL: String = - format!("{}/{}", *DEMO_PROJECT_URL_VPCS, *DEMO_VPC_NAME); + format!("/organizations/{}/projects/{}/vpcs/{}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_FIREWALL_RULES: String = - format!("{}/firewall/rules", *DEMO_VPC_URL); + format!("/organizations/{}/projects/{}/vpcs/{}/firewall/rules", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_ROUTERS: String = - format!("{}/routers", *DEMO_VPC_URL); + format!("/organizations/{}/projects/{}/vpcs/{}/routers", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_SUBNETS: String = - format!("{}/subnets", *DEMO_VPC_URL); + format!("/organizations/{}/projects/{}/vpcs/{}/subnets", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_CREATE: params::VpcCreate = params::VpcCreate { identity: IdentityMetadataCreateParams { @@ -819,7 +818,7 @@ lazy_static! { /* Organizations */ VerifyEndpoint { - url: "/organizations", + url: "/v1/organizations", visibility: Visibility::Public, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ @@ -830,15 +829,6 @@ lazy_static! { ], }, - VerifyEndpoint { - url: "/by-id/organizations/{id}", - visibility: Visibility::Protected, - unprivileged_access: UnprivilegedAccess::None, - allowed_methods: vec![ - AllowedMethod::Get, - ], - }, - VerifyEndpoint { url: &*DEMO_ORG_URL, visibility: Visibility::Protected, @@ -895,15 +885,6 @@ lazy_static! { ], }, - VerifyEndpoint { - url: "/by-id/projects/{id}", - visibility: Visibility::Protected, - unprivileged_access: UnprivilegedAccess::None, - allowed_methods: vec![ - AllowedMethod::Get, - ], - }, - VerifyEndpoint { url: &*DEMO_PROJECT_URL, visibility: Visibility::Protected, diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 7f6a6c300f1..a51326747d3 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -214,15 +214,15 @@ lazy_static! { }, // Create an Organization SetupReq::Post { - url: "/organizations", + url: "/v1/organizations", body: serde_json::to_value(&*DEMO_ORG_CREATE).unwrap(), - id_routes: vec!["/by-id/organizations/{id}"], + id_routes: vec![], }, // Create a Project in the Organization SetupReq::Post { url: &*DEMO_ORG_PROJECTS_URL, body: serde_json::to_value(&*DEMO_PROJECT_CREATE).unwrap(), - id_routes: vec!["/by-id/projects/{id}"], + id_routes: vec![], }, // Create a VPC in the Project SetupReq::Post { diff --git a/nexus/tests/integration_tests/unauthorized_coverage.rs b/nexus/tests/integration_tests/unauthorized_coverage.rs index 64ce7eabc82..856dafe394d 100644 --- a/nexus/tests/integration_tests/unauthorized_coverage.rs +++ b/nexus/tests/integration_tests/unauthorized_coverage.rs @@ -135,23 +135,16 @@ fn test_unauthorized_coverage() { // not `expectorage::assert_contents`? Because we only expect this file to // ever shrink, which is easy enough to fix by hand, and we don't want to // make it easy to accidentally add things to the allowlist.) - let expected_uncovered_endpoints = - std::fs::read_to_string("tests/output/uncovered-authz-endpoints.txt") - .expect("failed to load file of allowed uncovered endpoints"); - let mut unexpected_uncovered_endpoints = "These endpoints were expected to be covered by the unauthorized_coverage test but were not:\n".to_string(); - let mut has_uncovered_endpoints = false; - for endpoint in uncovered_endpoints.lines() { - if !expected_uncovered_endpoints.contains(endpoint) { - unexpected_uncovered_endpoints - .push_str(&format!("\t{}\n", endpoint)); - has_uncovered_endpoints = true; - } - } - assert_eq!( - has_uncovered_endpoints, false, - "{}\nMake sure you've added a test for this endpoint in unauthorized.rs.", - unexpected_uncovered_endpoints - ) + // let expected_uncovered_endpoints = + // std::fs::read_to_string("tests/output/uncovered-authz-endpoints.txt") + // .expect("failed to load file of allowed uncovered endpoints"); + + // TODO: Update this to remove overwrite capabilities + // See https://github.com/oxidecomputer/expectorate/pull/12 + assert_contents( + "tests/output/uncovered-authz-endpoints.txt", + uncovered_endpoints.as_str(), + ); } #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index 70576c0b21c..1f6005f6886 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -1,5 +1,21 @@ API endpoints with no coverage in authz tests: +organization_delete (delete "/organizations/{organization_name}") +project_delete (delete "/organizations/{organization_name}/projects/{project_name}") +instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") +instance_view_by_id (get "/by-id/instances/{id}") +organization_view_by_id (get "/by-id/organizations/{id}") +project_view_by_id (get "/by-id/projects/{id}") login_saml_begin (get "/login/{silo_name}/saml/{provider_name}") +organization_list (get "/organizations") +organization_view (get "/organizations/{organization_name}") +organization_policy_view (get "/organizations/{organization_name}/policy") +project_list (get "/organizations/{organization_name}/projects") +project_view (get "/organizations/{organization_name}/projects/{project_name}") +instance_list (get "/organizations/{organization_name}/projects/{project_name}/instances") +instance_view (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") +instance_serial_console (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console") +instance_serial_console_stream (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console/stream") +project_policy_view (get "/organizations/{organization_name}/projects/{project_name}/policy") device_auth_request (post "/device/auth") device_auth_confirm (post "/device/confirm") device_access_token (post "/device/token") @@ -7,16 +23,14 @@ login_spoof (post "/login") login_local (post "/login/{silo_name}/local") login_saml (post "/login/{silo_name}/saml/{provider_name}") logout (post "/logout") - -Deprecated API endpoints to be removed at the end of the V1 migration -instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") -instance_list (get "/organizations/{organization_name}/projects/{project_name}/instances") -instance_view (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") -instance_serial_console (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console") -instance_serial_console_stream (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console/stream") +organization_create (post "/organizations") +project_create (post "/organizations/{organization_name}/projects") instance_create (post "/organizations/{organization_name}/projects/{project_name}/instances") instance_migrate (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/migrate") instance_reboot (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/reboot") instance_start (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start") instance_stop (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop") -instance_view_by_id (get "/by-id/instances/{id}") \ No newline at end of file +organization_update (put "/organizations/{organization_name}") +organization_policy_update (put "/organizations/{organization_name}/policy") +project_update (put "/organizations/{organization_name}/projects/{project_name}") +project_policy_update (put "/organizations/{organization_name}/projects/{project_name}/policy") From 8db8fa2c639d05f9bd45c0b2a810200a020deb4b Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 17:25:05 -0500 Subject: [PATCH 08/45] Fix authz test --- nexus/src/external_api/http_entrypoints.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index aaaeed90945..1d00073fabd 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1891,15 +1891,20 @@ async fn project_policy_view( async fn project_policy_update_v1( rqctx: Arc>>, path_params: Path, + query_params: Query, new_policy: TypedBody>, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); + let query = query_params.into_inner(); let new_policy = new_policy.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = params::ProjectSelector::new(None, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; nexus .project_update_policy(&opctx, &project_lookup, &new_policy) From f949c6cea86bc989420ea1eb7afc763280be4ec2 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 18:42:21 -0500 Subject: [PATCH 09/45] Update nexus.json --- nexus/types/src/external_api/params.rs | 6 +----- openapi/nexus.json | 8 ++++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 121fc291de6..470371d9ee2 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -98,11 +98,7 @@ impl InstanceSelector { instance: NameOrId, ) -> Self { InstanceSelector { - project_selector: if let Some(p) = project { - Some(ProjectSelector::new(organization, p)) - } else { - None - }, + project_selector: project.map(|p| ProjectSelector::new(organization, p) ), instance, } } diff --git a/openapi/nexus.json b/openapi/nexus.json index df5124b7963..2c66ba85594 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8865,6 +8865,14 @@ "$ref": "#/components/schemas/NameOrId" }, "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" } ], "requestBody": { From bd033a0a13d456900085562886fdb6b00f0dfef7 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 19:03:30 -0500 Subject: [PATCH 10/45] format -_- --- nexus/types/src/external_api/params.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 470371d9ee2..6ab7a074242 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -98,7 +98,8 @@ impl InstanceSelector { instance: NameOrId, ) -> Self { InstanceSelector { - project_selector: project.map(|p| ProjectSelector::new(organization, p) ), + project_selector: project + .map(|p| ProjectSelector::new(organization, p)), instance, } } From 69ac00809a03f69a323a74986f9e8deb3af12abe Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 15 Dec 2022 16:04:20 -0500 Subject: [PATCH 11/45] Add expanded error checking --- nexus/src/app/instance.rs | 12 ++++++++++-- nexus/src/app/project.rs | 10 +++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 5f30d3dc54e..a08c0e11627 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -79,10 +79,18 @@ impl super::Nexus { .instance_name(Name::ref_cast(name)); Ok(instance) } + params::InstanceSelector { + project_selector: Some(_), + instance: NameOrId::Id(_), + } => { + Err(Error::invalid_request( + "when providing instance as an ID, project should not be specified", + )) + } _ => { - return Err(Error::invalid_request( + Err(Error::invalid_request( "instance should either be UUID or project should be specified", - )); + )) } } } diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index 0a20c5cc1cb..7ba52f34c3a 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -51,8 +51,16 @@ impl super::Nexus { .project_name(Name::ref_cast(name)); Ok(project) } + params::ProjectSelector { + project: NameOrId::Id(_), + organization_selector: Some(_) + } => { + Err(Error::invalid_request( + "when providing project as an ID, organization should not be specified", + )) + } _ => Err(Error::invalid_request( - "project should either be UUID or organization should be specified" + "project should either be specified by id or organization should be specified" )), } } From 83768b1e9da2f8984cc3dcdf6c745611e5e5aa5e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 15 Dec 2022 19:54:58 -0500 Subject: [PATCH 12/45] Start networking v1 apis --- nexus/src/app/vpc.rs | 99 +++--- nexus/src/external_api/http_entrypoints.rs | 242 ++++++++++++--- nexus/tests/output/nexus_tags.txt | 5 + nexus/types/src/external_api/params.rs | 35 +++ openapi/nexus.json | 340 ++++++++++++++++++--- 5 files changed, 577 insertions(+), 144 deletions(-) diff --git a/nexus/src/app/vpc.rs b/nexus/src/app/vpc.rs index c4544d323cb..7dc7f5af6b3 100644 --- a/nexus/src/app/vpc.rs +++ b/nexus/src/app/vpc.rs @@ -25,12 +25,14 @@ use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::LookupType; +use omicron_common::api::external::NameOrId; use omicron_common::api::external::RouteDestination; use omicron_common::api::external::RouteTarget; use omicron_common::api::external::RouterRouteCreateParams; use omicron_common::api::external::RouterRouteKind; use omicron_common::api::external::UpdateResult; use omicron_common::api::external::VpcFirewallRuleUpdateParams; +use ref_cast::RefCast; use sled_agent_client::types::IpNet; use sled_agent_client::types::NetworkInterface; @@ -43,6 +45,41 @@ use uuid::Uuid; impl super::Nexus { // VPCs + pub fn vpc_lookup<'a>( + &'a self, + opctx: &'a OpContext, + vpc_selector: &'a params::VpcSelector, + ) -> LookupResult> { + match vpc_selector { + params::VpcSelector { + vpc: NameOrId::Id(id), + project_selector: None, + } => { + let vpc = + LookupPath::new(opctx, &self.db_datastore).vpc_id(*id); + Ok(vpc) + } + params::VpcSelector { + vpc: NameOrId::Name(name), + project_selector: Some(selector), + } => { + let vpc = self + .project_lookup(opctx, selector)? + .vpc_name(Name::ref_cast(name)); + Ok(vpc) + } + params::VpcSelector { + vpc: NameOrId::Id(_), + project_selector: Some(_), + } => Err(Error::invalid_request( + "when providing vpc as an ID, project should not be specified", + )), + _ => Err(Error::invalid_request( + "vpc should either be an ID or project should be specified", + )), + } + } + pub async fn project_create_vpc( &self, opctx: &OpContext, @@ -192,62 +229,24 @@ impl super::Nexus { pub async fn project_list_vpcs( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::ListChildren) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore .project_list_vpcs(&opctx, &authz_project, pagparams) .await } - pub async fn vpc_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - ) -> LookupResult { - let (.., db_vpc) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .fetch() - .await?; - Ok(db_vpc) - } - - pub async fn vpc_fetch_by_id( - &self, - opctx: &OpContext, - vpc_id: &Uuid, - ) -> LookupResult { - let (.., db_vpc) = LookupPath::new(opctx, &self.db_datastore) - .vpc_id(*vpc_id) - .fetch() - .await?; - Ok(db_vpc) - } - pub async fn project_update_vpc( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, + vpc_lookup: &lookup::Vpc<'_>, params: ¶ms::VpcUpdate, ) -> UpdateResult { - let (.., authz_vpc) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .lookup_for(authz::Action::Modify) - .await?; + let (.., authz_vpc) = + vpc_lookup.lookup_for(authz::Action::Modify).await?; self.db_datastore .project_update_vpc(opctx, &authz_vpc, params.clone().into()) .await @@ -256,17 +255,9 @@ impl super::Nexus { pub async fn project_delete_vpc( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, + vpc_lookup: &lookup::Vpc<'_>, ) -> DeleteResult { - let (.., authz_vpc, db_vpc) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .fetch() - .await?; + let (.., authz_vpc, db_vpc) = vpc_lookup.fetch().await?; let authz_vpc_router = authz::VpcRouter::new( authz_vpc.clone(), diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 1d00073fabd..9eee8a85481 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -196,6 +196,12 @@ pub fn external_api() -> NexusApiDescription { api.register(vpc_update)?; api.register(vpc_delete)?; + api.register(vpc_list_v1)?; + api.register(vpc_create_v1)?; + api.register(vpc_view_v1)?; + api.register(vpc_update_v1)?; + api.register(vpc_delete_v1)?; + api.register(vpc_subnet_list)?; api.register(vpc_subnet_view)?; api.register(vpc_subnet_view_by_id)?; @@ -4047,6 +4053,43 @@ async fn snapshot_delete( // VPCs +#[endpoint { + method = GET, + path = "/v1/vpcs", + tags = ["vpcs"], +}] +async fn vpc_list_v1( + rqctx: Arc>>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_lookup = + nexus.project_lookup(&opctx, &query.project_selector)?; + let vpcs = nexus + .project_list_vpcs( + &opctx, + &project_lookup, + &data_page_params_for(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)), + ) + .await? + .into_iter() + .map(|p| p.into()) + .collect(); + + Ok(HttpResponseOk(ScanByName::results_page( + &query.pagination, + vpcs, + &marker_for_name, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// List VPCs #[endpoint { method = GET, @@ -4062,15 +4105,17 @@ async fn vpc_list( let nexus = &apictx.nexus; let query = query_params.into_inner(); let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; + let project_selector = params::ProjectSelector::new( + Some(path.organization_name.into()), + path.project_name.into(), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let vpcs = nexus .project_list_vpcs( &opctx, - &organization_name, - &project_name, + &project_lookup, &data_page_params_for(&rqctx, &query)? .map_name(|n| Name::ref_cast(n)), ) @@ -4088,6 +4133,88 @@ async fn vpc_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = POST, + path = "/v1/vpcs", + tags = ["vpcs"], +}] +async fn vpc_create_v1( + rqctx: Arc>>, + query_params: Query, + body: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let new_vpc_params = body.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_lookup = nexus.project_lookup(&opctx, &query)?; + let vpc = nexus + .project_create_vpc(&opctx, &project_lookup, &new_vpc_params) + .await?; + Ok(HttpResponseCreated(vpc.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Create a VPC +#[endpoint { + method = POST, + path = "/organizations/{organization_name}/projects/{project_name}/vpcs", + tags = ["vpcs"], +}] +async fn vpc_create( + rqctx: Arc>>, + path_params: Path, + new_vpc: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let new_vpc_params = &new_vpc.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = params::ProjectSelector::new( + Some(path.organization_name.into()), + path.project_name.into(), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let vpc = nexus + .project_create_vpc(&opctx, &project_lookup, &new_vpc_params) + .await?; + Ok(HttpResponseCreated(vpc.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +#[endpoint { + method = GET, + path = "/v1/vpcs/{vpc}", + tags = ["vpcs"], +}] +async fn vpc_view_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_selector = params::VpcSelector { + project_selector: query.project_selector, + vpc: path.vpc, + }; + let (.., vpc) = + nexus.vpc_lookup(&opctx, &vpc_selector)?.fetch().await?; + Ok(HttpResponseOk(vpc.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for VPC requests #[derive(Deserialize, JsonSchema)] struct VpcPathParam { @@ -4109,14 +4236,15 @@ async fn vpc_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let vpc_name = &path.vpc_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let vpc = nexus - .vpc_fetch(&opctx, &organization_name, &project_name, &vpc_name) - .await?; + let vpc_selector = params::VpcSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.vpc_name.into(), + ); + let (.., vpc) = + nexus.vpc_lookup(&opctx, &vpc_selector)?.fetch().await?; Ok(HttpResponseOk(vpc.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -4138,38 +4266,41 @@ async fn vpc_view_by_id( let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let vpc = nexus.vpc_fetch_by_id(&opctx, id).await?; + let vpc_selector = params::VpcSelector::new(None, None, path.id.into()); + let (.., vpc) = + nexus.vpc_lookup(&opctx, &vpc_selector)?.fetch().await?; Ok(HttpResponseOk(vpc.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Create a VPC #[endpoint { - method = POST, - path = "/organizations/{organization_name}/projects/{project_name}/vpcs", + method = PUT, + path = "/v1/vpcs/{vpc}", tags = ["vpcs"], }] -async fn vpc_create( +async fn vpc_update_v1( rqctx: Arc>>, - path_params: Path, - new_vpc: TypedBody, -) -> Result, HttpError> { + path_params: Path, + query_params: Query, + updated_vpc: TypedBody, +) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let new_vpc_params = &new_vpc.into_inner(); + let query = query_params.into_inner(); + let updated_vpc_params = &updated_vpc.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = params::ProjectSelector::new( - Some(path.organization_name.into()), - path.project_name.into(), - ); - let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let vpc_selector = params::VpcSelector { + project_selector: query.project_selector, + vpc: path.vpc, + }; + let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; let vpc = nexus - .project_create_vpc(&opctx, &project_lookup, &new_vpc_params) + .project_update_vpc(&opctx, &vpc_lookup, &updated_vpc_params) .await?; - Ok(HttpResponseCreated(vpc.into())) + Ok(HttpResponseOk(vpc.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -4190,20 +4321,47 @@ async fn vpc_update( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_selector = params::VpcSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.vpc_name.into(), + ); + let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; let newvpc = nexus - .project_update_vpc( - &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &updated_vpc.into_inner(), - ) + .project_update_vpc(&opctx, &vpc_lookup, &updated_vpc.into_inner()) .await?; Ok(HttpResponseOk(newvpc.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = DELETE, + path = "/v1/vpcs/{vpc}", + tags = ["vpcs"], +}] +async fn vpc_delete_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_selector = params::VpcSelector { + project_selector: query.project_selector, + vpc: path.vpc, + }; + let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; + nexus.project_delete_vpc(&opctx, &vpc_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete a VPC #[endpoint { method = DELETE, @@ -4217,19 +4375,15 @@ async fn vpc_delete( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let vpc_name = &path.vpc_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus - .project_delete_vpc( - &opctx, - &organization_name, - &project_name, - &vpc_name, - ) - .await?; + let vpc_selector = params::VpcSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.vpc_name.into(), + ); + let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; + nexus.project_delete_vpc(&opctx, &vpc_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 5d6714c6b12..53d54c1445c 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -185,10 +185,13 @@ updates_refresh /system/updates/refresh API operations found with tag "vpcs" OPERATION ID URL PATH vpc_create /organizations/{organization_name}/projects/{project_name}/vpcs +vpc_create_v1 /v1/vpcs vpc_delete /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name} +vpc_delete_v1 /v1/vpcs/{vpc} vpc_firewall_rules_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/firewall/rules vpc_firewall_rules_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/firewall/rules vpc_list /organizations/{organization_name}/projects/{project_name}/vpcs +vpc_list_v1 /v1/vpcs vpc_router_create /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers vpc_router_delete /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name} vpc_router_list /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers @@ -209,6 +212,8 @@ vpc_subnet_update /organizations/{organization_name}/proj vpc_subnet_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} vpc_subnet_view_by_id /by-id/vpc-subnets/{id} vpc_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name} +vpc_update_v1 /v1/vpcs/{vpc} vpc_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name} vpc_view_by_id /by-id/vpcs/{id} +vpc_view_v1 /v1/vpcs/{vpc} diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 6ab7a074242..77e5a6c49e8 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -34,6 +34,11 @@ pub struct InstancePath { pub instance: NameOrId, } +#[derive(Deserialize, JsonSchema)] +pub struct VpcPath { + pub vpc: NameOrId, +} + #[derive(Deserialize, JsonSchema)] pub struct OrganizationSelector { pub organization: NameOrId, @@ -122,6 +127,36 @@ pub struct InstanceSerialConsole { pub console_params: InstanceSerialConsoleRequest, } +#[derive(Deserialize, JsonSchema)] +pub struct VpcSelector { + #[serde(flatten)] + pub project_selector: Option, + pub vpc: NameOrId, +} + +// TODO-v1: delete this post migration +impl VpcSelector { + pub fn new( + organization: Option, + project: Option, + vpc: NameOrId, + ) -> Self { + VpcSelector { + project_selector: project + .map(|p| ProjectSelector::new(organization, p)), + vpc, + } + } +} + +#[derive(Deserialize, JsonSchema)] +pub struct VpcList { + #[serde(flatten)] + pub pagination: PaginatedByName, + #[serde(flatten)] + pub project_selector: ProjectSelector, +} + // Silos /// Create-time parameters for a [`Silo`](crate::external_api::views::Silo) diff --git a/openapi/nexus.json b/openapi/nexus.json index dfa8a2e40c0..7cad427de44 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7913,8 +7913,7 @@ "type": "integer", "format": "uint32", "minimum": 1 - }, - "style": "form" + } }, { "in": "query", @@ -7923,16 +7922,14 @@ "schema": { "nullable": true, "type": "string" - }, - "style": "form" + } }, { "in": "query", "name": "sort_by", "schema": { "$ref": "#/components/schemas/NameOrIdSortMode" - }, - "style": "form" + } } ], "responses": { @@ -8004,8 +8001,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -8039,8 +8035,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "requestBody": { @@ -8084,8 +8079,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -8114,8 +8108,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -8149,8 +8142,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "requestBody": { @@ -8200,8 +8192,7 @@ "type": "integer", "format": "uint32", "minimum": 1 - }, - "style": "form" + } }, { "in": "query", @@ -8209,8 +8200,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", @@ -8219,16 +8209,14 @@ "schema": { "nullable": true, "type": "string" - }, - "style": "form" + } }, { "in": "query", "name": "sort_by", "schema": { "$ref": "#/components/schemas/NameOrIdSortMode" - }, - "style": "form" + } } ], "responses": { @@ -8263,8 +8251,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { @@ -8310,16 +8297,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -8354,16 +8339,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { @@ -8408,16 +8391,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -8447,16 +8428,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -8491,16 +8470,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { @@ -8532,6 +8509,277 @@ } } } + }, + "/v1/vpcs": { + "get": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpcs/{vpc}": { + "get": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_view_v1", + "parameters": [ + { + "in": "path", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_update_v1", + "parameters": [ + { + "in": "path", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_delete_v1", + "parameters": [ + { + "in": "path", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { From 52db40cfb1c7d9bc2ac3080836baa0ff83858d27 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 16 Dec 2022 13:08:54 -0500 Subject: [PATCH 13/45] Start on subnets --- nexus/src/app/vpc_subnet.rs | 80 ++++++----- nexus/src/external_api/http_entrypoints.rs | 105 ++++++++++++-- nexus/tests/output/nexus_tags.txt | 2 + nexus/types/src/external_api/params.rs | 36 +++++ openapi/nexus.json | 158 +++++++++++++++++++++ 5 files changed, 327 insertions(+), 54 deletions(-) diff --git a/nexus/src/app/vpc_subnet.rs b/nexus/src/app/vpc_subnet.rs index 144e8abf793..49167fba36b 100644 --- a/nexus/src/app/vpc_subnet.rs +++ b/nexus/src/app/vpc_subnet.rs @@ -8,6 +8,7 @@ use crate::authz; use crate::context::OpContext; use crate::db; use crate::db::identity::Resource; +use crate::db::lookup; use crate::db::lookup::LookupPath; use crate::db::model::Name; use crate::db::model::VpcSubnet; @@ -17,13 +18,50 @@ use omicron_common::api::external; use omicron_common::api::external::CreateResult; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::DeleteResult; +use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; use omicron_common::nexus_config::MIN_VPC_IPV4_SUBNET_PREFIX; +use ref_cast::RefCast; use uuid::Uuid; impl super::Nexus { + pub fn vpc_subnet_lookup<'a>( + &'a self, + opctx: &'a OpContext, + subnet_selector: &'a params::SubnetSelector, + ) -> LookupResult> { + match subnet_selector { + params::SubnetSelector { + subnet: NameOrId::Id(id), + vpc_selector: None, + } => { + let subnet = LookupPath::new(opctx, &self.db_datastore) + .vpc_subnet_id(*id); + Ok(subnet) + } + params::SubnetSelector { + subnet: NameOrId::Name(name), + vpc_selector: Some(selector), + } => { + let subnet = self + .vpc_lookup(opctx, selector)? + .vpc_subnet_name(Name::ref_cast(name)); + Ok(subnet) + } + params::SubnetSelector { + subnet: NameOrId::Id(_), + vpc_selector: Some(_), + } => Err(Error::invalid_request( + "when providing subnet as an ID, vpc should not be specified", + )), + _ => Err(Error::invalid_request( + "subnet should either be an ID or vpc should be specified", + )), + } + } // TODO: When a subnet is created it should add a route entry into the VPC's // system router pub async fn vpc_create_subnet( @@ -186,50 +224,14 @@ impl super::Nexus { pub async fn vpc_list_subnets( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, + vpc_lookup: &lookup::Vpc<'_>, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let (.., authz_vpc) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .lookup_for(authz::Action::ListChildren) - .await?; + let (.., authz_vpc) = + vpc_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore.vpc_list_subnets(opctx, &authz_vpc, pagparams).await } - pub async fn vpc_subnet_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - subnet_name: &Name, - ) -> LookupResult { - let (.., db_vpc) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_subnet_name(subnet_name) - .fetch() - .await?; - Ok(db_vpc) - } - - pub async fn vpc_subnet_fetch_by_id( - &self, - opctx: &OpContext, - vpc_subnet_id: &Uuid, - ) -> LookupResult { - let (.., db_vpc) = LookupPath::new(opctx, &self.db_datastore) - .vpc_subnet_id(*vpc_subnet_id) - .fetch() - .await?; - Ok(db_vpc) - } - pub async fn vpc_update_subnet( &self, opctx: &OpContext, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 9eee8a85481..5ac7ee9f342 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -210,6 +210,13 @@ pub fn external_api() -> NexusApiDescription { api.register(vpc_subnet_update)?; api.register(vpc_subnet_list_network_interfaces)?; + api.register(vpc_subnet_list_v1)?; + api.register(vpc_subnet_view_v1)?; + // api.register(vpc_subnet_create_v1)?; + // api.register(vpc_subnet_delete_v1)?; + // api.register(vpc_subnet_update_v1)?; + // api.register(vpc_subnet_list_network_interfaces_v1)?; + api.register(instance_network_interface_create)?; api.register(instance_network_interface_list)?; api.register(instance_network_interface_view)?; @@ -4263,7 +4270,6 @@ async fn vpc_view_by_id( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let vpc_selector = params::VpcSelector::new(None, None, path.id.into()); @@ -4389,6 +4395,41 @@ async fn vpc_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = GET, + path = "/v1/vpc-subnets", + tags = ["vpcs"], +}] +async fn vpc_subnet_list_v1( + rqctx: Arc>>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_lookup = nexus.vpc_lookup(&opctx, &query.vpc_selector)?; + let vpcs = nexus + .vpc_list_subnets( + &opctx, + &vpc_lookup, + &data_page_params_for(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)), + ) + .await? + .into_iter() + .map(|vpc| vpc.into()) + .collect(); + Ok(HttpResponseOk(ScanByName::results_page( + &query.pagination, + vpcs, + &marker_for_name, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// List subnets #[endpoint { method = GET, @@ -4406,12 +4447,16 @@ async fn vpc_subnet_list( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_selector = params::VpcSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.vpc_name.into(), + ); + let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; let vpcs = nexus .vpc_list_subnets( &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, + &vpc_lookup, &data_page_params_for(&rqctx, &query)? .map_name(|n| Name::ref_cast(n)), ) @@ -4428,6 +4473,33 @@ async fn vpc_subnet_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = GET, + path = "/v1/vpc-subnets/{subnet}", + tags = ["vpcs"], +}] +async fn vpc_subnet_view_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let subnet_selector = params::SubnetSelector { + vpc_selector: Some(query), + subnet: path.subnet, + }; + let (.., subnet) = + nexus.vpc_subnet_lookup(&opctx, &subnet_selector)?.fetch().await?; + Ok(HttpResponseOk(subnet.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for VPC Subnet requests #[derive(Deserialize, JsonSchema)] struct VpcSubnetPathParam { @@ -4452,15 +4524,14 @@ async fn vpc_subnet_view( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let subnet = nexus - .vpc_subnet_fetch( - &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.subnet_name, - ) - .await?; + let subnet_selector = params::SubnetSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + path.subnet_name.into(), + ); + let (.., subnet) = + nexus.vpc_subnet_lookup(&opctx, &subnet_selector)?.fetch().await?; Ok(HttpResponseOk(subnet.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -4479,10 +4550,14 @@ async fn vpc_subnet_view_by_id( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let subnet = nexus.vpc_subnet_fetch_by_id(&opctx, id).await?; + let subnet_selector = params::SubnetSelector { + subnet: path.id.into(), + vpc_selector: None, + }; + let (.., subnet) = + nexus.vpc_subnet_lookup(&opctx, &subnet_selector)?.fetch().await?; Ok(HttpResponseOk(subnet.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 53d54c1445c..ec2c8d39d12 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -208,9 +208,11 @@ vpc_subnet_create /organizations/{organization_name}/proj vpc_subnet_delete /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} vpc_subnet_list /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets vpc_subnet_list_network_interfaces /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/network-interfaces +vpc_subnet_list_v1 /v1/vpc-subnets vpc_subnet_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} vpc_subnet_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} vpc_subnet_view_by_id /by-id/vpc-subnets/{id} +vpc_subnet_view_v1 /v1/vpc-subnets/{subnet} vpc_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name} vpc_update_v1 /v1/vpcs/{vpc} vpc_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name} diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 77e5a6c49e8..65193e1b264 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -39,6 +39,11 @@ pub struct VpcPath { pub vpc: NameOrId, } +#[derive(Deserialize, JsonSchema)] +pub struct SubnetPath { + pub subnet: NameOrId, +} + #[derive(Deserialize, JsonSchema)] pub struct OrganizationSelector { pub organization: NameOrId, @@ -157,6 +162,37 @@ pub struct VpcList { pub project_selector: ProjectSelector, } +#[derive(Deserialize, JsonSchema)] +pub struct SubnetSelector { + #[serde(flatten)] + pub vpc_selector: Option, + pub subnet: NameOrId, +} + +// TODO-v1: delete this post migration +impl SubnetSelector { + pub fn new( + organization: Option, + project: Option, + vpc: Option, + subnet: NameOrId, + ) -> Self { + SubnetSelector { + vpc_selector: vpc + .map(|vpc| VpcSelector::new(organization, project, vpc)), + subnet, + } + } +} + +#[derive(Deserialize, JsonSchema)] +pub struct SubnetList { + #[serde(flatten)] + pub pagination: PaginatedByName, + #[serde(flatten)] + pub vpc_selector: VpcSelector, +} + // Silos /// Create-time parameters for a [`Silo`](crate::external_api::views::Silo) diff --git a/openapi/nexus.json b/openapi/nexus.json index 7cad427de44..9b86d544957 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8510,6 +8510,164 @@ } } }, + "/v1/vpc-subnets": { + "get": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_subnet_list_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/v1/vpc-subnets/{subnet}": { + "get": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_subnet_view_v1", + "parameters": [ + { + "in": "path", + "name": "subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/vpcs": { "get": { "tags": [ From 11762116f12640bf172b9f652667f6f0cbb3f386 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 16 Dec 2022 13:16:54 -0500 Subject: [PATCH 14/45] Update nexus.json --- openapi/nexus.json | 69 ++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/openapi/nexus.json b/openapi/nexus.json index dfa8a2e40c0..d1f50144527 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7913,8 +7913,7 @@ "type": "integer", "format": "uint32", "minimum": 1 - }, - "style": "form" + } }, { "in": "query", @@ -7923,16 +7922,14 @@ "schema": { "nullable": true, "type": "string" - }, - "style": "form" + } }, { "in": "query", "name": "sort_by", "schema": { "$ref": "#/components/schemas/NameOrIdSortMode" - }, - "style": "form" + } } ], "responses": { @@ -8004,8 +8001,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -8039,8 +8035,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "requestBody": { @@ -8084,8 +8079,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -8114,8 +8108,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -8149,8 +8142,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "requestBody": { @@ -8200,8 +8192,7 @@ "type": "integer", "format": "uint32", "minimum": 1 - }, - "style": "form" + } }, { "in": "query", @@ -8209,8 +8200,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", @@ -8219,16 +8209,14 @@ "schema": { "nullable": true, "type": "string" - }, - "style": "form" + } }, { "in": "query", "name": "sort_by", "schema": { "$ref": "#/components/schemas/NameOrIdSortMode" - }, - "style": "form" + } } ], "responses": { @@ -8263,8 +8251,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { @@ -8310,16 +8297,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -8354,16 +8339,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { @@ -8408,16 +8391,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -8447,16 +8428,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -8491,16 +8470,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { From e18b5b80759a7001ad03cffc44b93c27d121c964 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 19 Dec 2022 15:59:03 -0500 Subject: [PATCH 15/45] Add v1 subnet create --- nexus/src/app/vpc_subnet.rs | 12 +-- nexus/src/external_api/http_entrypoints.rs | 88 ++++++++++++++-------- 2 files changed, 59 insertions(+), 41 deletions(-) diff --git a/nexus/src/app/vpc_subnet.rs b/nexus/src/app/vpc_subnet.rs index 49167fba36b..abdf3e0c697 100644 --- a/nexus/src/app/vpc_subnet.rs +++ b/nexus/src/app/vpc_subnet.rs @@ -67,18 +67,10 @@ impl super::Nexus { pub async fn vpc_create_subnet( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, + vpc_lookup: &lookup::Vpc<'_>, params: ¶ms::VpcSubnetCreate, ) -> CreateResult { - let (.., authz_vpc, db_vpc) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .fetch() - .await?; + let (.., authz_vpc, db_vpc) = vpc_lookup.fetch().await?; // Validate IPv4 range if !params.ipv4_block.network().is_private() { diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 5ac7ee9f342..80883942e2a 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -212,7 +212,7 @@ pub fn external_api() -> NexusApiDescription { api.register(vpc_subnet_list_v1)?; api.register(vpc_subnet_view_v1)?; - // api.register(vpc_subnet_create_v1)?; + api.register(vpc_subnet_create_v1)?; // api.register(vpc_subnet_delete_v1)?; // api.register(vpc_subnet_update_v1)?; // api.register(vpc_subnet_list_network_interfaces_v1)?; @@ -4473,6 +4473,62 @@ async fn vpc_subnet_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = POST, + path = "/v1/vpc-subnets", + tags = ["vpcs"], +}] +async fn vpc_subnet_create_v1( + rqctx: Arc>>, + query_params: Query, + create_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let create = create_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_lookup = nexus.vpc_lookup(&opctx, &query)?; + let subnet = + nexus.vpc_create_subnet(&opctx, &vpc_lookup, &create).await?; + Ok(HttpResponseCreated(subnet.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Create a subnet +/// Use `POST /v1/vpc-subnets` instead. +#[endpoint { + method = POST, + path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets", + tags = ["vpcs"], + deprecated = true +}] +async fn vpc_subnet_create( + rqctx: Arc>>, + path_params: Path, + create_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_selector = params::VpcSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.vpc_name.into(), + ); + let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; + let subnet = nexus + .vpc_create_subnet(&opctx, &vpc_lookup, &create_params.into_inner()) + .await?; + Ok(HttpResponseCreated(subnet.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + #[endpoint { method = GET, path = "/v1/vpc-subnets/{subnet}", @@ -4563,36 +4619,6 @@ async fn vpc_subnet_view_by_id( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Create a subnet -#[endpoint { - method = POST, - path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets", - tags = ["vpcs"], -}] -async fn vpc_subnet_create( - rqctx: Arc>>, - path_params: Path, - create_params: TypedBody, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let handler = async { - let opctx = OpContext::for_external_api(&rqctx).await?; - let subnet = nexus - .vpc_create_subnet( - &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &create_params.into_inner(), - ) - .await?; - Ok(HttpResponseCreated(subnet.into())) - }; - apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await -} - /// Delete a subnet #[endpoint { method = DELETE, From 96786b3dec7cc9e45cc683a5b3ca31c858cea9c9 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 19 Dec 2022 16:17:09 -0500 Subject: [PATCH 16/45] Added deleted; Added deprecations; Updated nexus.json --- nexus/src/app/vpc_subnet.rs | 13 +- nexus/src/external_api/http_entrypoints.rs | 68 +++++++-- nexus/tests/integration_tests/projects.rs | 2 +- nexus/tests/output/nexus_tags.txt | 2 + nexus/types/src/external_api/params.rs | 6 + openapi/nexus.json | 168 +++++++++++++++++---- 6 files changed, 206 insertions(+), 53 deletions(-) diff --git a/nexus/src/app/vpc_subnet.rs b/nexus/src/app/vpc_subnet.rs index abdf3e0c697..9bf93938fdd 100644 --- a/nexus/src/app/vpc_subnet.rs +++ b/nexus/src/app/vpc_subnet.rs @@ -250,19 +250,10 @@ impl super::Nexus { pub async fn vpc_delete_subnet( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - subnet_name: &Name, + vpc_subnet_lookup: &lookup::VpcSubnet<'_>, ) -> DeleteResult { let (.., authz_subnet, db_subnet) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_subnet_name(subnet_name) - .fetch_for(authz::Action::Delete) - .await?; + vpc_subnet_lookup.fetch_for(authz::Action::Delete).await?; self.db_datastore .vpc_delete_subnet(opctx, &db_subnet, &authz_subnet) .await diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 80883942e2a..0e5b81486e9 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -213,7 +213,7 @@ pub fn external_api() -> NexusApiDescription { api.register(vpc_subnet_list_v1)?; api.register(vpc_subnet_view_v1)?; api.register(vpc_subnet_create_v1)?; - // api.register(vpc_subnet_delete_v1)?; + api.register(vpc_subnet_delete_v1)?; // api.register(vpc_subnet_update_v1)?; // api.register(vpc_subnet_list_network_interfaces_v1)?; @@ -4098,10 +4098,12 @@ async fn vpc_list_v1( } /// List VPCs +/// Use `GET /v1/vpcs` instead. #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs", tags = ["vpcs"], + deprecated = true, }] async fn vpc_list( rqctx: Arc>>, @@ -4166,10 +4168,12 @@ async fn vpc_create_v1( } /// Create a VPC +/// Use `POST /v1/vpcs` instead. #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/vpcs", tags = ["vpcs"], + deprecated = true, }] async fn vpc_create( rqctx: Arc>>, @@ -4231,10 +4235,12 @@ struct VpcPathParam { } /// Fetch a VPC +/// Use `GET /v1/vpcs/{vpc}` instead. #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}", tags = ["vpcs"], + deprecated = true, }] async fn vpc_view( rqctx: Arc>>, @@ -4258,10 +4264,12 @@ async fn vpc_view( } /// Fetch a VPC +/// Use `GET /v1/vpcs/{id}` instead. #[endpoint { method = GET, path = "/by-id/vpcs/{id}", tags = ["vpcs"], + deprecated = true, }] async fn vpc_view_by_id( rqctx: Arc>>, @@ -4312,10 +4320,12 @@ async fn vpc_update_v1( } /// Update a VPC +/// Use `PUT /v1/vpcs/{vpc}` instead. #[endpoint { method = PUT, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}", tags = ["vpcs"], + deprecated = true, }] async fn vpc_update( rqctx: Arc>>, @@ -4369,10 +4379,12 @@ async fn vpc_delete_v1( } /// Delete a VPC +/// Use `DELETE /v1/vpcs/{vpc}` instead. #[endpoint { method = DELETE, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}", tags = ["vpcs"], + deprecated = true, }] async fn vpc_delete( rqctx: Arc>>, @@ -4431,10 +4443,12 @@ async fn vpc_subnet_list_v1( } /// List subnets +/// Use `GET /v1/vpc-subnets` instead. #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets", tags = ["vpcs"], + deprecated = true, }] async fn vpc_subnet_list( rqctx: Arc>>, @@ -4566,10 +4580,12 @@ struct VpcSubnetPathParam { } /// Fetch a subnet +/// Use `GET /v1/vpc-subnets/{subnet}` instead. #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}", tags = ["vpcs"], + deprecated = true }] async fn vpc_subnet_view( rqctx: Arc>>, @@ -4594,10 +4610,12 @@ async fn vpc_subnet_view( } /// Fetch a subnet by id +/// Use `GET /v1/vpc-subnets/{id}` instead. #[endpoint { method = GET, path = "/by-id/vpc-subnets/{id}", tags = ["vpcs"], + deprecated = true }] async fn vpc_subnet_view_by_id( rqctx: Arc>>, @@ -4619,11 +4637,41 @@ async fn vpc_subnet_view_by_id( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = DELETE, + path = "/v1/vpc-subnets/{subnet}", + tags = ["vpcs"], +}] +async fn vpc_subnet_delete_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let subnet_selector = params::SubnetSelector { + vpc_selector: query.vpc_selector, + subnet: path.subnet, + }; + let subnet_lookup = + nexus.vpc_subnet_lookup(&opctx, &subnet_selector)?; + nexus.vpc_delete_subnet(&opctx, &subnet_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete a subnet +/// Use `DELETE /v1/vpc-subnets/{subnet}` instead #[endpoint { method = DELETE, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}", tags = ["vpcs"], + deprecated = true }] async fn vpc_subnet_delete( rqctx: Arc>>, @@ -4634,15 +4682,15 @@ async fn vpc_subnet_delete( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus - .vpc_delete_subnet( - &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.subnet_name, - ) - .await?; + let subnet_selector = params::SubnetSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + path.subnet_name.into(), + ); + let subnet_lookup = + nexus.vpc_subnet_lookup(&opctx, &subnet_selector)?; + nexus.vpc_delete_subnet(&opctx, &subnet_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/integration_tests/projects.rs b/nexus/tests/integration_tests/projects.rs index cc220ba4aac..b431a1cc55c 100644 --- a/nexus/tests/integration_tests/projects.rs +++ b/nexus/tests/integration_tests/projects.rs @@ -412,7 +412,7 @@ async fn test_project_deletion_with_ip_pool( name: String::from(pool_name).parse().unwrap(), description: String::from("description"), }, - project: Some(params::ProjectPath { + project: Some(params::OldProjectPath { organization: org_name.parse().unwrap(), project: name.parse().unwrap(), }), diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index ec2c8d39d12..ac41294842f 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -205,7 +205,9 @@ vpc_router_update /organizations/{organization_name}/proj vpc_router_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name} vpc_router_view_by_id /by-id/vpc-routers/{id} vpc_subnet_create /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets +vpc_subnet_create_v1 /v1/vpc-subnets vpc_subnet_delete /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} +vpc_subnet_delete_v1 /v1/vpc-subnets/{subnet} vpc_subnet_list /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets vpc_subnet_list_network_interfaces /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/network-interfaces vpc_subnet_list_v1 /v1/vpc-subnets diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 65193e1b264..0e96272b8f8 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -139,6 +139,12 @@ pub struct VpcSelector { pub vpc: NameOrId, } +#[derive(Deserialize, JsonSchema)] +pub struct OptionalVpcSelector { + #[serde(flatten)] + pub vpc_selector: Option, +} + // TODO-v1: delete this post migration impl VpcSelector { pub fn new( diff --git a/openapi/nexus.json b/openapi/nexus.json index 9b86d544957..5992c5539c8 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -362,6 +362,7 @@ "vpcs" ], "summary": "Fetch a subnet by id", + "description": "Use `GET /v1/vpc-subnets/{id}` instead.", "operationId": "vpc_subnet_view_by_id", "parameters": [ { @@ -391,7 +392,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/by-id/vpcs/{id}": { @@ -400,6 +402,7 @@ "vpcs" ], "summary": "Fetch a VPC", + "description": "Use `GET /v1/vpcs/{id}` instead.", "operationId": "vpc_view_by_id", "parameters": [ { @@ -429,7 +432,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/device/auth": { @@ -3411,6 +3415,7 @@ "vpcs" ], "summary": "List VPCs", + "description": "Use `GET /v1/vpcs` instead.", "operationId": "vpc_list", "parameters": [ { @@ -3477,6 +3482,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -3484,6 +3490,7 @@ "vpcs" ], "summary": "Create a VPC", + "description": "Use `POST /v1/vpcs` instead.", "operationId": "vpc_create", "parameters": [ { @@ -3532,7 +3539,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}": { @@ -3541,6 +3549,7 @@ "vpcs" ], "summary": "Fetch a VPC", + "description": "Use `GET /v1/vpcs/{vpc}` instead.", "operationId": "vpc_view", "parameters": [ { @@ -3585,13 +3594,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "vpcs" ], "summary": "Update a VPC", + "description": "Use `PUT /v1/vpcs/{vpc}` instead.", "operationId": "vpc_update", "parameters": [ { @@ -3646,13 +3657,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a VPC", + "description": "Use `DELETE /v1/vpcs/{vpc}` instead.", "operationId": "vpc_delete", "parameters": [ { @@ -3690,7 +3703,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/firewall/rules": { @@ -4502,6 +4516,7 @@ "vpcs" ], "summary": "List subnets", + "description": "Use `GET /v1/vpc-subnets` instead.", "operationId": "vpc_subnet_list", "parameters": [ { @@ -4574,6 +4589,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -4581,6 +4597,7 @@ "vpcs" ], "summary": "Create a subnet", + "description": "Use `POST /v1/vpc-subnets` instead.", "operationId": "vpc_subnet_create", "parameters": [ { @@ -4635,7 +4652,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}": { @@ -4644,6 +4662,7 @@ "vpcs" ], "summary": "Fetch a subnet", + "description": "Use `GET /v1/vpc-subnets/{subnet}` instead.", "operationId": "vpc_subnet_view", "parameters": [ { @@ -4696,7 +4715,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ @@ -4772,6 +4792,7 @@ "vpcs" ], "summary": "Delete a subnet", + "description": "Use `DELETE /v1/vpc-subnets/{subnet}` instead", "operationId": "vpc_subnet_delete", "parameters": [ { @@ -4817,7 +4838,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/network-interfaces": { @@ -8517,28 +8539,6 @@ ], "operationId": "vpc_subnet_list_v1", "parameters": [ - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, { "in": "query", "name": "limit", @@ -8608,6 +8608,64 @@ } }, "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_subnet_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } } }, "/v1/vpc-subnets/{subnet}": { @@ -8666,6 +8724,54 @@ "$ref": "#/components/responses/Error" } } + }, + "delete": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_subnet_delete_v1", + "parameters": [ + { + "in": "path", + "name": "subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } } }, "/v1/vpcs": { From 341dbd1eb951bcae10befa1152f3241c5e424abb Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 19 Dec 2022 16:27:26 -0500 Subject: [PATCH 17/45] Add subnet update --- nexus/src/app/vpc_subnet.rs | 14 +---- nexus/src/external_api/http_entrypoints.rs | 49 +++++++++++++-- nexus/tests/output/nexus_tags.txt | 1 + openapi/nexus.json | 69 +++++++++++++++++++++- 4 files changed, 116 insertions(+), 17 deletions(-) diff --git a/nexus/src/app/vpc_subnet.rs b/nexus/src/app/vpc_subnet.rs index 9bf93938fdd..fc261666d88 100644 --- a/nexus/src/app/vpc_subnet.rs +++ b/nexus/src/app/vpc_subnet.rs @@ -227,19 +227,11 @@ impl super::Nexus { pub async fn vpc_update_subnet( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - subnet_name: &Name, + vpc_subnet_lookup: &lookup::VpcSubnet<'_>, params: ¶ms::VpcSubnetUpdate, ) -> UpdateResult { - let (.., authz_subnet) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_subnet_name(subnet_name) - .lookup_for(authz::Action::Modify) - .await?; + let (.., authz_subnet) = + vpc_subnet_lookup.lookup_for(authz::Action::Modify).await?; self.db_datastore .vpc_update_subnet(&opctx, &authz_subnet, params.clone().into()) .await diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 0e5b81486e9..d6b0013c9b8 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -214,7 +214,7 @@ pub fn external_api() -> NexusApiDescription { api.register(vpc_subnet_view_v1)?; api.register(vpc_subnet_create_v1)?; api.register(vpc_subnet_delete_v1)?; - // api.register(vpc_subnet_update_v1)?; + api.register(vpc_subnet_update_v1)?; // api.register(vpc_subnet_list_network_interfaces_v1)?; api.register(instance_network_interface_create)?; @@ -4696,11 +4696,45 @@ async fn vpc_subnet_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = PUT, + path = "/v1/vpc-subnets/{subnet}", + tags = ["vpcs"], +}] +async fn vpc_subnet_update_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, + subnet_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let subnet_params = subnet_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let subnet_selector = params::SubnetSelector { + vpc_selector: query.vpc_selector, + subnet: path.subnet, + }; + let subnet_lookup = + nexus.vpc_subnet_lookup(&opctx, &subnet_selector)?; + let subnet = nexus + .vpc_update_subnet(&opctx, &subnet_lookup, &subnet_params) + .await?; + Ok(HttpResponseOk(subnet.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Update a subnet +/// Use `PUT /v1/vpc-subnets/{subnet}` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}", tags = ["vpcs"], + deprecated = true }] async fn vpc_subnet_update( rqctx: Arc>>, @@ -4712,13 +4746,18 @@ async fn vpc_subnet_update( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let subnet_selector = params::SubnetSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + path.subnet_name.into(), + ); + let subnet_lookup = + nexus.vpc_subnet_lookup(&opctx, &subnet_selector)?; let subnet = nexus .vpc_update_subnet( &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.subnet_name, + &subnet_lookup, &subnet_params.into_inner(), ) .await?; diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index ac41294842f..b042a0e3a9f 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -212,6 +212,7 @@ vpc_subnet_list /organizations/{organization_name}/proj vpc_subnet_list_network_interfaces /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/network-interfaces vpc_subnet_list_v1 /v1/vpc-subnets vpc_subnet_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} +vpc_subnet_update_v1 /v1/vpc-subnets/{subnet} vpc_subnet_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} vpc_subnet_view_by_id /by-id/vpc-subnets/{id} vpc_subnet_view_v1 /v1/vpc-subnets/{subnet} diff --git a/openapi/nexus.json b/openapi/nexus.json index 5992c5539c8..59f27cad33d 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -4723,6 +4723,7 @@ "vpcs" ], "summary": "Update a subnet", + "description": "Use `PUT /v1/vpc-subnets/{subnet}` instead", "operationId": "vpc_subnet_update", "parameters": [ { @@ -4785,7 +4786,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ @@ -8725,6 +8727,71 @@ } } }, + "put": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_subnet_update_v1", + "parameters": [ + { + "in": "path", + "name": "subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, "delete": { "tags": [ "vpcs" From bbf0f36566e1bb0e13441197b9ec767779eca820 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 17 Jan 2023 11:44:44 -0500 Subject: [PATCH 18/45] Update pagination strategy --- nexus/src/app/vpc.rs | 21 +++-- nexus/src/app/vpc_subnet.rs | 18 ++++- nexus/src/db/datastore/vpc.rs | 40 +++++++++- nexus/src/external_api/http_entrypoints.rs | 93 ++++++++++++++-------- nexus/types/src/external_api/params.rs | 35 +------- 5 files changed, 128 insertions(+), 79 deletions(-) diff --git a/nexus/src/app/vpc.rs b/nexus/src/app/vpc.rs index db1a7474075..d765776bc7c 100644 --- a/nexus/src/app/vpc.rs +++ b/nexus/src/app/vpc.rs @@ -26,10 +26,6 @@ use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::LookupType; use omicron_common::api::external::NameOrId; -use omicron_common::api::external::RouteDestination; -use omicron_common::api::external::RouteTarget; -use omicron_common::api::external::RouterRouteCreateParams; -use omicron_common::api::external::RouterRouteKind; use omicron_common::api::external::UpdateResult; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use ref_cast::RefCast; @@ -110,7 +106,20 @@ impl super::Nexus { Ok(db_vpc) } - pub async fn project_list_vpcs( + pub async fn project_list_vpcs_by_id( + &self, + opctx: &OpContext, + project_lookup: &lookup::Project<'_>, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ListChildren).await?; + self.db_datastore + .project_list_vpcs_by_id(&opctx, &authz_project, pagparams) + .await + } + + pub async fn project_list_vpcs_by_name( &self, opctx: &OpContext, project_lookup: &lookup::Project<'_>, @@ -119,7 +128,7 @@ impl super::Nexus { let (.., authz_project) = project_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore - .project_list_vpcs(&opctx, &authz_project, pagparams) + .project_list_vpcs_by_name(&opctx, &authz_project, pagparams) .await } diff --git a/nexus/src/app/vpc_subnet.rs b/nexus/src/app/vpc_subnet.rs index 8be40ac0506..c7abf0ba3a3 100644 --- a/nexus/src/app/vpc_subnet.rs +++ b/nexus/src/app/vpc_subnet.rs @@ -214,7 +214,19 @@ impl super::Nexus { } } - pub async fn vpc_list_subnets( + pub async fn vpc_list_subnets_by_id( + &self, + opctx: &OpContext, + vpc_lookup: &lookup::Vpc<'_>, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + let (.., authz_vpc) = + vpc_lookup.lookup_for(authz::Action::ListChildren).await?; + self.db_datastore + .vpc_list_subnets_by_id(opctx, &authz_vpc, pagparams) + .await + } + pub async fn vpc_list_subnets_by_name( &self, opctx: &OpContext, vpc_lookup: &lookup::Vpc<'_>, @@ -222,7 +234,9 @@ impl super::Nexus { ) -> ListResultVec { let (.., authz_vpc) = vpc_lookup.lookup_for(authz::Action::ListChildren).await?; - self.db_datastore.vpc_list_subnets(opctx, &authz_vpc, pagparams).await + self.db_datastore + .vpc_list_subnets_by_name(opctx, &authz_vpc, pagparams) + .await } pub async fn vpc_update_subnet( diff --git a/nexus/src/db/datastore/vpc.rs b/nexus/src/db/datastore/vpc.rs index ba951d7730d..54b11146dc5 100644 --- a/nexus/src/db/datastore/vpc.rs +++ b/nexus/src/db/datastore/vpc.rs @@ -53,7 +53,25 @@ use std::collections::BTreeMap; use uuid::Uuid; impl DataStore { - pub async fn project_list_vpcs( + pub async fn project_list_vpcs_by_id( + &self, + opctx: &OpContext, + authz_project: &authz::Project, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + opctx.authorize(authz::Action::ListChildren, authz_project).await?; + + use db::schema::vpc::dsl; + paginated(dsl::vpc, dsl::id, &pagparams) + .filter(dsl::time_deleted.is_null()) + .filter(dsl::project_id.eq(authz_project.id())) + .select(Vpc::as_select()) + .load_async(self.pool_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) + } + + pub async fn project_list_vpcs_by_name( &self, opctx: &OpContext, authz_project: &authz::Project, @@ -359,7 +377,25 @@ impl DataStore { .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } - pub async fn vpc_list_subnets( + pub async fn vpc_list_subnets_by_id( + &self, + opctx: &OpContext, + authz_vpc: &authz::Vpc, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + opctx.authorize(authz::Action::ListChildren, authz_vpc).await?; + + use db::schema::vpc_subnet::dsl; + paginated(dsl::vpc_subnet, dsl::id, &pagparams) + .filter(dsl::time_deleted.is_null()) + .filter(dsl::vpc_id.eq(authz_vpc.id())) + .select(VpcSubnet::as_select()) + .load_async(self.pool_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) + } + + pub async fn vpc_list_subnets_by_name( &self, opctx: &OpContext, authz_vpc: &authz::Vpc, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index dc02fd09e82..9ac5fd285b5 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -4075,31 +4075,44 @@ async fn snapshot_delete( }] async fn vpc_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query>, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_lookup = - nexus.project_lookup(&opctx, &query.project_selector)?; - let vpcs = nexus - .project_list_vpcs( - &opctx, - &project_lookup, - &data_page_params_for(&rqctx, &query.pagination)? - .map_name(|n| Name::ref_cast(n)), - ) - .await? - .into_iter() - .map(|p| p.into()) - .collect(); + let vpcs = match name_or_id_pagination(&query, &pag_params)? { + PaginatedBy::Id(pag_params, selector) => { + let project_lookup = nexus.project_lookup(&opctx, &selector)?; + nexus + .project_list_vpcs_by_id( + &opctx, + &project_lookup, + &pag_params, + ) + .await? + } + PaginatedBy::Name(pag_params, selector) => { + let project_lookup = nexus.project_lookup(&opctx, &selector)?; + nexus + .project_list_vpcs_by_name( + &opctx, + &project_lookup, + &pag_params.map_name(|n| Name::ref_cast(n)), + ) + .await? + } + } + .into_iter() + .map(|p| p.into()) + .collect(); - Ok(HttpResponseOk(ScanByName::results_page( - &query.pagination, + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, vpcs, - &marker_for_name, + &marker_for_name_or_id, )?)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -4130,7 +4143,7 @@ async fn vpc_list( let opctx = OpContext::for_external_api(&rqctx).await?; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let vpcs = nexus - .project_list_vpcs( + .project_list_vpcs_by_name( &opctx, &project_lookup, &data_page_params_for(&rqctx, &query)? @@ -4422,29 +4435,39 @@ async fn vpc_delete( }] async fn vpc_subnet_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query>, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let vpc_lookup = nexus.vpc_lookup(&opctx, &query.vpc_selector)?; - let vpcs = nexus - .vpc_list_subnets( - &opctx, - &vpc_lookup, - &data_page_params_for(&rqctx, &query.pagination)? - .map_name(|n| Name::ref_cast(n)), - ) - .await? - .into_iter() - .map(|vpc| vpc.into()) - .collect(); - Ok(HttpResponseOk(ScanByName::results_page( - &query.pagination, + let vpcs = match name_or_id_pagination(&query, &pag_params)? { + PaginatedBy::Id(pag_params, selector) => { + let vpc_lookup = nexus.vpc_lookup(&opctx, &selector)?; + nexus + .vpc_list_subnets_by_id(&opctx, &vpc_lookup, &pag_params) + .await? + } + PaginatedBy::Name(pag_params, selector) => { + let vpc_lookup = nexus.vpc_lookup(&opctx, &selector)?; + nexus + .vpc_list_subnets_by_name( + &opctx, + &vpc_lookup, + &pag_params.map_name(|n| Name::ref_cast(n)), + ) + .await? + } + } + .into_iter() + .map(|vpc| vpc.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, vpcs, - &marker_for_name, + &marker_for_name_or_id, )?)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -4476,7 +4499,7 @@ async fn vpc_subnet_list( ); let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; let vpcs = nexus - .vpc_list_subnets( + .vpc_list_subnets_by_name( &opctx, &vpc_lookup, &data_page_params_for(&rqctx, &query)? diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 276d2514f42..f352424a445 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -107,24 +107,7 @@ impl InstanceSelector { } } -#[derive(Deserialize, JsonSchema)] -pub struct InstanceList { - #[serde(flatten)] - pub pagination: PaginatedByName, - #[serde(flatten)] - pub project_selector: ProjectSelector, -} - -#[derive(Deserialize, JsonSchema)] -pub struct InstanceSerialConsole { - #[serde(flatten)] - pub project_selector: Option, - - #[serde(flatten)] - pub console_params: InstanceSerialConsoleRequest, -} - -#[derive(Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct VpcSelector { #[serde(flatten)] pub project_selector: Option, @@ -152,14 +135,6 @@ impl VpcSelector { } } -#[derive(Deserialize, JsonSchema)] -pub struct VpcList { - #[serde(flatten)] - pub pagination: PaginatedByName, - #[serde(flatten)] - pub project_selector: ProjectSelector, -} - #[derive(Deserialize, JsonSchema)] pub struct SubnetSelector { #[serde(flatten)] @@ -183,14 +158,6 @@ impl SubnetSelector { } } -#[derive(Deserialize, JsonSchema)] -pub struct SubnetList { - #[serde(flatten)] - pub pagination: PaginatedByName, - #[serde(flatten)] - pub vpc_selector: VpcSelector, -} - // Silos /// Create-time parameters for a [`Silo`](crate::external_api::views::Silo) From f3c1d85ec0fcddc904e2eeea375ba96903ff37ec Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 17 Jan 2023 12:15:36 -0500 Subject: [PATCH 19/45] Update authz tests --- nexus/tests/integration_tests/endpoints.rs | 10 +++++----- nexus/tests/output/uncovered-authz-endpoints.txt | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index c08db6b02cb..8535efc71c6 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -115,7 +115,7 @@ lazy_static! { pub static ref DEMO_PROJECT_URL_SNAPSHOTS: String = format!("/organizations/{}/projects/{}/snapshots", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_VPCS: String = - format!("/organizations/{}/projects/{}/vpcs", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("/v1/vpcs?organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_CREATE: params::ProjectCreate = params::ProjectCreate { identity: IdentityMetadataCreateParams { @@ -127,13 +127,13 @@ lazy_static! { // VPC used for testing pub static ref DEMO_VPC_NAME: Name = "demo-vpc".parse().unwrap(); pub static ref DEMO_VPC_URL: String = - format!("/organizations/{}/projects/{}/vpcs/{}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); + format!("/v1/vpcs/{}?organization={}&project={}", *DEMO_VPC_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_VPC_URL_FIREWALL_RULES: String = format!("/organizations/{}/projects/{}/vpcs/{}/firewall/rules", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_ROUTERS: String = format!("/organizations/{}/projects/{}/vpcs/{}/routers", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_SUBNETS: String = - format!("/organizations/{}/projects/{}/vpcs/{}/subnets", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); + format!("/v1/vpc-subnets?organization={}&project={}&vpc={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_CREATE: params::VpcCreate = params::VpcCreate { identity: IdentityMetadataCreateParams { @@ -148,9 +148,9 @@ lazy_static! { pub static ref DEMO_VPC_SUBNET_NAME: Name = "demo-vpc-subnet".parse().unwrap(); pub static ref DEMO_VPC_SUBNET_URL: String = - format!("{}/{}", *DEMO_VPC_URL_SUBNETS, *DEMO_VPC_SUBNET_NAME); + format!("/v1/vpc-subnets/{}?organization={}&project={}&vpc={}", *DEMO_VPC_SUBNET_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_SUBNET_INTERFACES_URL: String = - format!("{}/network-interfaces", *DEMO_VPC_SUBNET_URL); + format!("/organizations/{}/projects/{}/vpcs/{}/subnets/{}/network-interfaces", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_VPC_SUBNET_NAME); pub static ref DEMO_VPC_SUBNET_CREATE: params::VpcSubnetCreate = params::VpcSubnetCreate { identity: IdentityMetadataCreateParams { diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index 1f6005f6886..0cbb5da337d 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -2,6 +2,8 @@ API endpoints with no coverage in authz tests: organization_delete (delete "/organizations/{organization_name}") project_delete (delete "/organizations/{organization_name}/projects/{project_name}") instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") +vpc_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") +vpc_subnet_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") instance_view_by_id (get "/by-id/instances/{id}") organization_view_by_id (get "/by-id/organizations/{id}") project_view_by_id (get "/by-id/projects/{id}") @@ -16,6 +18,10 @@ instance_view (get "/organizations/{organization_n instance_serial_console (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console") instance_serial_console_stream (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console/stream") project_policy_view (get "/organizations/{organization_name}/projects/{project_name}/policy") +vpc_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs") +vpc_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") +vpc_subnet_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets") +vpc_subnet_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") device_auth_request (post "/device/auth") device_auth_confirm (post "/device/confirm") device_access_token (post "/device/token") @@ -30,7 +36,11 @@ instance_migrate (post "/organizations/{organization_n instance_reboot (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/reboot") instance_start (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start") instance_stop (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop") +vpc_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs") +vpc_subnet_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets") organization_update (put "/organizations/{organization_name}") organization_policy_update (put "/organizations/{organization_name}/policy") project_update (put "/organizations/{organization_name}/projects/{project_name}") project_policy_update (put "/organizations/{organization_name}/projects/{project_name}/policy") +vpc_update (put "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") +vpc_subnet_update (put "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") From 12396b539ef20c9651a986c807cd129ba4d08adf Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 17 Jan 2023 12:17:58 -0500 Subject: [PATCH 20/45] Update api spec --- openapi/nexus.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openapi/nexus.json b/openapi/nexus.json index 994533c4a0f..6f56d16369e 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8570,13 +8570,12 @@ "in": "query", "name": "sort_by", "schema": { - "$ref": "#/components/schemas/NameSortMode" + "$ref": "#/components/schemas/NameOrIdSortMode" } }, { "in": "query", "name": "vpc", - "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } @@ -8869,7 +8868,6 @@ { "in": "query", "name": "project", - "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } @@ -8878,7 +8876,7 @@ "in": "query", "name": "sort_by", "schema": { - "$ref": "#/components/schemas/NameSortMode" + "$ref": "#/components/schemas/NameOrIdSortMode" } } ], From a336a5b46e4a74dad90d6289348d93f261d5a275 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 18 Jan 2023 13:24:01 -0500 Subject: [PATCH 21/45] Update vpc tests --- nexus/tests/integration_tests/vpcs.rs | 44 ++++++++++++++++++--------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/nexus/tests/integration_tests/vpcs.rs b/nexus/tests/integration_tests/vpcs.rs index 5e6ffaca928..5023f50ec10 100644 --- a/nexus/tests/integration_tests/vpcs.rs +++ b/nexus/tests/integration_tests/vpcs.rs @@ -23,20 +23,34 @@ use omicron_nexus::external_api::{params, views::Vpc}; type ControlPlaneTestContext = nexus_test_utils::ControlPlaneTestContext; +static ORG_NAME: &str = "test-org"; +static PROJECT_NAME: &str = "springfield-squidport"; +static PROJECT_NAME_2: &str = "peeky-park"; + +fn get_vpc_url(vpc_name: &str) -> String { + format!( + "/v1/vpcs/{vpc_name}?organization={}&project={}", + ORG_NAME, PROJECT_NAME + ) +} + +fn get_subnet_url(vpc_name: &str, subnet_name: &str) -> String { + format!( + "/v1/vpc-subnets/{subnet_name}?organization={}&project={}&vpc={}", + ORG_NAME, PROJECT_NAME, vpc_name + ) +} + #[nexus_test] async fn test_vpcs(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; // Create a project that we'll use for testing. - let org_name = "test-org"; - create_organization(&client, &org_name).await; - let project_name = "springfield-squidport"; + create_organization(&client, &ORG_NAME).await; let vpcs_url = - format!("/organizations/{}/projects/{}/vpcs", org_name, project_name); - let _ = create_project(&client, &org_name, &project_name).await; - - let project_name2 = "pokemon"; - let _ = create_project(&client, &org_name, &project_name2).await; + format!("/v1/vpcs?organization={}&project={}", ORG_NAME, PROJECT_NAME); + let _ = create_project(&client, &ORG_NAME, &PROJECT_NAME).await; + let _ = create_project(&client, &ORG_NAME, &PROJECT_NAME_2).await; // List vpcs. We see the default VPC, and nothing else. let mut vpcs = vpcs_list(&client, &vpcs_url).await; @@ -46,7 +60,7 @@ async fn test_vpcs(cptestctx: &ControlPlaneTestContext) { let default_vpc = vpcs.remove(0); // Make sure we get a 404 if we fetch or delete one. - let vpc_url = format!("{}/just-rainsticks", vpcs_url); + let vpc_url = get_vpc_url("just-rainsticks"); for method in &[Method::GET, Method::DELETE] { let error: HttpErrorResponseBody = NexusRequest::expect_failure( client, @@ -88,7 +102,7 @@ async fn test_vpcs(cptestctx: &ControlPlaneTestContext) { // Create a VPC. let vpc_name = "just-rainsticks"; - let vpc = create_vpc(&client, org_name, project_name, vpc_name).await; + let vpc = create_vpc(&client, ORG_NAME, PROJECT_NAME, vpc_name).await; assert_eq!(vpc.identity.name, "just-rainsticks"); assert_eq!(vpc.identity.description, "vpc description"); assert_eq!(vpc.dns_name, "abc"); @@ -105,8 +119,8 @@ async fn test_vpcs(cptestctx: &ControlPlaneTestContext) { // Attempt to create a second VPC with a conflicting name. let error = create_vpc_with_error( &client, - org_name, - project_name, + ORG_NAME, + PROJECT_NAME, vpc_name, StatusCode::BAD_REQUEST, ) @@ -115,7 +129,7 @@ async fn test_vpcs(cptestctx: &ControlPlaneTestContext) { // creating a VPC with the same name in another project works, though let vpc2: Vpc = - create_vpc(&client, org_name, project_name2, vpc_name).await; + create_vpc(&client, ORG_NAME, PROJECT_NAME_2, vpc_name).await; assert_eq!(vpc2.identity.name, "just-rainsticks"); // List VPCs again and expect to find the one we just created. @@ -157,7 +171,7 @@ async fn test_vpcs(cptestctx: &ControlPlaneTestContext) { assert_eq!(error.message, "not found: vpc with name \"just-rainsticks\""); // new url with new name - let vpc_url = format!("{}/new-name", vpcs_url); + let vpc_url = get_vpc_url("new-name"); // Fetch the VPC again. It should have the updated properties. let vpc = vpc_get(&client, &vpc_url).await; @@ -181,7 +195,7 @@ async fn test_vpcs(cptestctx: &ControlPlaneTestContext) { assert_eq!(error.message, "VPC cannot be deleted while VPC Subnets exist",); // Delete the default VPC Subnet and VPC. - let default_subnet_url = format!("{vpc_url}/subnets/default"); + let default_subnet_url = get_subnet_url("new-name", "default"); NexusRequest::object_delete(client, &default_subnet_url) .authn_as(AuthnMode::PrivilegedUser) .execute() From 6cff4f932ceafbe88585e7eca17c2ac34aa4acbc Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 25 Jan 2023 00:10:42 -0500 Subject: [PATCH 22/45] Add comments, housekeeping --- nexus/src/app/vpc.rs | 1 - nexus/src/external_api/http_entrypoints.rs | 20 ++++++++++++++------ openapi/nexus.json | 10 ++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/nexus/src/app/vpc.rs b/nexus/src/app/vpc.rs index 6cfd7343a5e..3fc64efc5c7 100644 --- a/nexus/src/app/vpc.rs +++ b/nexus/src/app/vpc.rs @@ -19,7 +19,6 @@ use nexus_defaults as defaults; use omicron_common::api::external; use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::CreateResult; -use omicron_common::api::external::DataPageParams; use omicron_common::api::external::DeleteResult; use omicron_common::api::external::Error; use omicron_common::api::external::InternalContext; diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index a7d63a345d5..cc42fde54bb 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -4347,6 +4347,7 @@ async fn snapshot_delete( // VPCs +/// List VPCs #[endpoint { method = GET, path = "/v1/vpcs", @@ -4426,6 +4427,7 @@ async fn vpc_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Create a VPC #[endpoint { method = POST, path = "/v1/vpcs", @@ -4465,10 +4467,10 @@ async fn vpc_create( new_vpc: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let new_vpc_params = &new_vpc.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let new_vpc_params = &new_vpc.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( Some(path.organization_name.into()), @@ -4573,6 +4575,7 @@ async fn vpc_view_by_id( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Update a VPC #[endpoint { method = PUT, path = "/v1/vpcs/{vpc}", @@ -4636,6 +4639,7 @@ async fn vpc_update( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Delete a VPC #[endpoint { method = DELETE, path = "/v1/vpcs/{vpc}", @@ -4692,6 +4696,7 @@ async fn vpc_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Fetch a subnet #[endpoint { method = GET, path = "/v1/vpc-subnets", @@ -4751,11 +4756,10 @@ async fn vpc_subnet_list( ); let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; let vpcs = nexus - .vpc_list_subnets_by_name( + .vpc_subnet_list( &opctx, &vpc_lookup, - &data_page_params_for(&rqctx, &query)? - .map_name(|n| Name::ref_cast(n)), + &PaginatedBy::Name(data_page_params_for(&rqctx, &query)?), ) .await? .into_iter() @@ -4770,6 +4774,7 @@ async fn vpc_subnet_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Create a subnet #[endpoint { method = POST, path = "/v1/vpc-subnets", @@ -4826,6 +4831,7 @@ async fn vpc_subnet_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Fetch a subnet #[endpoint { method = GET, path = "/v1/vpc-subnets/{subnet}", @@ -4920,6 +4926,7 @@ async fn vpc_subnet_view_by_id( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Delete a subnet #[endpoint { method = DELETE, path = "/v1/vpc-subnets/{subnet}", @@ -4979,6 +4986,7 @@ async fn vpc_subnet_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Update a subnet #[endpoint { method = PUT, path = "/v1/vpc-subnets/{subnet}", diff --git a/openapi/nexus.json b/openapi/nexus.json index 745d025c23d..26cb5e39be7 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -9205,6 +9205,7 @@ "tags": [ "vpcs" ], + "summary": "Fetch a subnet", "operationId": "vpc_subnet_list_v1", "parameters": [ { @@ -9280,6 +9281,7 @@ "tags": [ "vpcs" ], + "summary": "Create a subnet", "operationId": "vpc_subnet_create_v1", "parameters": [ { @@ -9340,6 +9342,7 @@ "tags": [ "vpcs" ], + "summary": "Fetch a subnet", "operationId": "vpc_subnet_view_v1", "parameters": [ { @@ -9396,6 +9399,7 @@ "tags": [ "vpcs" ], + "summary": "Update a subnet", "operationId": "vpc_subnet_update_v1", "parameters": [ { @@ -9461,6 +9465,7 @@ "tags": [ "vpcs" ], + "summary": "Delete a subnet", "operationId": "vpc_subnet_delete_v1", "parameters": [ { @@ -9511,6 +9516,7 @@ "tags": [ "vpcs" ], + "summary": "List VPCs", "operationId": "vpc_list_v1", "parameters": [ { @@ -9579,6 +9585,7 @@ "tags": [ "vpcs" ], + "summary": "Create a VPC", "operationId": "vpc_create_v1", "parameters": [ { @@ -9632,6 +9639,7 @@ "tags": [ "vpcs" ], + "summary": "Fetch a VPC", "operationId": "vpc_view_v1", "parameters": [ { @@ -9680,6 +9688,7 @@ "tags": [ "vpcs" ], + "summary": "Update a VPC", "operationId": "vpc_update_v1", "parameters": [ { @@ -9738,6 +9747,7 @@ "tags": [ "vpcs" ], + "summary": "Delete a VPC", "operationId": "vpc_delete_v1", "parameters": [ { From 88cff99bc6e3667e5a7b2da58a7d18cea6a57ca1 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 26 Jan 2023 10:14:09 -0500 Subject: [PATCH 23/45] Update vpc router and routes --- common/src/api/external/mod.rs | 18 - nexus/db-model/src/vpc_route.rs | 7 +- nexus/src/app/sagas/vpc_create.rs | 3 +- nexus/src/app/vpc_router.rs | 270 ++++----- nexus/src/db/datastore/vpc.rs | 60 +- nexus/src/external_api/http_entrypoints.rs | 650 ++++++++++++++++----- nexus/types/src/external_api/params.rs | 87 ++- 7 files changed, 747 insertions(+), 348 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 4919274c004..fc19fb17766 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -1513,24 +1513,6 @@ pub struct RouterRoute { pub destination: RouteDestination, } -/// Create-time parameters for a [`RouterRoute`] -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct RouterRouteCreateParams { - #[serde(flatten)] - pub identity: IdentityMetadataCreateParams, - pub target: RouteTarget, - pub destination: RouteDestination, -} - -/// Updateable properties of a [`RouterRoute`] -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct RouterRouteUpdateParams { - #[serde(flatten)] - pub identity: IdentityMetadataUpdateParams, - pub target: RouteTarget, - pub destination: RouteDestination, -} - /// A single rule in a VPC firewall #[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct VpcFirewallRule { diff --git a/nexus/db-model/src/vpc_route.rs b/nexus/db-model/src/vpc_route.rs index c8316bef14b..39c918eeecb 100644 --- a/nexus/db-model/src/vpc_route.rs +++ b/nexus/db-model/src/vpc_route.rs @@ -11,6 +11,7 @@ use diesel::deserialize::{self, FromSql}; use diesel::pg::Pg; use diesel::serialize::{self, ToSql}; use diesel::sql_types; +use nexus_types::external_api::params; use nexus_types::identity::Resource; use omicron_common::api::external; use std::io::Write; @@ -115,7 +116,7 @@ impl RouterRoute { route_id: Uuid, vpc_router_id: Uuid, kind: external::RouterRouteKind, - params: external::RouterRouteCreateParams, + params: params::RouterRouteCreate, ) -> Self { let identity = RouterRouteIdentity::new(route_id, params.identity); Self { @@ -150,8 +151,8 @@ pub struct RouterRouteUpdate { pub destination: RouteDestination, } -impl From for RouterRouteUpdate { - fn from(params: external::RouterRouteUpdateParams) -> Self { +impl From for RouterRouteUpdate { + fn from(params: params::RouterRouteUpdate) -> Self { Self { name: params.identity.name.map(Name), description: params.identity.description, diff --git a/nexus/src/app/sagas/vpc_create.rs b/nexus/src/app/sagas/vpc_create.rs index a816b8c0b87..44bbaee309d 100644 --- a/nexus/src/app/sagas/vpc_create.rs +++ b/nexus/src/app/sagas/vpc_create.rs @@ -17,7 +17,6 @@ use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::LookupType; use omicron_common::api::external::RouteDestination; use omicron_common::api::external::RouteTarget; -use omicron_common::api::external::RouterRouteCreateParams; use omicron_common::api::external::RouterRouteKind; use serde::Deserialize; use serde::Serialize; @@ -221,7 +220,7 @@ async fn svc_create_route( default_route_id, system_router_id, RouterRouteKind::Default, - RouterRouteCreateParams { + params::RouterRouteCreate { identity: IdentityMetadataCreateParams { name: "default".parse().unwrap(), description: "The default route of a vpc".to_string(), diff --git a/nexus/src/app/vpc_router.rs b/nexus/src/app/vpc_router.rs index 9af86c6ed24..f6d14fe2fa4 100644 --- a/nexus/src/app/vpc_router.rs +++ b/nexus/src/app/vpc_router.rs @@ -7,42 +7,71 @@ use crate::authz; use crate::context::OpContext; use crate::db; +use crate::db::lookup; use crate::db::lookup::LookupPath; use crate::db::model::Name; use crate::db::model::RouterRoute; use crate::db::model::VpcRouter; use crate::db::model::VpcRouterKind; use crate::external_api::params; +use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::CreateResult; -use omicron_common::api::external::DataPageParams; use omicron_common::api::external::DeleteResult; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; -use omicron_common::api::external::RouterRouteCreateParams; +use omicron_common::api::external::NameOrId; use omicron_common::api::external::RouterRouteKind; -use omicron_common::api::external::RouterRouteUpdateParams; use omicron_common::api::external::UpdateResult; +use ref_cast::RefCast; use uuid::Uuid; impl super::Nexus { // Routers + pub fn vpc_router_lookup<'a>( + &'a self, + opctx: &'a OpContext, + router_selector: &'a params::RouterSelector, + ) -> LookupResult> { + match router_selector { + params::RouterSelector { + router: NameOrId::Id(id), + vpc_selector: None, + } => { + let router = LookupPath::new(opctx, &self.db_datastore) + .vpc_router_id(*id); + Ok(router) + } + params::RouterSelector { + router: NameOrId::Name(name), + vpc_selector: Some(vpc_selector), + } => { + let router = self + .vpc_lookup(opctx, vpc_selector)? + .vpc_router_name(Name::ref_cast(name)); + Ok(router) + } + params::RouterSelector { + router: NameOrId::Id(_), + vpc_selector: Some(_), + } => Err(Error::invalid_request( + "when providing subnet as an ID, vpc should not be specified", + )), + _ => Err(Error::invalid_request( + "router should either be an ID or vpc should be specified", + )), + } + } pub async fn vpc_create_router( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, + vpc_lookup: &lookup::Vpc<'_>, kind: &VpcRouterKind, params: ¶ms::VpcRouterCreate, ) -> CreateResult { - let (.., authz_vpc) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .lookup_for(authz::Action::CreateChild) - .await?; + let (.., authz_vpc) = + vpc_lookup.lookup_for(authz::Action::CreateChild).await?; let id = Uuid::new_v4(); let router = db::model::VpcRouter::new( id, @@ -57,73 +86,29 @@ impl super::Nexus { Ok(router) } - pub async fn vpc_list_routers( + pub async fn vpc_router_list( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - pagparams: &DataPageParams<'_, Name>, + vpc_lookup: &lookup::Vpc<'_>, + pagparams: &PaginatedBy<'_>, ) -> ListResultVec { - let (.., authz_vpc) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .lookup_for(authz::Action::ListChildren) - .await?; + let (.., authz_vpc) = + vpc_lookup.lookup_for(authz::Action::ListChildren).await?; let routers = self .db_datastore - .vpc_list_routers(opctx, &authz_vpc, pagparams) + .vpc_router_list(opctx, &authz_vpc, pagparams) .await?; Ok(routers) } - pub async fn vpc_router_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, - ) -> LookupResult { - let (.., db_router) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_router_name(router_name) - .fetch() - .await?; - Ok(db_router) - } - - pub async fn vpc_router_fetch_by_id( - &self, - opctx: &OpContext, - vpc_router_id: &Uuid, - ) -> LookupResult { - let (.., db_router) = LookupPath::new(opctx, &self.db_datastore) - .vpc_router_id(*vpc_router_id) - .fetch() - .await?; - Ok(db_router) - } - pub async fn vpc_update_router( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, + vpc_router_lookup: &lookup::VpcRouter<'_>, params: ¶ms::VpcRouterUpdate, ) -> UpdateResult { - let (.., authz_router) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_router_name(router_name) - .lookup_for(authz::Action::Modify) - .await?; + let (.., authz_router) = + vpc_router_lookup.lookup_for(authz::Action::Modify).await?; self.db_datastore .vpc_update_router(opctx, &authz_router, params.clone().into()) .await @@ -135,19 +120,10 @@ impl super::Nexus { pub async fn vpc_delete_router( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, + vpc_router_lookup: &lookup::VpcRouter<'_>, ) -> DeleteResult { let (.., authz_router, db_router) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_router_name(router_name) - .fetch() - .await?; + vpc_router_lookup.fetch_for(authz::Action::Delete).await?; // TODO-performance shouldn't this check be part of the "update" // database query? This shouldn't affect correctness, assuming that a // router kind cannot be changed, but it might be able to save us a @@ -162,24 +138,50 @@ impl super::Nexus { // Routes - #[allow(clippy::too_many_arguments)] + pub fn vpc_router_route_lookup<'a>( + &'a self, + opctx: &'a OpContext, + route_selector: &'a params::RouteSelector, + ) -> LookupResult> { + match route_selector { + params::RouteSelector { + route: NameOrId::Id(id), + router_selector: None, + } => { + let route = LookupPath::new(opctx, &self.db_datastore) + .router_route_id(*id); + Ok(route) + } + params::RouteSelector { + route: NameOrId::Name(name), + router_selector: Some(selector), + } => { + let route = self + .vpc_router_lookup(opctx, selector)? + .router_route_name(Name::ref_cast(name)); + Ok(route) + } + params::RouteSelector { + route: NameOrId::Id(_), + router_selector: Some(_), + } => Err(Error::invalid_request( + "when providing subnet as an ID, vpc should not be specified", + )), + _ => Err(Error::invalid_request( + "router should either be an ID or vpc should be specified", + )), + } + } + pub async fn router_create_route( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, + router_lookup: &lookup::VpcRouter<'_>, kind: &RouterRouteKind, - params: &RouterRouteCreateParams, + params: ¶ms::RouterRouteCreate, ) -> CreateResult { - let (.., authz_router) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_router_name(router_name) - .lookup_for(authz::Action::CreateChild) - .await?; + let (.., authz_router) = + router_lookup.lookup_for(authz::Action::CreateChild).await?; let id = Uuid::new_v4(); let route = db::model::RouterRoute::new( id, @@ -194,79 +196,27 @@ impl super::Nexus { Ok(route) } - pub async fn router_list_routes( + pub async fn vpc_router_route_list( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, - pagparams: &DataPageParams<'_, Name>, + vpc_router_lookup: &lookup::VpcRouter<'_>, + pagparams: &PaginatedBy<'_>, ) -> ListResultVec { - let (.., authz_router) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_router_name(router_name) - .lookup_for(authz::Action::ListChildren) - .await?; + let (.., authz_router) = + vpc_router_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore - .router_list_routes(opctx, &authz_router, pagparams) + .vpc_router_route_list(opctx, &authz_router, pagparams) .await } - pub async fn route_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, - route_name: &Name, - ) -> LookupResult { - let (.., db_route) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_router_name(router_name) - .router_route_name(route_name) - .fetch() - .await?; - Ok(db_route) - } - - pub async fn route_fetch_by_id( - &self, - opctx: &OpContext, - route_id: &Uuid, - ) -> LookupResult { - let (.., db_route) = LookupPath::new(opctx, &self.db_datastore) - .router_route_id(*route_id) - .fetch() - .await?; - Ok(db_route) - } - - #[allow(clippy::too_many_arguments)] pub async fn router_update_route( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, - route_name: &Name, - params: &RouterRouteUpdateParams, + route_lookup: &lookup::RouterRoute<'_>, + params: ¶ms::RouterRouteUpdate, ) -> UpdateResult { - let (.., authz_route, db_route) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_router_name(router_name) - .router_route_name(route_name) - .fetch() - .await?; + let (.., vpc, _, authz_route, db_route) = + route_lookup.fetch_for(authz::Action::Modify).await?; // TODO: Write a test for this once there's a way to test it (i.e. // subnets automatically register to the system router table) match db_route.kind.0 { @@ -276,7 +226,8 @@ impl super::Nexus { internal_message: format!( "routes of type {} from the system table of VPC {:?} \ are not modifiable", - db_route.kind.0, vpc_name + db_route.kind.0, + vpc.id() ), }) } @@ -289,21 +240,10 @@ impl super::Nexus { pub async fn router_delete_route( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - vpc_name: &Name, - router_name: &Name, - route_name: &Name, + route_lookup: &lookup::RouterRoute<'_>, ) -> DeleteResult { let (.., authz_route, db_route) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .vpc_name(vpc_name) - .vpc_router_name(router_name) - .router_route_name(route_name) - .fetch() - .await?; + route_lookup.fetch_for(authz::Action::Delete).await?; // Only custom routes can be deleted // TODO Shouldn't this constraint be checked by the database query? diff --git a/nexus/src/db/datastore/vpc.rs b/nexus/src/db/datastore/vpc.rs index 58124e8e3ad..5f70faa8be1 100644 --- a/nexus/src/db/datastore/vpc.rs +++ b/nexus/src/db/datastore/vpc.rs @@ -537,24 +537,31 @@ impl DataStore { .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } - pub async fn vpc_list_routers( + pub async fn vpc_router_list( &self, opctx: &OpContext, authz_vpc: &authz::Vpc, - pagparams: &DataPageParams<'_, Name>, + pagparams: &PaginatedBy<'_>, ) -> ListResultVec { opctx.authorize(authz::Action::ListChildren, authz_vpc).await?; use db::schema::vpc_router::dsl; - paginated(dsl::vpc_router, dsl::name, pagparams) - .filter(dsl::time_deleted.is_null()) - .filter(dsl::vpc_id.eq(authz_vpc.id())) - .select(VpcRouter::as_select()) - .load_async::( - self.pool_authorized(opctx).await?, - ) - .await - .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) + match pagparams { + PaginatedBy::Id(pagparams) => { + paginated(dsl::vpc_router, dsl::id, pagparams) + } + PaginatedBy::Name(pagparams) => paginated( + dsl::vpc_router, + dsl::name, + &pagparams.map_name(|n| Name::ref_cast(n)), + ), + } + .filter(dsl::time_deleted.is_null()) + .filter(dsl::vpc_id.eq(authz_vpc.id())) + .select(VpcRouter::as_select()) + .load_async::(self.pool_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } pub async fn vpc_create_router( @@ -641,24 +648,33 @@ impl DataStore { }) } - pub async fn router_list_routes( + pub async fn vpc_router_route_list( &self, opctx: &OpContext, authz_router: &authz::VpcRouter, - pagparams: &DataPageParams<'_, Name>, + pagparams: &PaginatedBy<'_>, ) -> ListResultVec { opctx.authorize(authz::Action::ListChildren, authz_router).await?; use db::schema::router_route::dsl; - paginated(dsl::router_route, dsl::name, pagparams) - .filter(dsl::time_deleted.is_null()) - .filter(dsl::vpc_router_id.eq(authz_router.id())) - .select(RouterRoute::as_select()) - .load_async::( - self.pool_authorized(opctx).await?, - ) - .await - .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) + match pagparams { + PaginatedBy::Id(pagparams) => { + paginated(dsl::router_route, dsl::id, pagparams) + } + PaginatedBy::Name(pagparams) => paginated( + dsl::router_route, + dsl::name, + &pagparams.map_name(|n| Name::ref_cast(n)), + ), + } + .filter(dsl::time_deleted.is_null()) + .filter(dsl::vpc_router_id.eq(authz_router.id())) + .select(RouterRoute::as_select()) + .load_async::( + self.pool_authorized(opctx).await?, + ) + .await + .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } pub async fn router_create_route( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index cc42fde54bb..18fd06bb399 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -61,9 +61,7 @@ use omicron_common::api::external::InternalContext; use omicron_common::api::external::NameOrId; use omicron_common::api::external::NetworkInterface; use omicron_common::api::external::RouterRoute; -use omicron_common::api::external::RouterRouteCreateParams; use omicron_common::api::external::RouterRouteKind; -use omicron_common::api::external::RouterRouteUpdateParams; use omicron_common::api::external::Saga; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use omicron_common::api::external::VpcFirewallRules; @@ -238,6 +236,12 @@ pub fn external_api() -> NexusApiDescription { api.register(vpc_router_delete)?; api.register(vpc_router_update)?; + api.register(vpc_router_list_v1)?; + api.register(vpc_router_view_v1)?; + api.register(vpc_router_create_v1)?; + api.register(vpc_router_delete_v1)?; + api.register(vpc_router_update_v1)?; + api.register(vpc_router_route_list)?; api.register(vpc_router_route_view)?; api.register(vpc_router_route_view_by_id)?; @@ -245,6 +249,12 @@ pub fn external_api() -> NexusApiDescription { api.register(vpc_router_route_delete)?; api.register(vpc_router_route_update)?; + api.register(vpc_router_route_list_v1)?; + api.register(vpc_router_route_view_v1)?; + api.register(vpc_router_route_create_v1)?; + api.register(vpc_router_route_delete_v1)?; + api.register(vpc_router_route_update_v1)?; + api.register(vpc_firewall_rules_view)?; api.register(vpc_firewall_rules_update)?; @@ -4384,7 +4394,7 @@ async fn vpc_list_v1( } /// List VPCs -/// Use `GET /v1/vpcs` instead. +/// Use `GET /v1/vpcs` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs", @@ -4454,7 +4464,7 @@ async fn vpc_create_v1( } /// Create a VPC -/// Use `POST /v1/vpcs` instead. +/// Use `POST /v1/vpcs` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/vpcs", @@ -4522,7 +4532,7 @@ struct VpcPathParam { } /// Fetch a VPC -/// Use `GET /v1/vpcs/{vpc}` instead. +/// Use `GET /v1/vpcs/{vpc}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}", @@ -4551,7 +4561,7 @@ async fn vpc_view( } /// Fetch a VPC -/// Use `GET /v1/vpcs/{id}` instead. +/// Use `GET /v1/vpcs/{id}` instead #[endpoint { method = GET, path = "/by-id/vpcs/{id}", @@ -4608,7 +4618,7 @@ async fn vpc_update_v1( } /// Update a VPC -/// Use `PUT /v1/vpcs/{vpc}` instead. +/// Use `PUT /v1/vpcs/{vpc}` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}", @@ -4668,7 +4678,7 @@ async fn vpc_delete_v1( } /// Delete a VPC -/// Use `DELETE /v1/vpcs/{vpc}` instead. +/// Use `DELETE /v1/vpcs/{vpc}` instead #[endpoint { method = DELETE, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}", @@ -4731,7 +4741,7 @@ async fn vpc_subnet_list_v1( } /// List subnets -/// Use `GET /v1/vpc-subnets` instead. +/// Use `GET /v1/vpc-subnets` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets", @@ -4744,10 +4754,10 @@ async fn vpc_subnet_list( path_params: Path, ) -> Result>, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let query = query_params.into_inner(); - let path = path_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let vpc_selector = params::VpcSelector::new( Some(path.organization_name.into()), @@ -4786,10 +4796,10 @@ async fn vpc_subnet_create_v1( create_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let query = query_params.into_inner(); - let create = create_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let create = create_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let vpc_lookup = nexus.vpc_lookup(&opctx, &query)?; let subnet = @@ -4800,7 +4810,7 @@ async fn vpc_subnet_create_v1( } /// Create a subnet -/// Use `POST /v1/vpc-subnets` instead. +/// Use `POST /v1/vpc-subnets` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets", @@ -4813,9 +4823,9 @@ async fn vpc_subnet_create( create_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let vpc_selector = params::VpcSelector::new( Some(path.organization_name.into()), @@ -4843,10 +4853,10 @@ async fn vpc_subnet_view_v1( query_params: Query, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let query = query_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let subnet_selector = params::SubnetSelector { vpc_selector: Some(query), @@ -4869,7 +4879,7 @@ struct VpcSubnetPathParam { } /// Fetch a subnet -/// Use `GET /v1/vpc-subnets/{subnet}` instead. +/// Use `GET /v1/vpc-subnets/{subnet}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}", @@ -4881,10 +4891,10 @@ async fn vpc_subnet_view( path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); let subnet_selector = params::SubnetSelector::new( Some(path.organization_name.into()), Some(path.project_name.into()), @@ -4899,7 +4909,7 @@ async fn vpc_subnet_view( } /// Fetch a subnet by id -/// Use `GET /v1/vpc-subnets/{id}` instead. +/// Use `GET /v1/vpc-subnets/{id}` instead #[endpoint { method = GET, path = "/by-id/vpc-subnets/{id}", @@ -4938,10 +4948,10 @@ async fn vpc_subnet_delete_v1( query_params: Query, ) -> Result { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let query = query_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let subnet_selector = params::SubnetSelector { vpc_selector: query.vpc_selector, @@ -4968,9 +4978,9 @@ async fn vpc_subnet_delete( path_params: Path, ) -> Result { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let subnet_selector = params::SubnetSelector::new( Some(path.organization_name.into()), @@ -4999,11 +5009,11 @@ async fn vpc_subnet_update_v1( subnet_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let query = query_params.into_inner(); - let subnet_params = subnet_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let subnet_params = subnet_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let subnet_selector = params::SubnetSelector { vpc_selector: query.vpc_selector, @@ -5033,9 +5043,9 @@ async fn vpc_subnet_update( subnet_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let subnet_selector = params::SubnetSelector::new( Some(path.organization_name.into()), @@ -5170,10 +5180,46 @@ async fn vpc_firewall_rules_update( // VPC Routers /// List routers +#[endpoint { + method = GET, + path = "/v1/vpc-routers", + tags = ["vpcs"], +}] +async fn vpc_router_list_v1( + rqctx: RequestContext>, + query_params: Query>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanByNameOrId::from_query(&query)?; + let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; + let vpc_lookup = nexus.vpc_lookup(&opctx, &scan_params.selector)?; + let routers = nexus + .vpc_router_list(&opctx, &vpc_lookup, &paginated_by) + .await? + .into_iter() + .map(|s| s.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + routers, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// List routers +/// Use `GET /v1/vpc-routers` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers", tags = ["vpcs"], + deprecated = true, }] async fn vpc_router_list( rqctx: RequestContext>, @@ -5181,19 +5227,22 @@ async fn vpc_router_list( path_params: Path, ) -> Result>, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let query = query_params.into_inner(); - let path = path_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_selector = params::VpcSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.vpc_name.into(), + ); + let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; let routers = nexus - .vpc_list_routers( + .vpc_router_list( &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &data_page_params_for(&rqctx, &query)? - .map_name(|n| Name::ref_cast(n)), + &vpc_lookup, + &PaginatedBy::Name(data_page_params_for(&rqctx, &query)?), ) .await? .into_iter() @@ -5208,6 +5257,34 @@ async fn vpc_router_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get a router +#[endpoint { + method = GET, + path = "/v1/vpc-routers/{router}", + tags = ["vpcs"], +}] +async fn vpc_router_view_v1( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let opctx = OpContext::for_external_api(&rqctx).await?; + let router_selector = params::RouterSelector { + vpc_selector: Some(query), + router: path.router, + }; + let (.., vpc_router) = + nexus.vpc_router_lookup(&opctx, &router_selector)?.fetch().await?; + Ok(HttpResponseOk(vpc_router.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for VPC Router requests #[derive(Deserialize, JsonSchema)] struct VpcRouterPathParam { @@ -5218,29 +5295,32 @@ struct VpcRouterPathParam { } /// Get a router +/// Use `GET /v1/vpc-routers/{router}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}", tags = ["vpcs"], + deprecated = true, }] async fn vpc_router_view( rqctx: RequestContext>, path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let vpc_router = nexus - .vpc_router_fetch( - &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.router_name, - ) - .await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let router_selector = params::RouterSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + path.router_name.into(), + ); + let router_lookup = + nexus.vpc_router_lookup(&opctx, &router_selector)?; + let (.., vpc_router) = router_lookup.fetch().await?; + Ok(HttpResponseOk(vpc_router.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -5257,22 +5337,60 @@ async fn vpc_router_view_by_id( path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let router = nexus.vpc_router_fetch_by_id(&opctx, id).await?; - Ok(HttpResponseOk(router.into())) + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let router_selector = params::RouterSelector { + vpc_selector: None, + router: path.id.into(), + }; + let router_lookup = + nexus.vpc_router_lookup(&opctx, &router_selector)?; + let (.., vpc_router) = router_lookup.fetch().await?; + Ok(HttpResponseOk(vpc_router.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +// Create a router +#[endpoint { + method = POST, + path = "/v1/vpc-routers", + tags = ["vpcs"], +}] +async fn vpc_router_create_v1( + rqctx: RequestContext>, + query_params: Query, + create_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let create = create_params.into_inner(); + let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_lookup = nexus.vpc_lookup(&opctx, &query)?; + let router = nexus + .vpc_create_router( + &opctx, + &vpc_lookup, + &db::model::VpcRouterKind::Custom, + &create, + ) + .await?; + Ok(HttpResponseCreated(router.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Create a router +/// Use `POST /v1/vpc-routers` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers", tags = ["vpcs"], + deprecated = true, }] async fn vpc_router_create( rqctx: RequestContext>, @@ -5280,16 +5398,20 @@ async fn vpc_router_create( create_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc_selector = params::VpcSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.vpc_name.into(), + ); + let vpc_lookup = nexus.vpc_lookup(&opctx, &vpc_selector)?; let router = nexus .vpc_create_router( &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, + &vpc_lookup, &db::model::VpcRouterKind::Custom, &create_params.into_inner(), ) @@ -5305,34 +5427,99 @@ async fn vpc_router_create( path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}", tags = ["vpcs"], }] +async fn vpc_router_delete_v1( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let opctx = OpContext::for_external_api(&rqctx).await?; + let router_selector = params::RouterSelector { + vpc_selector: query.vpc_selector, + router: path.router, + }; + let router_lookup = + nexus.vpc_router_lookup(&opctx, &router_selector)?; + nexus.vpc_delete_router(&opctx, &router_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Delete a router +/// Use `DELETE /v1/vpc-router/{router}` instead +#[endpoint { + method = DELETE, + path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}", + tags = ["vpcs"], + deprecated = true, +}] async fn vpc_router_delete( rqctx: RequestContext>, path_params: Path, ) -> Result { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; - nexus - .vpc_delete_router( - &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.router_name, - ) - .await?; + let router_selector = params::RouterSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + path.router_name.into(), + ); + let router_lookup = + nexus.vpc_router_lookup(&opctx, &router_selector)?; + nexus.vpc_delete_router(&opctx, &router_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Update a router +#[endpoint { + method = PUT, + path = "/v1/vpc-router/{router}", + tags = ["vpcs"], +}] +async fn vpc_router_update_v1( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, + router_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let router_params = router_params.into_inner(); + let opctx = OpContext::for_external_api(&rqctx).await?; + let router_selector = params::RouterSelector { + vpc_selector: query.vpc_selector, + router: path.router, + }; + let router_lookup = + nexus.vpc_router_lookup(&opctx, &router_selector)?; + let router = nexus + .vpc_update_router(&opctx, &router_lookup, &router_params) + .await?; + Ok(HttpResponseOk(router.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Update a router #[endpoint { method = PUT, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}", tags = ["vpcs"], + deprecated = true, }] async fn vpc_router_update( rqctx: RequestContext>, @@ -5340,17 +5527,22 @@ async fn vpc_router_update( router_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; + let router_selector = params::RouterSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + path.router_name.into(), + ); + let router_lookup = + nexus.vpc_router_lookup(&opctx, &router_selector)?; let router = nexus .vpc_update_router( &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.router_name, + &router_lookup, &router_params.into_inner(), ) .await?; @@ -5359,6 +5551,43 @@ async fn vpc_router_update( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +// List routes +/// +/// List the routes associated with a router in a particular VPC. +#[endpoint { + method = GET, + path = "/v1/vpc-router-routes/", + tags = ["vpcs"], +}] +async fn vpc_router_route_list_v1( + rqctx: RequestContext>, + query_params: Query>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanByNameOrId::from_query(&query)?; + let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; + let router_lookup = + nexus.vpc_router_lookup(&opctx, &scan_params.selector)?; + let routes = nexus + .vpc_router_route_list(&opctx, &router_lookup, &paginated_by) + .await? + .into_iter() + .map(|route| route.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + routes, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + // Vpc Router Routes /// List routes @@ -5375,20 +5604,24 @@ async fn vpc_router_route_list( path_params: Path, ) -> Result>, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let query = query_params.into_inner(); - let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let path = path_params.into_inner(); + let router_selector = params::RouterSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + path.router_name.into(), + ); + let router_lookup = + nexus.vpc_router_lookup(&opctx, &router_selector)?; let routes = nexus - .router_list_routes( + .vpc_router_route_list( &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.router_name, - &data_page_params_for(&rqctx, &query)? - .map_name(|n| Name::ref_cast(n)), + &router_lookup, + &PaginatedBy::Name(data_page_params_for(&rqctx, &query)?), ) .await? .into_iter() @@ -5403,6 +5636,36 @@ async fn vpc_router_route_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Fetch a route +#[endpoint { + method = GET, + path = "/v1/vpc-router-routes/{route}", + tags = ["vpcs"], +}] +async fn vpc_router_route_view_v1( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let route_selector = params::RouteSelector { + router_selector: Some(query), + route: path.route, + }; + let (.., route) = nexus + .vpc_router_route_lookup(&opctx, &route_selector)? + .fetch() + .await?; + Ok(HttpResponseOk(route.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Router Route requests #[derive(Deserialize, JsonSchema)] struct RouterRoutePathParam { @@ -5414,6 +5677,7 @@ struct RouterRoutePathParam { } /// Fetch a route +/// Use `GET /v1/vpc-router-routes/{route}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}", @@ -5424,19 +5688,20 @@ async fn vpc_router_route_view( path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let route = nexus - .route_fetch( - &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.router_name, - &path.route_name, - ) + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let route_selector = params::RouteSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + Some(path.router_name.into()), + path.route_name.into(), + ); + let (.., route) = nexus + .vpc_router_route_lookup(&opctx, &route_selector)? + .fetch() .await?; Ok(HttpResponseOk(route.into())) }; @@ -5454,17 +5719,54 @@ async fn vpc_router_route_view_by_id( path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let route = nexus.route_fetch_by_id(&opctx, id).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let route_selector = params::RouteSelector { + router_selector: None, + route: path.id.into(), + }; + let (.., route) = nexus + .vpc_router_route_lookup(&opctx, &route_selector)? + .fetch() + .await?; Ok(HttpResponseOk(route.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Create a router +#[endpoint { + method = POST, + path = "/v1/vpc-router-routes", + tags = ["vpcs"], +}] +async fn vpc_router_route_create_v1( + rqctx: RequestContext>, + query_params: Query, + create_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let create = create_params.into_inner(); + let router_lookup = nexus.vpc_router_lookup(&opctx, &query)?; + let route = nexus + .router_create_route( + &opctx, + &router_lookup, + &RouterRouteKind::Custom, + &create, + ) + .await?; + Ok(HttpResponseCreated(route.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a router #[endpoint { method = POST, @@ -5474,20 +5776,25 @@ async fn vpc_router_route_view_by_id( async fn vpc_router_route_create( rqctx: RequestContext>, path_params: Path, - create_params: TypedBody, + create_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let router_selector = params::RouterSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + path.router_name.into(), + ); + let router_lookup = + nexus.vpc_router_lookup(&opctx, &router_selector)?; let route = nexus .router_create_route( &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.router_name, + &router_lookup, &RouterRouteKind::Custom, &create_params.into_inner(), ) @@ -5498,63 +5805,132 @@ async fn vpc_router_route_create( } /// Delete a route +#[endpoint { + method = DELETE, + path = "/v1/vpc-router-routes/{route}", + tags = ["vpcs"], +}] +async fn vpc_router_route_delete_v1( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let route_selector = params::RouteSelector { + router_selector: query.router_selector, + route: path.route, + }; + let route_lookup = + nexus.vpc_router_route_lookup(&opctx, &route_selector)?; + nexus.router_delete_route(&opctx, &route_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Delete a route +/// Use `DELETE /v1/vpc-router-routes/{route}` instead #[endpoint { method = DELETE, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}", tags = ["vpcs"], + deprecated = true }] async fn vpc_router_route_delete( rqctx: RequestContext>, path_params: Path, ) -> Result { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus - .router_delete_route( - &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.router_name, - &path.route_name, - ) - .await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let route_selector = params::RouteSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + Some(path.router_name.into()), + path.route_name.into(), + ); + let route_lookup = + nexus.vpc_router_route_lookup(&opctx, &route_selector)?; + nexus.router_delete_route(&opctx, &route_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Update a route +#[endpoint { + method = PUT, + path = "/v1/vpc-router-routes/{route}", + tags = ["vpcs"], +}] +async fn vpc_router_route_update_v1( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, + router_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let router_params = router_params.into_inner(); + let opctx = OpContext::for_external_api(&rqctx).await?; + let route_selector = params::RouteSelector { + router_selector: query.router_selector, + route: path.route, + }; + let route_lookup = + nexus.vpc_router_route_lookup(&opctx, &route_selector)?; + let route = nexus + .router_update_route(&opctx, &route_lookup, &router_params) + .await?; + Ok(HttpResponseOk(route.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} /// Update a route #[endpoint { method = PUT, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}", tags = ["vpcs"], + deprecated = true }] async fn vpc_router_route_update( rqctx: RequestContext>, path_params: Path, - router_params: TypedBody, + router_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let router_route = nexus + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let route_selector = params::RouteSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.vpc_name.into()), + Some(path.router_name.into()), + path.route_name.into(), + ); + let route_lookup = + nexus.vpc_router_route_lookup(&opctx, &route_selector)?; + let route = nexus .router_update_route( &opctx, - &path.organization_name, - &path.project_name, - &path.vpc_name, - &path.router_name, - &path.route_name, + &route_lookup, &router_params.into_inner(), ) .await?; - Ok(HttpResponseOk(router_route.into())) + Ok(HttpResponseOk(route.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 95d99e5abaf..eb793e08b1b 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -9,7 +9,8 @@ use base64::Engine; use chrono::{DateTime, Utc}; use omicron_common::api::external::{ ByteCount, IdentityMetadataCreateParams, IdentityMetadataUpdateParams, - InstanceCpuCount, Ipv4Net, Ipv6Net, Name, NameOrId, + InstanceCpuCount, Ipv4Net, Ipv6Net, Name, NameOrId, RouteDestination, + RouteTarget, }; use schemars::JsonSchema; use serde::{ @@ -45,6 +46,16 @@ pub struct SubnetPath { pub subnet: NameOrId, } +#[derive(Deserialize, JsonSchema)] +pub struct RouterPath { + pub router: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +pub struct RoutePath { + pub route: NameOrId, +} + #[derive(Serialize, Deserialize, JsonSchema)] pub struct DiskPath { pub disk: NameOrId, @@ -185,6 +196,60 @@ impl SubnetSelector { } } +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct RouterSelector { + #[serde(flatten)] + pub vpc_selector: Option, + pub router: NameOrId, +} + +// TODO-v1: delete this post migration +impl RouterSelector { + pub fn new( + organization: Option, + project: Option, + vpc: Option, + router: NameOrId, + ) -> Self { + RouterSelector { + vpc_selector: vpc + .map(|vpc| VpcSelector::new(organization, project, vpc)), + router, + } + } +} + +#[derive(Deserialize, JsonSchema)] +pub struct OptionalRouterSelector { + #[serde(flatten)] + pub router_selector: Option, +} + +#[derive(Deserialize, JsonSchema)] +pub struct RouteSelector { + #[serde(flatten)] + pub router_selector: Option, + pub route: NameOrId, +} + +// TODO-v1: delete this post migration +impl RouteSelector { + pub fn new( + organization: Option, + project: Option, + vpc: Option, + router: Option, + route: NameOrId, + ) -> Self { + RouteSelector { + router_selector: router.map(|router| { + RouterSelector::new(organization, project, vpc, router) + }), + route, + } + } +} + // Silos /// Create-time parameters for a [`Silo`](crate::external_api::views::Silo) @@ -986,6 +1051,26 @@ pub struct VpcRouterUpdate { pub identity: IdentityMetadataUpdateParams, } +// VPC ROUTER ROUTES + +/// Create-time parameters for a [`RouterRoute`] +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct RouterRouteCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + pub target: RouteTarget, + pub destination: RouteDestination, +} + +/// Updateable properties of a [`RouterRoute`] +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct RouterRouteUpdate { + #[serde(flatten)] + pub identity: IdentityMetadataUpdateParams, + pub target: RouteTarget, + pub destination: RouteDestination, +} + // DISKS pub const MIN_DISK_SIZE_BYTES: u32 = 1 << 30; // 1 GiB From 7a2ddbc5e40e33ee1a918bd8f7bcb5cb8ca72592 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 26 Jan 2023 10:51:49 -0500 Subject: [PATCH 24/45] Update API spec with new routes --- nexus/src/external_api/http_entrypoints.rs | 9 +- nexus/tests/integration_tests/endpoints.rs | 8 +- .../tests/integration_tests/router_routes.rs | 8 +- nexus/tests/output/nexus_tags.txt | 10 + openapi/nexus.json | 710 +++++++++++++++++- 5 files changed, 712 insertions(+), 33 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 18fd06bb399..dab4f3306b4 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -5424,7 +5424,7 @@ async fn vpc_router_create( /// Delete a router #[endpoint { method = DELETE, - path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}", + path = "/v1/vpc-routers/{router}", tags = ["vpcs"], }] async fn vpc_router_delete_v1( @@ -5451,7 +5451,7 @@ async fn vpc_router_delete_v1( } /// Delete a router -/// Use `DELETE /v1/vpc-router/{router}` instead +/// Use `DELETE /v1/vpc-routers/{router}` instead #[endpoint { method = DELETE, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}", @@ -5484,7 +5484,7 @@ async fn vpc_router_delete( /// Update a router #[endpoint { method = PUT, - path = "/v1/vpc-router/{router}", + path = "/v1/vpc-routers/{router}", tags = ["vpcs"], }] async fn vpc_router_update_v1( @@ -5556,7 +5556,7 @@ async fn vpc_router_update( /// List the routes associated with a router in a particular VPC. #[endpoint { method = GET, - path = "/v1/vpc-router-routes/", + path = "/v1/vpc-router-routes", tags = ["vpcs"], }] async fn vpc_router_route_list_v1( @@ -5898,6 +5898,7 @@ async fn vpc_router_route_update_v1( } /// Update a route +/// Use `PUT /v1/vpc-router-routes/{route}` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}", diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 470ff2f6740..9ab1daa7f26 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -23,8 +23,6 @@ use omicron_common::api::external::Ipv4Net; use omicron_common::api::external::Name; use omicron_common::api::external::RouteDestination; use omicron_common::api::external::RouteTarget; -use omicron_common::api::external::RouterRouteCreateParams; -use omicron_common::api::external::RouterRouteUpdateParams; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use omicron_nexus::authn; use omicron_nexus::authz; @@ -182,8 +180,8 @@ lazy_static! { "demo-router-route".parse().unwrap(); pub static ref DEMO_ROUTER_ROUTE_URL: String = format!("{}/{}", *DEMO_VPC_ROUTER_URL_ROUTES, *DEMO_ROUTER_ROUTE_NAME); - pub static ref DEMO_ROUTER_ROUTE_CREATE: RouterRouteCreateParams = - RouterRouteCreateParams { + pub static ref DEMO_ROUTER_ROUTE_CREATE: params::RouterRouteCreate = + params::RouterRouteCreate { identity: IdentityMetadataCreateParams { name: DEMO_ROUTER_ROUTE_NAME.clone(), description: String::from(""), @@ -1129,7 +1127,7 @@ lazy_static! { allowed_methods: vec![ AllowedMethod::Get, AllowedMethod::Put( - serde_json::to_value(&RouterRouteUpdateParams { + serde_json::to_value(¶ms::RouterRouteUpdate { identity: IdentityMetadataUpdateParams { name: None, description: Some("different".to_string()) diff --git a/nexus/tests/integration_tests/router_routes.rs b/nexus/tests/integration_tests/router_routes.rs index 5ef1a5af71a..21af569440c 100644 --- a/nexus/tests/integration_tests/router_routes.rs +++ b/nexus/tests/integration_tests/router_routes.rs @@ -11,9 +11,9 @@ use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::{ IdentityMetadataCreateParams, IdentityMetadataUpdateParams, - RouteDestination, RouteTarget, RouterRoute, RouterRouteCreateParams, - RouterRouteKind, RouterRouteUpdateParams, + RouteDestination, RouteTarget, RouterRoute, RouterRouteKind, }; +use omicron_nexus::external_api::params; use std::net::IpAddr; use std::net::Ipv4Addr; @@ -110,7 +110,7 @@ async fn test_router_routes(cptestctx: &ControlPlaneTestContext) { let route_created: RouterRoute = NexusRequest::objects_post( client, get_routes_url(router_name).as_str(), - &RouterRouteCreateParams { + ¶ms::RouterRouteCreate { identity: IdentityMetadataCreateParams { name: route_name.parse().unwrap(), description: "It's a route, what else can I say?".to_string(), @@ -151,7 +151,7 @@ async fn test_router_routes(cptestctx: &ControlPlaneTestContext) { NexusRequest::object_put( client, route_url.as_str(), - Some(&RouterRouteUpdateParams { + Some(¶ms::RouterRouteUpdate { identity: IdentityMetadataUpdateParams { name: Some(route_name.parse().unwrap()), description: None, diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 9e041dc88f1..0ad61ef8609 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -205,17 +205,27 @@ vpc_firewall_rules_view /organizations/{organization_name}/proj vpc_list /organizations/{organization_name}/projects/{project_name}/vpcs vpc_list_v1 /v1/vpcs vpc_router_create /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers +vpc_router_create_v1 /v1/vpc-routers vpc_router_delete /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name} +vpc_router_delete_v1 /v1/vpc-routers/{router} vpc_router_list /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers +vpc_router_list_v1 /v1/vpc-routers vpc_router_route_create /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes +vpc_router_route_create_v1 /v1/vpc-router-routes vpc_router_route_delete /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name} +vpc_router_route_delete_v1 /v1/vpc-router-routes/{route} vpc_router_route_list /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes +vpc_router_route_list_v1 /v1/vpc-router-routes vpc_router_route_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name} +vpc_router_route_update_v1 /v1/vpc-router-routes/{route} vpc_router_route_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name} vpc_router_route_view_by_id /by-id/vpc-router-routes/{id} +vpc_router_route_view_v1 /v1/vpc-router-routes/{route} vpc_router_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name} +vpc_router_update_v1 /v1/vpc-routers/{router} vpc_router_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name} vpc_router_view_by_id /by-id/vpc-routers/{id} +vpc_router_view_v1 /v1/vpc-routers/{router} vpc_subnet_create /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets vpc_subnet_create_v1 /v1/vpc-subnets vpc_subnet_delete /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} diff --git a/openapi/nexus.json b/openapi/nexus.json index 26cb5e39be7..991770a4b6f 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -364,7 +364,7 @@ "vpcs" ], "summary": "Fetch a subnet by id", - "description": "Use `GET /v1/vpc-subnets/{id}` instead.", + "description": "Use `GET /v1/vpc-subnets/{id}` instead", "operationId": "vpc_subnet_view_by_id", "parameters": [ { @@ -404,7 +404,7 @@ "vpcs" ], "summary": "Fetch a VPC", - "description": "Use `GET /v1/vpcs/{id}` instead.", + "description": "Use `GET /v1/vpcs/{id}` instead", "operationId": "vpc_view_by_id", "parameters": [ { @@ -3446,7 +3446,7 @@ "vpcs" ], "summary": "List VPCs", - "description": "Use `GET /v1/vpcs` instead.", + "description": "Use `GET /v1/vpcs` instead", "operationId": "vpc_list", "parameters": [ { @@ -3521,7 +3521,7 @@ "vpcs" ], "summary": "Create a VPC", - "description": "Use `POST /v1/vpcs` instead.", + "description": "Use `POST /v1/vpcs` instead", "operationId": "vpc_create", "parameters": [ { @@ -3580,7 +3580,7 @@ "vpcs" ], "summary": "Fetch a VPC", - "description": "Use `GET /v1/vpcs/{vpc}` instead.", + "description": "Use `GET /v1/vpcs/{vpc}` instead", "operationId": "vpc_view", "parameters": [ { @@ -3633,7 +3633,7 @@ "vpcs" ], "summary": "Update a VPC", - "description": "Use `PUT /v1/vpcs/{vpc}` instead.", + "description": "Use `PUT /v1/vpcs/{vpc}` instead", "operationId": "vpc_update", "parameters": [ { @@ -3696,7 +3696,7 @@ "vpcs" ], "summary": "Delete a VPC", - "description": "Use `DELETE /v1/vpcs/{vpc}` instead.", + "description": "Use `DELETE /v1/vpcs/{vpc}` instead", "operationId": "vpc_delete", "parameters": [ { @@ -3858,6 +3858,7 @@ "vpcs" ], "summary": "List routers", + "description": "Use `GET /v1/vpc-routers` instead", "operationId": "vpc_router_list", "parameters": [ { @@ -3930,6 +3931,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -3937,6 +3939,7 @@ "vpcs" ], "summary": "Create a router", + "description": "Use `POST /v1/vpc-routers` instead", "operationId": "vpc_router_create", "parameters": [ { @@ -3991,7 +3994,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}": { @@ -4000,6 +4004,7 @@ "vpcs" ], "summary": "Get a router", + "description": "Use `GET /v1/vpc-routers/{router}` instead", "operationId": "vpc_router_view", "parameters": [ { @@ -4052,7 +4057,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ @@ -4121,13 +4127,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a router", + "description": "Use `DELETE /v1/vpc-routers/{router}` instead", "operationId": "vpc_router_delete", "parameters": [ { @@ -4173,7 +4181,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes": { @@ -4309,7 +4318,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRouteCreateParams" + "$ref": "#/components/schemas/RouterRouteCreate" } } }, @@ -4341,6 +4350,7 @@ "vpcs" ], "summary": "Fetch a route", + "description": "Use `GET /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_view", "parameters": [ { @@ -4408,6 +4418,7 @@ "vpcs" ], "summary": "Update a route", + "description": "Use `PUT /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_update", "parameters": [ { @@ -4455,7 +4466,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRouteUpdateParams" + "$ref": "#/components/schemas/RouterRouteUpdate" } } }, @@ -4478,13 +4489,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a route", + "description": "Use `DELETE /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_delete", "parameters": [ { @@ -4538,7 +4551,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets": { @@ -4547,7 +4561,7 @@ "vpcs" ], "summary": "List subnets", - "description": "Use `GET /v1/vpc-subnets` instead.", + "description": "Use `GET /v1/vpc-subnets` instead", "operationId": "vpc_subnet_list", "parameters": [ { @@ -4628,7 +4642,7 @@ "vpcs" ], "summary": "Create a subnet", - "description": "Use `POST /v1/vpc-subnets` instead.", + "description": "Use `POST /v1/vpc-subnets` instead", "operationId": "vpc_subnet_create", "parameters": [ { @@ -4693,7 +4707,7 @@ "vpcs" ], "summary": "Fetch a subnet", - "description": "Use `GET /v1/vpc-subnets/{subnet}` instead.", + "description": "Use `GET /v1/vpc-subnets/{subnet}` instead", "operationId": "vpc_subnet_view", "parameters": [ { @@ -9200,6 +9214,662 @@ } } }, + "/v1/vpc-router-routes": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List the routes associated with a router in a particular VPC.", + "operationId": "vpc_router_route_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create a router", + "operationId": "vpc_router_route_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-router-routes/{route}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch a route", + "operationId": "vpc_router_route_view_v1", + "parameters": [ + { + "in": "path", + "name": "route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update a route", + "operationId": "vpc_router_route_update_v1", + "parameters": [ + { + "in": "path", + "name": "route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete a route", + "operationId": "vpc_router_route_delete_v1", + "parameters": [ + { + "in": "path", + "name": "route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-routers": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List routers", + "operationId": "vpc_router_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "vpcs" + ], + "operationId": "vpc_router_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-routers/{router}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Get a router", + "operationId": "vpc_router_view_v1", + "parameters": [ + { + "in": "path", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update a router", + "operationId": "vpc_router_update_v1", + "parameters": [ + { + "in": "path", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete a router", + "operationId": "vpc_router_delete_v1", + "parameters": [ + { + "in": "path", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/vpc-subnets": { "get": { "tags": [ @@ -13065,7 +13735,7 @@ "vpc_router_id" ] }, - "RouterRouteCreateParams": { + "RouterRouteCreate": { "description": "Create-time parameters for a [`RouterRoute`]", "type": "object", "properties": { @@ -13143,7 +13813,7 @@ "items" ] }, - "RouterRouteUpdateParams": { + "RouterRouteUpdate": { "description": "Updateable properties of a [`RouterRoute`]", "type": "object", "properties": { From c0d325f7341bb2daf9f7a7ae4254333e2060116e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 26 Jan 2023 11:28:30 -0500 Subject: [PATCH 25/45] Update authz tests --- nexus/tests/integration_tests/endpoints.rs | 10 +++++----- nexus/tests/output/uncovered-authz-endpoints.txt | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 9ab1daa7f26..68b0481ae52 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -107,7 +107,7 @@ lazy_static! { pub static ref DEMO_PROJECT_POLICY_URL: String = format!("/v1/projects/{}/policy?organization={}", *DEMO_PROJECT_NAME, *DEMO_ORG_NAME); pub static ref DEMO_PROJECT_URL_DISKS: String = - format!("/organizations/{}/projects/{}/disks", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("/v1/disks?organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_IMAGES: String = format!("/organizations/{}/projects/{}/images", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_INSTANCES: String = format!("/v1/instances?organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); @@ -130,7 +130,7 @@ lazy_static! { pub static ref DEMO_VPC_URL_FIREWALL_RULES: String = format!("/organizations/{}/projects/{}/vpcs/{}/firewall/rules", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_ROUTERS: String = - format!("/organizations/{}/projects/{}/vpcs/{}/routers", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); + format!("/v1/vpc-routers?organization={}&project={}&vpc={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_SUBNETS: String = format!("/v1/vpc-subnets?organization={}&project={}&vpc={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_CREATE: params::VpcCreate = @@ -164,9 +164,9 @@ lazy_static! { pub static ref DEMO_VPC_ROUTER_NAME: Name = "demo-vpc-router".parse().unwrap(); pub static ref DEMO_VPC_ROUTER_URL: String = - format!("{}/{}", *DEMO_VPC_URL_ROUTERS, *DEMO_VPC_ROUTER_NAME); + format!("/v1/vpc-routers/{}?organization={}&project={}&vpc={}", *DEMO_VPC_ROUTER_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_ROUTER_URL_ROUTES: String = - format!("{}/routes", *DEMO_VPC_ROUTER_URL); + format!("/v1/vpc-router-routes?organization={}&project={}&vpc={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_ROUTER_CREATE: params::VpcRouterCreate = params::VpcRouterCreate { identity: IdentityMetadataCreateParams { @@ -179,7 +179,7 @@ lazy_static! { pub static ref DEMO_ROUTER_ROUTE_NAME: Name = "demo-router-route".parse().unwrap(); pub static ref DEMO_ROUTER_ROUTE_URL: String = - format!("{}/{}", *DEMO_VPC_ROUTER_URL_ROUTES, *DEMO_ROUTER_ROUTE_NAME); + format!("/v1/vpc-router-routes/{}?organization={}&project={}&vpc={}", *DEMO_ROUTER_ROUTE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_ROUTER_ROUTE_CREATE: params::RouterRouteCreate = params::RouterRouteCreate { identity: IdentityMetadataCreateParams { diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index 6314de4bf4b..d0035eee4dc 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -4,6 +4,8 @@ project_delete (delete "/organizations/{organization_n disk_delete (delete "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}") instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") vpc_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") +vpc_router_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}") +vpc_router_route_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}") vpc_subnet_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") disk_view_by_id (get "/by-id/disks/{id}") instance_view_by_id (get "/by-id/instances/{id}") @@ -25,6 +27,10 @@ instance_serial_console_stream (get "/organizations/{organization_n project_policy_view (get "/organizations/{organization_name}/projects/{project_name}/policy") vpc_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs") vpc_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") +vpc_router_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers") +vpc_router_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}") +vpc_router_route_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes") +vpc_router_route_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}") vpc_subnet_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets") vpc_subnet_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") device_auth_request (post "/device/auth") @@ -45,10 +51,14 @@ instance_reboot (post "/organizations/{organization_n instance_start (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start") instance_stop (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop") vpc_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs") +vpc_router_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers") +vpc_router_route_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes") vpc_subnet_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets") organization_update (put "/organizations/{organization_name}") organization_policy_update (put "/organizations/{organization_name}/policy") project_update (put "/organizations/{organization_name}/projects/{project_name}") project_policy_update (put "/organizations/{organization_name}/projects/{project_name}/policy") vpc_update (put "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") +vpc_router_update (put "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}") +vpc_router_route_update (put "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}") vpc_subnet_update (put "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") From ba1b8b81b0573db0d15163bcce89113ce954693e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 26 Jan 2023 17:45:57 -0500 Subject: [PATCH 26/45] WIP subnet tests --- nexus/tests/integration_tests/vpc_subnets.rs | 44 ++++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/nexus/tests/integration_tests/vpc_subnets.rs b/nexus/tests/integration_tests/vpc_subnets.rs index 9e0485b11e1..36eb9acde02 100644 --- a/nexus/tests/integration_tests/vpc_subnets.rs +++ b/nexus/tests/integration_tests/vpc_subnets.rs @@ -44,10 +44,19 @@ async fn test_delete_vpc_subnet_with_interfaces_fails( populate_ip_pool(client, "default", None).await; let vpcs_url = - format!("/organizations/{}/projects/{}/vpcs", org_name, project_name); - let vpc_url = format!("{vpcs_url}/default"); - let subnets_url = format!("{vpc_url}/subnets"); - let subnet_url = format!("{subnets_url}/default"); + format!("/v1/vpcs?organization={}&project={}", org_name, project_name); + let vpc_url = format!( + "/v1/vpcs/default?organization={}&project={}", + org_name, project_name + ); + let subnets_url = format!( + "/v1/vpc-subnets?organization={}&project={}", + org_name, project_name + ); + let subnet_url = format!( + "/v1/vpc-subnets/default?organization={}&project={}", + org_name, project_name + ); // get subnets should return the default subnet let subnets = @@ -57,7 +66,7 @@ async fn test_delete_vpc_subnet_with_interfaces_fails( // Create an instance in the default VPC and VPC Subnet. Verify that we // cannot delete the subnet until the instance is gone. let instance_url = format!( - "/organizations/{org_name}/projects/{project_name}/instances/{instance_name}" + "/v1/instances/{instance_name}?organization={org_name}&project={project_name}" ); let instance = create_instance(client, &org_name, project_name, instance_name).await; @@ -92,7 +101,10 @@ async fn test_delete_vpc_subnet_with_interfaces_fails( // Now deleting the subnet should succeed NexusRequest::object_delete( &client, - &format!("{}/{}", subnets_url, subnets[0].identity.name), + &format!( + "/v1/vpc-subnets/{}?organization={org_name}&project={project_name}", + subnets[0].identity.name + ), ) .authn_as(AuthnMode::PrivilegedUser) .execute() @@ -111,16 +123,16 @@ async fn test_vpc_subnets(cptestctx: &ControlPlaneTestContext) { let org_name = "test-org"; create_organization(&client, &org_name).await; let project_name = "springfield-squidport"; - let vpcs_url = - format!("/organizations/{}/projects/{}/vpcs", org_name, project_name); let _ = create_project(&client, org_name, project_name).await; // Create a VPC. let vpc_name = "vpc1"; let vpc = create_vpc(&client, org_name, project_name, vpc_name).await; - let vpc_url = format!("{}/{}", vpcs_url, vpc_name); - let subnets_url = format!("{}/subnets", vpc_url); + let subnets_url = format!( + "/v1/vpc-subnets?organization={}&project={}", + org_name, project_name + ); // get subnets should return the default subnet let subnets = @@ -143,7 +155,10 @@ async fn test_vpc_subnets(cptestctx: &ControlPlaneTestContext) { assert_eq!(subnets.len(), 0); let subnet_name = "subnet1"; - let subnet_url = format!("{}/{}", subnets_url, subnet_name); + let subnet_url = format!( + "/v1/vpc-subnets/{}?organization={}&project={}", + subnet_name, org_name, project_name + ); // fetching a particular subnet should 404 let error: dropshot::HttpErrorResponseBody = NexusRequest::expect_failure( @@ -261,7 +276,10 @@ async fn test_vpc_subnets(cptestctx: &ControlPlaneTestContext) { assert_eq!(error.message, "already exists: vpc-subnet \"subnet1\""); let subnet2_name = "subnet2"; - let subnet2_url = format!("{}/{}", subnets_url, subnet2_name); + let subnet2_url = format!( + "/v1/vpc-subnets/{}?organization={}&project={}", + subnet2_name, org_name, project_name + ); // second subnet 404s before it's created let error: dropshot::HttpErrorResponseBody = NexusRequest::expect_failure( @@ -405,7 +423,7 @@ async fn test_vpc_subnets(cptestctx: &ControlPlaneTestContext) { let subnet_same_name: VpcSubnet = NexusRequest::objects_post( client, - format!("{}/{}/subnets", vpcs_url, vpc2_name).as_str(), + format!("/v1/vpc-subnets?organization={org_name}&project={project_name}&vpc={vpc2_name}").as_str(), &new_subnet, ) .authn_as(AuthnMode::PrivilegedUser) From 92f9fdaa7bc442f025794770dae395590997eb80 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 30 Jan 2023 14:48:07 -0500 Subject: [PATCH 27/45] WIP: progress on network interfaces --- nexus/src/app/instance.rs | 25 +--- nexus/src/db/datastore/network_interface.rs | 28 +++-- nexus/src/external_api/http_entrypoints.rs | 132 ++++++++++++++++++-- nexus/types/src/external_api/params.rs | 2 +- 4 files changed, 145 insertions(+), 42 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 8bc2d1f4d07..87b76f6ea5c 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -806,18 +806,11 @@ impl super::Nexus { pub async fn instance_create_network_interface( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + instance_lookup: &lookup::Instance<'_>, params: ¶ms::NetworkInterfaceCreate, ) -> CreateResult { let (.., authz_project, authz_instance) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) - .lookup_for(authz::Action::Modify) - .await?; + instance_lookup.lookup_for(authz::Action::Modify).await?; // NOTE: We need to lookup the VPC and VPC Subnet, since we need both // IDs for creating the network interface. @@ -882,17 +875,11 @@ impl super::Nexus { pub async fn instance_list_network_interfaces( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, - pagparams: &DataPageParams<'_, Name>, + instance_lookup: &lookup::Instance<'_>, + pagparams: &PaginatedBy<'_>, ) -> ListResultVec { - let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) - .lookup_for(authz::Action::ListChildren) - .await?; + let (.., authz_instance) = + instance_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore .instance_list_network_interfaces(opctx, &authz_instance, pagparams) .await diff --git a/nexus/src/db/datastore/network_interface.rs b/nexus/src/db/datastore/network_interface.rs index c49ea2dc7da..a1ca240c069 100644 --- a/nexus/src/db/datastore/network_interface.rs +++ b/nexus/src/db/datastore/network_interface.rs @@ -27,13 +27,14 @@ use async_bb8_diesel::AsyncRunQueryDsl; use chrono::Utc; use diesel::prelude::*; use omicron_common::api::external; -use omicron_common::api::external::DataPageParams; +use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::DeleteResult; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupType; use omicron_common::api::external::ResourceType; use omicron_common::api::external::UpdateResult; +use ref_cast::RefCast; use sled_agent_client::types as sled_client_types; /// OPTE requires information that's currently split across the network @@ -286,18 +287,27 @@ impl DataStore { &self, opctx: &OpContext, authz_instance: &authz::Instance, - pagparams: &DataPageParams<'_, Name>, + pagparams: &PaginatedBy<'_>, ) -> ListResultVec { opctx.authorize(authz::Action::ListChildren, authz_instance).await?; use db::schema::network_interface::dsl; - paginated(dsl::network_interface, dsl::name, &pagparams) - .filter(dsl::time_deleted.is_null()) - .filter(dsl::instance_id.eq(authz_instance.id())) - .select(NetworkInterface::as_select()) - .load_async::(self.pool_authorized(opctx).await?) - .await - .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) + match pagparams { + PaginatedBy::Id(pagparams) => { + paginated(dsl::network_interface, dsl::id, &pagparams) + } + PaginatedBy::Name(pagparams) => paginated( + dsl::network_interface, + dsl::name, + &pagparams.map_name(|n| Name::ref_cast(n)), + ), + } + .filter(dsl::time_deleted.is_null()) + .filter(dsl::instance_id.eq(authz_instance.id())) + .select(NetworkInterface::as_select()) + .load_async::(self.pool_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } /// Update a network interface associated with a given instance. diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index dab4f3306b4..3cdeb414bd1 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -227,6 +227,12 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_network_interface_update)?; api.register(instance_network_interface_delete)?; + api.register(instance_network_interface_create_v1)?; + api.register(instance_network_interface_list_v1)?; + api.register(instance_network_interface_view_v1)?; + api.register(instance_network_interface_update_v1)?; + api.register(instance_network_interface_delete_v1)?; + api.register(instance_external_ip_list)?; api.register(vpc_router_list)?; @@ -2281,12 +2287,12 @@ async fn disk_list_v1( ) -> Result>, HttpError> { let apictx = rqctx.context(); let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; let nexus = &apictx.nexus; let query = query_params.into_inner(); let pag_params = data_page_params_for(&rqctx, &query)?; let scan_params = ScanByNameOrId::from_query(&query)?; let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; - let opctx = OpContext::for_external_api(&rqctx).await?; let project_lookup = nexus.project_lookup(&opctx, &scan_params.selector)?; let disks = nexus @@ -3937,10 +3943,43 @@ async fn image_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/* - * VPCs - */ - +/// List network interfaces +#[endpoint { + method = GET, + path = "/v1/network-interfaces", + tags = ["instances"], +}] +async fn instance_network_interface_list_v1( + rqctx: RequestContext>, + query_params: Query>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanByNameOrId::from_query(&query)?; + let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; + let instance_lookup = nexus.instance_lookup(&opctx, &query)?; + let interfaces = nexus + .instance_list_network_interfaces( + &opctx, + &instance_lookup, + &paginated_by, + ) + .await? + .into_iter() + .map(|d| d.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + interfaces, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} /// List network interfaces #[endpoint { method = GET, @@ -3984,10 +4023,41 @@ async fn instance_network_interface_list( } /// Create a network interface +#[endpoint { + method = POST, + path = "/v1/network-interfaces", + tags = ["instances"], +}] +async fn instance_network_interface_create_v1( + rqctx: RequestContext>, + query_params: Query, + interface_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let instance_lookup = nexus.instance_lookup(&opctx, &query)?; + let iface = nexus + .instance_create_network_interface( + &opctx, + &instance_lookup, + &interface_params.into_inner(), + ) + .await?; + Ok(HttpResponseCreated(iface.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Create a network interface +/// Use `POST /v1/network-interfaces` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces", tags = ["instances"], + deprecated = true }] async fn instance_network_interface_create( rqctx: RequestContext>, @@ -3995,19 +4065,21 @@ async fn instance_network_interface_create( interface_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let instance_selector = params::InstanceSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), + ); + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; let iface = nexus .instance_create_network_interface( &opctx, - &organization_name, - &project_name, - &instance_name, + &instance_lookup, &interface_params.into_inner(), ) .await?; @@ -4062,11 +4134,45 @@ async fn instance_network_interface_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Fetch a network interface +#[endpoint { + method = GET, + path = "/v1/network-interfaces/{interface_name}", + tags = ["instances"], +}] +async fn instance_network_interface_view_v1( + rqctx: RequestContext>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let organization_name = &path.organization_name; + let project_name = &path.project_name; + let instance_name = &path.instance_name; + let interface_name = &path.interface_name; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let interface = nexus + .network_interface_fetch( + &opctx, + organization_name, + project_name, + instance_name, + interface_name, + ) + .await?; + Ok(HttpResponseOk(NetworkInterface::from(interface))) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Fetch a network interface #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}", tags = ["instances"], + deprecated = true }] async fn instance_network_interface_view( rqctx: RequestContext>, diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index eb793e08b1b..231bffccc4d 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -123,7 +123,7 @@ impl DiskSelector { } } -#[derive(Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct InstanceSelector { #[serde(flatten)] pub project_selector: Option, From 45427c1b56482b1bcce1c29b2f1830fd68c108fa Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 3 Feb 2023 11:06:09 -0500 Subject: [PATCH 28/45] Add nic update --- nexus/src/external_api/http_entrypoints.rs | 37 + nexus/types/src/external_api/params.rs | 26 + openapi/nexus.json | 1381 ++------------------ 3 files changed, 183 insertions(+), 1261 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 7c675da2ec9..ad3664415da 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -4240,6 +4240,43 @@ async fn instance_network_interface_view_by_id( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Update a network interface +#[endpoint { + method = PUT, + path = "/v1/network-interfaces/{interface_name}", + tags = ["instances"], +}] +async fn instance_network_interface_update( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, + updated_iface: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let updated_iface = updated_iface.into_inner(); + let network_interface_slector = params::NetworkInterfaceSelector { + instance_selector: query.instance_selector, + network_interface: path.network_interface, + }; + let network_interface_lookup = + nexus.network_interface_lookup(&opctx, &instance_selector)?; + let interface = nexus + .network_interface_update( + &opctx, + &network_interface_lookup, + updated_iface, + ) + .await?; + Ok(HttpResponseOk(NetworkInterface::from(interface))) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Update a network interface #[endpoint { method = PUT, diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 88dd851520d..2a5e1f7c911 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -145,6 +145,32 @@ impl InstanceSelector { } } +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct OptionalInstanceSelector { + pub instance_selector: Option, +} + +pub struct NetworkInterfaceSelector { + pub instance_selector: Option, + pub network_interface: NameOrId, +} + +impl NetworkInterfaceSelector { + pub fn new( + organization: Option, + project: Option, + instance: Option, + network_interface: NameOrId, + ) -> Self { + NetworkInterfaceSelector { + instance_selector: instance.map(|instance| { + InstanceSelector::new(organization, project, instance) + }), + network_interface, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct VpcSelector { #[serde(flatten)] diff --git a/openapi/nexus.json b/openapi/nexus.json index 2e3d2778327..29689c713ca 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -364,7 +364,6 @@ "vpcs" ], "summary": "Fetch a subnet by id", - "description": "Use `GET /v1/vpc-subnets/{id}` instead", "operationId": "vpc_subnet_view_by_id", "parameters": [ { @@ -394,8 +393,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } } }, "/by-id/vpcs/{id}": { @@ -404,7 +402,6 @@ "vpcs" ], "summary": "Fetch a VPC", - "description": "Use `GET /v1/vpcs/{id}` instead", "operationId": "vpc_view_by_id", "parameters": [ { @@ -434,8 +431,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } } }, "/device/auth": { @@ -3446,7 +3442,6 @@ "vpcs" ], "summary": "List VPCs", - "description": "Use `GET /v1/vpcs` instead", "operationId": "vpc_list", "parameters": [ { @@ -3513,7 +3508,6 @@ "$ref": "#/components/responses/Error" } }, - "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -3521,7 +3515,6 @@ "vpcs" ], "summary": "Create a VPC", - "description": "Use `POST /v1/vpcs` instead", "operationId": "vpc_create", "parameters": [ { @@ -3570,8 +3563,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}": { @@ -3580,7 +3572,6 @@ "vpcs" ], "summary": "Fetch a VPC", - "description": "Use `GET /v1/vpcs/{vpc}` instead", "operationId": "vpc_view", "parameters": [ { @@ -3625,15 +3616,13 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } }, "put": { "tags": [ "vpcs" ], "summary": "Update a VPC", - "description": "Use `PUT /v1/vpcs/{vpc}` instead", "operationId": "vpc_update", "parameters": [ { @@ -3688,15 +3677,13 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a VPC", - "description": "Use `DELETE /v1/vpcs/{vpc}` instead", "operationId": "vpc_delete", "parameters": [ { @@ -3734,8 +3721,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/firewall/rules": { @@ -3858,7 +3844,6 @@ "vpcs" ], "summary": "List routers", - "description": "Use `GET /v1/vpc-routers` instead", "operationId": "vpc_router_list", "parameters": [ { @@ -3931,7 +3916,6 @@ "$ref": "#/components/responses/Error" } }, - "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -3939,7 +3923,6 @@ "vpcs" ], "summary": "Create a router", - "description": "Use `POST /v1/vpc-routers` instead", "operationId": "vpc_router_create", "parameters": [ { @@ -3994,8 +3977,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}": { @@ -4004,7 +3986,6 @@ "vpcs" ], "summary": "Get a router", - "description": "Use `GET /v1/vpc-routers/{router}` instead", "operationId": "vpc_router_view", "parameters": [ { @@ -4057,8 +4038,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } }, "put": { "tags": [ @@ -4127,15 +4107,13 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a router", - "description": "Use `DELETE /v1/vpc-routers/{router}` instead", "operationId": "vpc_router_delete", "parameters": [ { @@ -4181,8 +4159,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes": { @@ -4318,7 +4295,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRouteCreate" + "$ref": "#/components/schemas/RouterRouteCreateParams" } } }, @@ -4350,7 +4327,6 @@ "vpcs" ], "summary": "Fetch a route", - "description": "Use `GET /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_view", "parameters": [ { @@ -4418,7 +4394,6 @@ "vpcs" ], "summary": "Update a route", - "description": "Use `PUT /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_update", "parameters": [ { @@ -4466,7 +4441,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRouteUpdate" + "$ref": "#/components/schemas/RouterRouteUpdateParams" } } }, @@ -4489,15 +4464,13 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a route", - "description": "Use `DELETE /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_delete", "parameters": [ { @@ -4551,8 +4524,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets": { @@ -4561,7 +4533,6 @@ "vpcs" ], "summary": "List subnets", - "description": "Use `GET /v1/vpc-subnets` instead", "operationId": "vpc_subnet_list", "parameters": [ { @@ -4634,7 +4605,6 @@ "$ref": "#/components/responses/Error" } }, - "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -4642,7 +4612,6 @@ "vpcs" ], "summary": "Create a subnet", - "description": "Use `POST /v1/vpc-subnets` instead", "operationId": "vpc_subnet_create", "parameters": [ { @@ -4697,8 +4666,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}": { @@ -4707,7 +4675,6 @@ "vpcs" ], "summary": "Fetch a subnet", - "description": "Use `GET /v1/vpc-subnets/{subnet}` instead", "operationId": "vpc_subnet_view", "parameters": [ { @@ -4760,15 +4727,13 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } }, "put": { "tags": [ "vpcs" ], "summary": "Update a subnet", - "description": "Use `PUT /v1/vpc-subnets/{subnet}` instead", "operationId": "vpc_subnet_update", "parameters": [ { @@ -4831,15 +4796,13 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a subnet", - "description": "Use `DELETE /v1/vpc-subnets/{subnet}` instead", "operationId": "vpc_subnet_delete", "parameters": [ { @@ -4885,8 +4848,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "deprecated": true + } } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/network-interfaces": { @@ -9321,15 +9283,6 @@ } } }, -<<<<<<< HEAD - "/v1/vpc-router-routes": { - "get": { - "tags": [ - "vpcs" - ], - "summary": "List the routes associated with a router in a particular VPC.", - "operationId": "vpc_router_route_list_v1", -======= "/v1/system/update/components": { "get": { "tags": [ @@ -9337,7 +9290,6 @@ ], "summary": "View version and update status of component tree", "operationId": "system_component_version_list", ->>>>>>> main "parameters": [ { "in": "query", @@ -9352,16 +9304,6 @@ }, { "in": "query", -<<<<<<< HEAD - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", -======= ->>>>>>> main "name": "page_token", "description": "Token returned by previous call to retrieve the subsequent page", "schema": { @@ -9371,36 +9313,9 @@ }, { "in": "query", -<<<<<<< HEAD - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "router", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "sort_by", - "schema": { - "$ref": "#/components/schemas/NameOrIdSortMode" - } - }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" -======= "name": "sort_by", "schema": { "$ref": "#/components/schemas/IdSortMode" ->>>>>>> main } } ], @@ -9410,11 +9325,7 @@ "content": { "application/json": { "schema": { -<<<<<<< HEAD - "$ref": "#/components/schemas/RouterRouteResultsPage" -======= "$ref": "#/components/schemas/UpdateableComponentResultsPage" ->>>>>>> main } } } @@ -9427,62 +9338,51 @@ } }, "x-dropshot-pagination": true -<<<<<<< HEAD - }, - "post": { + } + }, + "/v1/system/update/deployments": { + "get": { "tags": [ - "vpcs" + "system" ], - "summary": "Create a router", - "operationId": "vpc_router_route_create_v1", + "summary": "List all update deployments", + "operationId": "update_deployments_list", "parameters": [ { "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", + "name": "limit", + "description": "Maximum number of items returned by a single call", "schema": { - "$ref": "#/components/schemas/NameOrId" + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 } }, { "in": "query", - "name": "router", - "required": true, + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", "schema": { - "$ref": "#/components/schemas/NameOrId" + "nullable": true, + "type": "string" } }, { "in": "query", - "name": "vpc", + "name": "sort_by", "schema": { - "$ref": "#/components/schemas/NameOrId" + "$ref": "#/components/schemas/IdSortMode" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RouterRouteCreate" - } - } - }, - "required": true - }, "responses": { - "201": { - "description": "successful creation", + "200": { + "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRoute" + "$ref": "#/components/schemas/UpdateDeploymentResultsPage" } } } @@ -9493,52 +9393,25 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "x-dropshot-pagination": true } }, - "/v1/vpc-router-routes/{route}": { + "/v1/system/update/deployments/{id}": { "get": { "tags": [ - "vpcs" + "system" ], - "summary": "Fetch a route", - "operationId": "vpc_router_route_view_v1", + "summary": "Fetch a system update deployment", + "operationId": "update_deployment_view", "parameters": [ { "in": "path", - "name": "route", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "router", + "name": "id", "required": true, "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" + "type": "string", + "format": "uuid" } } ], @@ -9548,7 +9421,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRoute" + "$ref": "#/components/schemas/UpdateDeployment" } } } @@ -9560,68 +9433,52 @@ "$ref": "#/components/responses/Error" } } - }, - "put": { + } + }, + "/v1/system/update/refresh": { + "post": { "tags": [ - "vpcs" + "system" ], - "summary": "Update a route", - "operationId": "vpc_router_route_update_v1", - "parameters": [ - { - "in": "path", - "name": "route", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } + "summary": "Refresh update data", + "operationId": "system_update_refresh", + "responses": { + "204": { + "description": "resource updated" }, - { - "in": "query", - "name": "router", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } + "4XX": { + "$ref": "#/components/responses/Error" }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } + "5XX": { + "$ref": "#/components/responses/Error" } + } + } + }, + "/v1/system/update/start": { + "post": { + "tags": [ + "system" ], + "summary": "Start system update", + "operationId": "system_update_start", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRouteUpdate" + "$ref": "#/components/schemas/SystemUpdateStart" } } }, "required": true }, "responses": { - "200": { - "description": "successful operation", + "202": { + "description": "successfully enqueued operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRoute" + "$ref": "#/components/schemas/UpdateDeployment" } } } @@ -9633,54 +9490,19 @@ "$ref": "#/components/responses/Error" } } - }, - "delete": { + } + }, + "/v1/system/update/stop": { + "post": { "tags": [ - "vpcs" - ], - "summary": "Delete a route", - "operationId": "vpc_router_route_delete_v1", - "parameters": [ - { - "in": "path", - "name": "route", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "router", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } + "system" ], + "summary": "Stop system update", + "description": "If there is no update in progress, do nothing.", + "operationId": "system_update_stop", "responses": { "204": { - "description": "successful deletion" + "description": "resource updated" }, "4XX": { "$ref": "#/components/responses/Error" @@ -9691,24 +9513,13 @@ } } }, - "/v1/vpc-routers": { - "get": { - "tags": [ - "vpcs" - ], - "summary": "List routers", - "operationId": "vpc_router_list_v1", -======= - } - }, - "/v1/system/update/deployments": { + "/v1/system/update/updates": { "get": { "tags": [ "system" ], - "summary": "List all update deployments", - "operationId": "update_deployments_list", ->>>>>>> main + "summary": "List all updates", + "operationId": "system_update_list", "parameters": [ { "in": "query", @@ -9723,16 +9534,6 @@ }, { "in": "query", -<<<<<<< HEAD - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", -======= ->>>>>>> main "name": "page_token", "description": "Token returned by previous call to retrieve the subsequent page", "schema": { @@ -9742,29 +9543,9 @@ }, { "in": "query", -<<<<<<< HEAD - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "sort_by", - "schema": { - "$ref": "#/components/schemas/NameOrIdSortMode" - } - }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" -======= "name": "sort_by", "schema": { "$ref": "#/components/schemas/IdSortMode" ->>>>>>> main } } ], @@ -9774,11 +9555,7 @@ "content": { "application/json": { "schema": { -<<<<<<< HEAD - "$ref": "#/components/schemas/VpcRouterResultsPage" -======= - "$ref": "#/components/schemas/UpdateDeploymentResultsPage" ->>>>>>> main + "$ref": "#/components/schemas/SystemUpdateResultsPage" } } } @@ -9791,54 +9568,32 @@ } }, "x-dropshot-pagination": true -<<<<<<< HEAD - }, - "post": { + } + }, + "/v1/system/update/updates/{version}": { + "get": { "tags": [ - "vpcs" + "system" ], - "operationId": "vpc_router_create_v1", + "summary": "View system update", + "operationId": "system_update_view", "parameters": [ { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", + "in": "path", + "name": "version", "required": true, "schema": { - "$ref": "#/components/schemas/NameOrId" + "$ref": "#/components/schemas/SemverVersion" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VpcRouterCreate" - } - } - }, - "required": true - }, "responses": { - "201": { - "description": "successful creation", + "200": { + "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VpcRouter" + "$ref": "#/components/schemas/SystemUpdate" } } } @@ -9852,42 +9607,20 @@ } } }, - "/v1/vpc-routers/{router}": { + "/v1/system/update/updates/{version}/components": { "get": { "tags": [ - "vpcs" + "system" ], - "summary": "Get a router", - "operationId": "vpc_router_view_v1", + "summary": "View system update component tree", + "operationId": "system_update_components_list", "parameters": [ { "in": "path", - "name": "router", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", + "name": "version", "required": true, "schema": { - "$ref": "#/components/schemas/NameOrId" + "$ref": "#/components/schemas/SemverVersion" } } ], @@ -9897,7 +9630,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VpcRouter" + "$ref": "#/components/schemas/ComponentUpdateResultsPage" } } } @@ -9909,61 +9642,22 @@ "$ref": "#/components/responses/Error" } } - }, - "put": { + } + }, + "/v1/system/update/version": { + "get": { "tags": [ - "vpcs" - ], - "summary": "Update a router", - "operationId": "vpc_router_update_v1", - "parameters": [ - { - "in": "path", - "name": "router", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } + "system" ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VpcRouterUpdate" - } - } - }, - "required": true - }, + "summary": "View system version and update status", + "operationId": "system_version", "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VpcRouter" + "$ref": "#/components/schemas/SystemVersion" } } } @@ -9975,841 +9669,6 @@ "$ref": "#/components/responses/Error" } } - }, - "delete": { - "tags": [ - "vpcs" - ], - "summary": "Delete a router", - "operationId": "vpc_router_delete_v1", - "parameters": [ - { - "in": "path", - "name": "router", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "responses": { - "204": { - "description": "successful deletion" -======= - } - }, - "/v1/system/update/deployments/{id}": { - "get": { - "tags": [ - "system" - ], - "summary": "Fetch a system update deployment", - "operationId": "update_deployment_view", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDeployment" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/v1/system/update/refresh": { - "post": { - "tags": [ - "system" - ], - "summary": "Refresh update data", - "operationId": "system_update_refresh", - "responses": { - "204": { - "description": "resource updated" ->>>>>>> main - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, -<<<<<<< HEAD - "/v1/vpc-subnets": { - "get": { - "tags": [ - "vpcs" - ], - "summary": "Fetch a subnet", - "operationId": "vpc_subnet_list_v1", -======= - "/v1/system/update/start": { - "post": { - "tags": [ - "system" - ], - "summary": "Start system update", - "operationId": "system_update_start", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SystemUpdateStart" - } - } - }, - "required": true - }, - "responses": { - "202": { - "description": "successfully enqueued operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDeployment" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/v1/system/update/stop": { - "post": { - "tags": [ - "system" - ], - "summary": "Stop system update", - "description": "If there is no update in progress, do nothing.", - "operationId": "system_update_stop", - "responses": { - "204": { - "description": "resource updated" - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/v1/system/update/updates": { - "get": { - "tags": [ - "system" - ], - "summary": "List all updates", - "operationId": "system_update_list", ->>>>>>> main - "parameters": [ - { - "in": "query", - "name": "limit", - "description": "Maximum number of items returned by a single call", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint32", - "minimum": 1 - } - }, - { - "in": "query", -<<<<<<< HEAD - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", -======= ->>>>>>> main - "name": "page_token", - "description": "Token returned by previous call to retrieve the subsequent page", - "schema": { - "nullable": true, - "type": "string" - } - }, - { - "in": "query", -<<<<<<< HEAD - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "sort_by", - "schema": { - "$ref": "#/components/schemas/NameOrIdSortMode" - } - }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" -======= - "name": "sort_by", - "schema": { - "$ref": "#/components/schemas/IdSortMode" ->>>>>>> main - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { -<<<<<<< HEAD - "$ref": "#/components/schemas/VpcSubnetResultsPage" -======= - "$ref": "#/components/schemas/SystemUpdateResultsPage" ->>>>>>> main - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - }, - "x-dropshot-pagination": true -<<<<<<< HEAD - }, - "post": { - "tags": [ - "vpcs" - ], - "summary": "Create a subnet", - "operationId": "vpc_subnet_create_v1", - "parameters": [ - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VpcSubnetCreate" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "successful creation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VpcSubnet" -======= - } - }, - "/v1/system/update/updates/{version}": { - "get": { - "tags": [ - "system" - ], - "summary": "View system update", - "operationId": "system_update_view", - "parameters": [ - { - "in": "path", - "name": "version", - "required": true, - "schema": { - "$ref": "#/components/schemas/SemverVersion" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SystemUpdate" ->>>>>>> main - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, -<<<<<<< HEAD - "/v1/vpc-subnets/{subnet}": { - "get": { - "tags": [ - "vpcs" - ], - "summary": "Fetch a subnet", - "operationId": "vpc_subnet_view_v1", - "parameters": [ - { - "in": "path", - "name": "subnet", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" -======= - "/v1/system/update/updates/{version}/components": { - "get": { - "tags": [ - "system" - ], - "summary": "View system update component tree", - "operationId": "system_update_components_list", - "parameters": [ - { - "in": "path", - "name": "version", - "required": true, - "schema": { - "$ref": "#/components/schemas/SemverVersion" ->>>>>>> main - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { -<<<<<<< HEAD - "$ref": "#/components/schemas/VpcSubnet" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - }, - "put": { - "tags": [ - "vpcs" - ], - "summary": "Update a subnet", - "operationId": "vpc_subnet_update_v1", - "parameters": [ - { - "in": "path", - "name": "subnet", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VpcSubnetUpdate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VpcSubnet" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - }, - "delete": { - "tags": [ - "vpcs" - ], - "summary": "Delete a subnet", - "operationId": "vpc_subnet_delete_v1", - "parameters": [ - { - "in": "path", - "name": "subnet", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "vpc", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "responses": { - "204": { - "description": "successful deletion" - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/v1/vpcs": { - "get": { - "tags": [ - "vpcs" - ], - "summary": "List VPCs", - "operationId": "vpc_list_v1", - "parameters": [ - { - "in": "query", - "name": "limit", - "description": "Maximum number of items returned by a single call", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint32", - "minimum": 1 - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "page_token", - "description": "Token returned by previous call to retrieve the subsequent page", - "schema": { - "nullable": true, - "type": "string" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "sort_by", - "schema": { - "$ref": "#/components/schemas/NameOrIdSortMode" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VpcResultsPage" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - }, - "x-dropshot-pagination": true - }, - "post": { - "tags": [ - "vpcs" - ], - "summary": "Create a VPC", - "operationId": "vpc_create_v1", - "parameters": [ - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VpcCreate" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "successful creation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Vpc" -======= - "$ref": "#/components/schemas/ComponentUpdateResultsPage" ->>>>>>> main - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, -<<<<<<< HEAD - "/v1/vpcs/{vpc}": { - "get": { - "tags": [ - "vpcs" - ], - "summary": "Fetch a VPC", - "operationId": "vpc_view_v1", - "parameters": [ - { - "in": "path", - "name": "vpc", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], -======= - "/v1/system/update/version": { - "get": { - "tags": [ - "system" - ], - "summary": "View system version and update status", - "operationId": "system_version", ->>>>>>> main - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { -<<<<<<< HEAD - "$ref": "#/components/schemas/Vpc" -======= - "$ref": "#/components/schemas/SystemVersion" ->>>>>>> main - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } -<<<<<<< HEAD - }, - "put": { - "tags": [ - "vpcs" - ], - "summary": "Update a VPC", - "operationId": "vpc_update_v1", - "parameters": [ - { - "in": "path", - "name": "vpc", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VpcUpdate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Vpc" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - }, - "delete": { - "tags": [ - "vpcs" - ], - "summary": "Delete a VPC", - "operationId": "vpc_delete_v1", - "parameters": [ - { - "in": "path", - "name": "vpc", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "responses": { - "204": { - "description": "successful deletion" - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } -======= ->>>>>>> main } } }, @@ -14243,7 +13102,7 @@ "vpc_router_id" ] }, - "RouterRouteCreate": { + "RouterRouteCreateParams": { "description": "Create-time parameters for a [`RouterRoute`]", "type": "object", "properties": { @@ -14321,7 +13180,7 @@ "items" ] }, - "RouterRouteUpdate": { + "RouterRouteUpdateParams": { "description": "Updateable properties of a [`RouterRoute`]", "type": "object", "properties": { From 35c9881c67750bac8d3b37edcbedc4c58ed45885 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 8 Feb 2023 13:25:58 -0500 Subject: [PATCH 29/45] Move network interfaces into their own file --- nexus/src/app/instance.rs | 203 ---------------------------- nexus/src/app/mod.rs | 1 + nexus/src/app/network_interface.rs | 210 +++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 203 deletions(-) create mode 100644 nexus/src/app/network_interface.rs diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 87b76f6ea5c..4338c0d1ddc 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -10,14 +10,12 @@ use super::MAX_NICS_PER_INSTANCE; use crate::app::sagas; use crate::authn; use crate::authz; -use crate::authz::ApiResource; use crate::cidata::InstanceCiData; use crate::context::OpContext; use crate::db; use crate::db::identity::Resource; use crate::db::lookup; use crate::db::lookup::LookupPath; -use crate::db::queries::network_interface; use crate::external_api::params; use futures::future::Fuse; use futures::{FutureExt, SinkExt, StreamExt}; @@ -799,207 +797,6 @@ impl super::Nexus { Ok(disk) } - /// Create a network interface attached to the provided instance. - // TODO-performance: Add a version of this that accepts the instance ID - // directly. This will avoid all the internal database lookups in the event - // that we create many NICs for the same instance, such as in a saga. - pub async fn instance_create_network_interface( - &self, - opctx: &OpContext, - instance_lookup: &lookup::Instance<'_>, - params: ¶ms::NetworkInterfaceCreate, - ) -> CreateResult { - let (.., authz_project, authz_instance) = - instance_lookup.lookup_for(authz::Action::Modify).await?; - - // NOTE: We need to lookup the VPC and VPC Subnet, since we need both - // IDs for creating the network interface. - // - // TODO-correctness: There are additional races here. The VPC and VPC - // Subnet could both be deleted between the time we fetch them and - // actually insert the record for the interface. The solution is likely - // to make both objects implement `DatastoreCollection` for their - // children, and then use `VpcSubnet::insert_resource` inside the - // `instance_create_network_interface` method. See - // https://github.com/oxidecomputer/omicron/issues/738. - let vpc_name = db::model::Name(params.vpc_name.clone()); - let subnet_name = db::model::Name(params.subnet_name.clone()); - let (.., authz_vpc, authz_subnet, db_subnet) = - LookupPath::new(opctx, &self.db_datastore) - .project_id(authz_project.id()) - .vpc_name(&vpc_name) - .vpc_subnet_name(&subnet_name) - .fetch() - .await?; - let interface_id = Uuid::new_v4(); - let interface = db::model::IncompleteNetworkInterface::new( - interface_id, - authz_instance.id(), - authz_vpc.id(), - db_subnet, - params.identity.clone(), - params.ip, - )?; - self.db_datastore - .instance_create_network_interface( - opctx, - &authz_subnet, - &authz_instance, - interface, - ) - .await - .map_err(|e| { - debug!( - self.log, - "failed to create network interface"; - "instance_id" => ?authz_instance.id(), - "interface_id" => ?interface_id, - "error" => ?e, - ); - if matches!( - e, - network_interface::InsertError::InstanceNotFound(_) - ) { - // Return the not-found message of the authz interface - // object, so that the message reflects how the caller - // originally looked it up - authz_instance.not_found() - } else { - // Convert other errors into an appropriate client error - network_interface::InsertError::into_external(e) - } - }) - } - - /// Lists network interfaces attached to the instance. - pub async fn instance_list_network_interfaces( - &self, - opctx: &OpContext, - instance_lookup: &lookup::Instance<'_>, - pagparams: &PaginatedBy<'_>, - ) -> ListResultVec { - let (.., authz_instance) = - instance_lookup.lookup_for(authz::Action::ListChildren).await?; - self.db_datastore - .instance_list_network_interfaces(opctx, &authz_instance, pagparams) - .await - } - - /// Fetch a network interface attached to the given instance. - pub async fn network_interface_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, - interface_name: &Name, - ) -> LookupResult { - let (.., db_interface) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) - .network_interface_name(interface_name) - .fetch() - .await?; - Ok(db_interface) - } - - pub async fn network_interface_fetch_by_id( - &self, - opctx: &OpContext, - interface_id: &Uuid, - ) -> LookupResult { - let (.., db_interface) = LookupPath::new(opctx, &self.db_datastore) - .network_interface_id(*interface_id) - .fetch() - .await?; - Ok(db_interface) - } - - /// Update a network interface for the given instance. - pub async fn network_interface_update( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, - interface_name: &Name, - updates: params::NetworkInterfaceUpdate, - ) -> UpdateResult { - let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) - .lookup_for(authz::Action::Modify) - .await?; - let (.., authz_interface) = LookupPath::new(opctx, &self.db_datastore) - .instance_id(authz_instance.id()) - .network_interface_name(interface_name) - .lookup_for(authz::Action::Modify) - .await?; - self.db_datastore - .instance_update_network_interface( - opctx, - &authz_instance, - &authz_interface, - db::model::NetworkInterfaceUpdate::from(updates), - ) - .await - } - - /// Delete a network interface from the provided instance. - /// - /// Note that the primary interface for an instance cannot be deleted if - /// there are any secondary interfaces. - pub async fn instance_delete_network_interface( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, - interface_name: &Name, - ) -> DeleteResult { - let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) - .lookup_for(authz::Action::Modify) - .await?; - let (.., authz_interface) = LookupPath::new(opctx, &self.db_datastore) - .instance_id(authz_instance.id()) - .network_interface_name(interface_name) - .lookup_for(authz::Action::Delete) - .await?; - self.db_datastore - .instance_delete_network_interface( - opctx, - &authz_instance, - &authz_interface, - ) - .await - .map_err(|e| { - debug!( - self.log, - "failed to delete network interface"; - "instance_id" => ?authz_instance.id(), - "interface_id" => ?authz_interface.id(), - "error" => ?e, - ); - if matches!( - e, - network_interface::DeleteError::InstanceNotFound(_) - ) { - // Return the not-found message of the authz interface - // object, so that the message reflects how the caller - // originally looked it up - authz_instance.not_found() - } else { - // Convert other errors into an appropriate client error - network_interface::DeleteError::into_external(e) - } - }) - } - /// Invoked by a sled agent to publish an updated runtime state for an /// Instance. pub async fn notify_instance_updated( diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index 261f5d46d4a..4d2dd0d5ce3 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -34,6 +34,7 @@ mod image; mod instance; mod ip_pool; mod metrics; +mod network_interface; mod organization; mod oximeter; mod project; diff --git a/nexus/src/app/network_interface.rs b/nexus/src/app/network_interface.rs new file mode 100644 index 00000000000..d023e60e20d --- /dev/null +++ b/nexus/src/app/network_interface.rs @@ -0,0 +1,210 @@ +use crate::authz::ApiResource; +use crate::db::queries::network_interface; +use nexus_db_model::Name; +use nexus_types::external_api::params; +use omicron_common::api::external::CreateResult; +use omicron_common::api::external::DeleteResult; +use omicron_common::api::external::Error; +use omicron_common::api::external::ListResultVec; +use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; + +use omicron_common::api::external::http_pagination::PaginatedBy; +use omicron_common::api::external::UpdateResult; +use ref_cast::RefCast; +use uuid::Uuid; + +use crate::authz; +use crate::db; +use crate::{ + context::OpContext, + db::lookup::{self, LookupPath}, +}; + +impl super::Nexus { + pub fn network_interface_lookup<'a>( + &'a self, + opctx: &'a OpContext, + network_interface_selector: &'a params::NetworkInterfaceSelector, + ) -> LookupResult> { + match network_interface_selector { + params::NetworkInterfaceSelector { + instance_selector: None, + network_interface: NameOrId::Id(id), + } => { + let network_interface = + LookupPath::new(opctx, &self.db_datastore) + .network_interface_id(*id); + Ok(network_interface) + } + params::NetworkInterfaceSelector { + instance_selector: Some(instance_selector), + network_interface: NameOrId::Name(name), + } => { + let network_interface = self + .instance_lookup(opctx, instance_selector)? + .network_interface_name(Name::ref_cast(name)); + Ok(network_interface) + } + params::NetworkInterfaceSelector { + instance_selector: Some(_), + network_interface: NameOrId::Id(_), + } => { + Err(Error::invalid_request( + "when providing network_interface as an id, instance_selector should not be specified" + )) + } + _ => { + Err(Error::invalid_request( + "network_interface should either be a UUID or instance should be specified" + )) + } + } + } + + /// Create a network interface attached to the provided instance. + // TODO-performance: Add a version of this that accepts the instance ID + // directly. This will avoid all the internal database lookups in the event + // that we create many NICs for the same instance, such as in a saga. + pub async fn network_interface_create( + &self, + opctx: &OpContext, + instance_lookup: &lookup::Instance<'_>, + params: ¶ms::NetworkInterfaceCreate, + ) -> CreateResult { + let (.., authz_project, authz_instance) = + instance_lookup.lookup_for(authz::Action::Modify).await?; + + // NOTE: We need to lookup the VPC and VPC Subnet, since we need both + // IDs for creating the network interface. + // + // TODO-correctness: There are additional races here. The VPC and VPC + // Subnet could both be deleted between the time we fetch them and + // actually insert the record for the interface. The solution is likely + // to make both objects implement `DatastoreCollection` for their + // children, and then use `VpcSubnet::insert_resource` inside the + // `instance_create_network_interface` method. See + // https://github.com/oxidecomputer/omicron/issues/738. + let vpc_name = db::model::Name(params.vpc_name.clone()); + let subnet_name = db::model::Name(params.subnet_name.clone()); + let (.., authz_vpc, authz_subnet, db_subnet) = + LookupPath::new(opctx, &self.db_datastore) + .project_id(authz_project.id()) + .vpc_name(&vpc_name) + .vpc_subnet_name(&subnet_name) + .fetch() + .await?; + let interface_id = Uuid::new_v4(); + let interface = db::model::IncompleteNetworkInterface::new( + interface_id, + authz_instance.id(), + authz_vpc.id(), + db_subnet, + params.identity.clone(), + params.ip, + )?; + self.db_datastore + .instance_create_network_interface( + opctx, + &authz_subnet, + &authz_instance, + interface, + ) + .await + .map_err(|e| { + debug!( + self.log, + "failed to create network interface"; + "instance_id" => ?authz_instance.id(), + "interface_id" => ?interface_id, + "error" => ?e, + ); + if matches!( + e, + network_interface::InsertError::InstanceNotFound(_) + ) { + // Return the not-found message of the authz interface + // object, so that the message reflects how the caller + // originally looked it up + authz_instance.not_found() + } else { + // Convert other errors into an appropriate client error + network_interface::InsertError::into_external(e) + } + }) + } + + /// Lists network interfaces attached to the instance. + pub async fn network_interface_list( + &self, + opctx: &OpContext, + instance_lookup: &lookup::Instance<'_>, + pagparams: &PaginatedBy<'_>, + ) -> ListResultVec { + let (.., authz_instance) = + instance_lookup.lookup_for(authz::Action::ListChildren).await?; + self.db_datastore + .instance_list_network_interfaces(opctx, &authz_instance, pagparams) + .await + } + + /// Update a network interface for the given instance. + pub async fn network_interface_update( + &self, + opctx: &OpContext, + network_interface_lookup: &lookup::NetworkInterface<'_>, + updates: params::NetworkInterfaceUpdate, + ) -> UpdateResult { + let (.., authz_instance, authz_interface) = + network_interface_lookup.lookup_for(authz::Action::Modify).await?; + self.db_datastore + .instance_update_network_interface( + opctx, + &authz_instance, + &authz_interface, + db::model::NetworkInterfaceUpdate::from(updates), + ) + .await + } + + /// Delete a network interface from the provided instance. + /// + /// Note that the primary interface for an instance cannot be deleted if + /// there are any secondary interfaces. + pub async fn network_interface_delete( + &self, + opctx: &OpContext, + network_interface_lookup: &lookup::NetworkInterface<'_>, + ) -> DeleteResult { + let (.., authz_instance, authz_interface) = + network_interface_lookup.lookup_for(authz::Action::Delete).await?; + self.db_datastore + .instance_delete_network_interface( + opctx, + &authz_instance, + &authz_interface, + ) + .await + .map_err(|e| { + debug!( + self.log, + "failed to delete network interface"; + "instance_id" => ?authz_instance.id(), + "interface_id" => ?authz_interface.id(), + "error" => ?e, + ); + if matches!( + e, + network_interface::DeleteError::InstanceNotFound(_) + ) { + // Return the not-found message of the authz interface + // object, so that the message reflects how the caller + // originally looked it up + authz_instance.not_found() + } else { + // Convert other errors into an appropriate client error + network_interface::DeleteError::into_external(e) + } + }) + } +} From 5f4d1b3778435bb84d15108289dd0070cc487876 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 8 Feb 2023 13:30:43 -0500 Subject: [PATCH 30/45] Fix type failures --- nexus/src/external_api/http_entrypoints.rs | 191 ++++++++++++--------- nexus/types/src/external_api/params.rs | 5 + 2 files changed, 112 insertions(+), 84 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index ad3664415da..6fd3d7e6fb0 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -3976,13 +3976,10 @@ async fn instance_network_interface_list_v1( let pag_params = data_page_params_for(&rqctx, &query)?; let scan_params = ScanByNameOrId::from_query(&query)?; let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; - let instance_lookup = nexus.instance_lookup(&opctx, &query)?; + let instance_lookup = + nexus.instance_lookup(&opctx, &scan_params.selector)?; let interfaces = nexus - .instance_list_network_interfaces( - &opctx, - &instance_lookup, - &paginated_by, - ) + .network_interface_list(&opctx, &instance_lookup, &paginated_by) .await? .into_iter() .map(|d| d.into()) @@ -4020,7 +4017,7 @@ async fn instance_network_interface_list( let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; let interfaces = nexus - .instance_list_network_interfaces( + .network_interface_list( &opctx, &instance_lookup, &PaginatedBy::Name(data_page_params_for(&rqctx, &query)?), @@ -4056,7 +4053,7 @@ async fn instance_network_interface_create_v1( let query = query_params.into_inner(); let instance_lookup = nexus.instance_lookup(&opctx, &query)?; let iface = nexus - .instance_create_network_interface( + .network_interface_create( &opctx, &instance_lookup, &interface_params.into_inner(), @@ -4093,7 +4090,7 @@ async fn instance_network_interface_create( let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; let iface = nexus - .instance_create_network_interface( + .network_interface_create( &opctx, &instance_lookup, &interface_params.into_inner(), @@ -4112,6 +4109,40 @@ pub struct NetworkInterfacePathParam { pub interface_name: Name, } +/// Delete a network interface +/// +/// Note that the primary interface for an instance cannot be deleted if there +/// are any secondary interfaces. A new primary interface must be designated +/// first. The primary interface can be deleted if there are no secondary +/// interfaces. +#[endpoint { + method = DELETE, + path = "/v1/network-interfaces/{interface}", + tags = ["instances"], +}] +async fn instance_network_interface_delete_v1( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let interface_selector = params::NetworkInterfaceSelector { + instance_selector: query.instance_selector, + network_interface: path.interface.into(), + }; + let interface_lookup = + nexus.network_interface_lookup(&opctx, &interface_selector)?; + nexus.network_interface_delete(&opctx, &interface_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete a network interface /// /// Note that the primary interface for an instance cannot be deleted if there @@ -4128,23 +4159,19 @@ async fn instance_network_interface_delete( path_params: Path, ) -> Result { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let interface_name = &path.interface_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus - .instance_delete_network_interface( - &opctx, - organization_name, - project_name, - instance_name, - interface_name, - ) - .await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let interface_selector = params::NetworkInterfaceSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.instance_name.into()), + path.interface_name.into(), + ); + let interface_lookup = + nexus.network_interface_lookup(&opctx, &interface_selector)?; + nexus.network_interface_delete(&opctx, &interface_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -4153,32 +4180,29 @@ async fn instance_network_interface_delete( /// Fetch a network interface #[endpoint { method = GET, - path = "/v1/network-interfaces/{interface_name}", + path = "/v1/network-interfaces/{interface}", tags = ["instances"], }] async fn instance_network_interface_view_v1( rqctx: RequestContext>, - path_params: Path, + path_params: Path, + query_params: Query, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let interface_name = &path.interface_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let interface = nexus - .network_interface_fetch( - &opctx, - organization_name, - project_name, - instance_name, - interface_name, - ) + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let interface_selector = params::NetworkInterfaceSelector { + instance_selector: query.instance_selector, + network_interface: path.interface, + }; + let (.., interface) = nexus + .network_interface_lookup(&opctx, &interface_selector)? + .fetch() .await?; - Ok(HttpResponseOk(NetworkInterface::from(interface))) + Ok(HttpResponseOk(interface.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -4195,24 +4219,21 @@ async fn instance_network_interface_view( path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let interface_name = &path.interface_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let interface = nexus - .network_interface_fetch( - &opctx, - organization_name, - project_name, - instance_name, - interface_name, - ) + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let interface_selector = params::NetworkInterfaceSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.instance_name.into()), + path.interface_name.into(), + ); + let (.., interface) = nexus + .network_interface_lookup(&opctx, &interface_selector)? + .fetch() .await?; - Ok(HttpResponseOk(NetworkInterface::from(interface))) + Ok(HttpResponseOk(interface.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -4228,14 +4249,19 @@ async fn instance_network_interface_view_by_id( path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let network_interface = - nexus.network_interface_fetch_by_id(&opctx, id).await?; - Ok(HttpResponseOk(network_interface.into())) + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let interface_selector = params::NetworkInterfaceSelector { + instance_selector: None, + network_interface: path.id.into(), + }; + let (.., interface) = nexus + .network_interface_lookup(&opctx, &interface_selector)? + .fetch() + .await?; + Ok(HttpResponseOk(interface.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -4246,9 +4272,9 @@ async fn instance_network_interface_view_by_id( path = "/v1/network-interfaces/{interface_name}", tags = ["instances"], }] -async fn instance_network_interface_update( +async fn instance_network_interface_update_v1( rqctx: RequestContext>, - path_params: Path, + path_params: Path, query_params: Query, updated_iface: TypedBody, ) -> Result, HttpError> { @@ -4259,12 +4285,12 @@ async fn instance_network_interface_update( let path = path_params.into_inner(); let query = query_params.into_inner(); let updated_iface = updated_iface.into_inner(); - let network_interface_slector = params::NetworkInterfaceSelector { + let network_interface_selector = params::NetworkInterfaceSelector { instance_selector: query.instance_selector, - network_interface: path.network_interface, + network_interface: path.interface, }; - let network_interface_lookup = - nexus.network_interface_lookup(&opctx, &instance_selector)?; + let network_interface_lookup = nexus + .network_interface_lookup(&opctx, &network_interface_selector)?; let interface = nexus .network_interface_update( &opctx, @@ -4289,26 +4315,23 @@ async fn instance_network_interface_update( updated_iface: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let interface_name = &path.interface_name; - let updated_iface = updated_iface.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let updated_iface = updated_iface.into_inner(); + let interface_selector = params::NetworkInterfaceSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + Some(path.instance_name.into()), + path.interface_name.into(), + ); + let interface_lookup = + nexus.network_interface_lookup(&opctx, &interface_selector)?; let interface = nexus - .network_interface_update( - &opctx, - organization_name, - project_name, - instance_name, - interface_name, - updated_iface, - ) + .network_interface_update(&opctx, &interface_lookup, updated_iface) .await?; - Ok(HttpResponseOk(NetworkInterface::from(interface))) + Ok(HttpResponseOk(interface.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 2a5e1f7c911..63441b2871f 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -36,6 +36,11 @@ pub struct InstancePath { pub instance: NameOrId, } +#[derive(Deserialize, JsonSchema)] +pub struct NetworkInterfacePath { + pub interface: NameOrId, +} + #[derive(Deserialize, JsonSchema)] pub struct VpcPath { pub vpc: NameOrId, From 6ef30f075f413d061c7657773547b1e4087be9f8 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 8 Feb 2023 13:41:19 -0500 Subject: [PATCH 31/45] Remove unncessary into --- nexus/src/external_api/http_entrypoints.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 6fd3d7e6fb0..d226f5bb06e 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -4133,7 +4133,7 @@ async fn instance_network_interface_delete_v1( let query = query_params.into_inner(); let interface_selector = params::NetworkInterfaceSelector { instance_selector: query.instance_selector, - network_interface: path.interface.into(), + network_interface: path.interface, }; let interface_lookup = nexus.network_interface_lookup(&opctx, &interface_selector)?; From 2be9970f614b439a6a8a3cf48fe9dd123f7c61ee Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 8 Feb 2023 14:19:44 -0500 Subject: [PATCH 32/45] Remove unused imports --- nexus/tests/integration_tests/endpoints.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index ccc90b28244..a1df91ad878 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -23,8 +23,6 @@ use omicron_common::api::external::Ipv4Net; use omicron_common::api::external::Name; use omicron_common::api::external::RouteDestination; use omicron_common::api::external::RouteTarget; -use omicron_common::api::external::RouterRouteCreateParams; -use omicron_common::api::external::RouterRouteUpdateParams; use omicron_common::api::external::SemverVersion; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use omicron_nexus::authn; From 7aeab079b123b8f1b5e2f51a5855b576d26a6bac Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 8 Feb 2023 17:23:24 -0500 Subject: [PATCH 33/45] Ensure selectors can be flattened --- nexus/types/src/external_api/params.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 63441b2871f..b6a2e33919d 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -152,10 +152,12 @@ impl InstanceSelector { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct OptionalInstanceSelector { + #[serde(flatten)] pub instance_selector: Option, } pub struct NetworkInterfaceSelector { + #[serde(flatten)] pub instance_selector: Option, pub network_interface: NameOrId, } From 0b2f882761d69a48cff134c4a90e1468d20bdce2 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 8 Feb 2023 17:32:22 -0500 Subject: [PATCH 34/45] Fix remaining path issues; update nexus spec --- nexus/src/external_api/http_entrypoints.rs | 4 +- nexus/tests/output/nexus_tags.txt | 5 + nexus/types/src/external_api/params.rs | 1 + openapi/nexus.json | 1727 +++++++++++++++++++- 4 files changed, 1669 insertions(+), 68 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index d226f5bb06e..5902085e654 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -4269,7 +4269,7 @@ async fn instance_network_interface_view_by_id( /// Update a network interface #[endpoint { method = PUT, - path = "/v1/network-interfaces/{interface_name}", + path = "/v1/network-interfaces/{interface}", tags = ["instances"], }] async fn instance_network_interface_update_v1( @@ -5535,7 +5535,7 @@ async fn vpc_router_view_by_id( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -// Create a router +/// Create a VPC router #[endpoint { method = POST, path = "/v1/vpc-routers", diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 06e72234f08..6663338be0d 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -47,11 +47,16 @@ instance_list_v1 /v1/instances instance_migrate /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/migrate instance_migrate_v1 /v1/instances/{instance}/migrate instance_network_interface_create /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces +instance_network_interface_create_v1 /v1/network-interfaces instance_network_interface_delete /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name} +instance_network_interface_delete_v1 /v1/network-interfaces/{interface} instance_network_interface_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces +instance_network_interface_list_v1 /v1/network-interfaces instance_network_interface_update /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name} +instance_network_interface_update_v1 /v1/network-interfaces/{interface} instance_network_interface_view /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name} instance_network_interface_view_by_id /by-id/network-interfaces/{id} +instance_network_interface_view_v1 /v1/network-interfaces/{interface} instance_reboot /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/reboot instance_reboot_v1 /v1/instances/{instance}/reboot instance_serial_console /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index b6a2e33919d..affd1f4feb5 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -156,6 +156,7 @@ pub struct OptionalInstanceSelector { pub instance_selector: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct NetworkInterfaceSelector { #[serde(flatten)] pub instance_selector: Option, diff --git a/openapi/nexus.json b/openapi/nexus.json index 0640d349b23..112e55c1534 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -364,6 +364,7 @@ "vpcs" ], "summary": "Fetch a subnet by id", + "description": "Use `GET /v1/vpc-subnets/{id}` instead", "operationId": "vpc_subnet_view_by_id", "parameters": [ { @@ -393,7 +394,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/by-id/vpcs/{id}": { @@ -402,6 +404,7 @@ "vpcs" ], "summary": "Fetch a VPC", + "description": "Use `GET /v1/vpcs/{id}` instead", "operationId": "vpc_view_by_id", "parameters": [ { @@ -431,7 +434,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/device/auth": { @@ -2563,6 +2567,7 @@ "instances" ], "summary": "Create a network interface", + "description": "Use `POST /v1/network-interfaces` instead", "operationId": "instance_network_interface_create", "parameters": [ { @@ -2617,7 +2622,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}": { @@ -2678,7 +2684,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ @@ -3442,6 +3449,7 @@ "vpcs" ], "summary": "List VPCs", + "description": "Use `GET /v1/vpcs` instead", "operationId": "vpc_list", "parameters": [ { @@ -3508,6 +3516,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -3515,6 +3524,7 @@ "vpcs" ], "summary": "Create a VPC", + "description": "Use `POST /v1/vpcs` instead", "operationId": "vpc_create", "parameters": [ { @@ -3563,7 +3573,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}": { @@ -3572,6 +3583,7 @@ "vpcs" ], "summary": "Fetch a VPC", + "description": "Use `GET /v1/vpcs/{vpc}` instead", "operationId": "vpc_view", "parameters": [ { @@ -3616,13 +3628,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "vpcs" ], "summary": "Update a VPC", + "description": "Use `PUT /v1/vpcs/{vpc}` instead", "operationId": "vpc_update", "parameters": [ { @@ -3677,13 +3691,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a VPC", + "description": "Use `DELETE /v1/vpcs/{vpc}` instead", "operationId": "vpc_delete", "parameters": [ { @@ -3721,7 +3737,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/firewall/rules": { @@ -3844,6 +3861,7 @@ "vpcs" ], "summary": "List routers", + "description": "Use `GET /v1/vpc-routers` instead", "operationId": "vpc_router_list", "parameters": [ { @@ -3916,6 +3934,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -3923,6 +3942,7 @@ "vpcs" ], "summary": "Create a router", + "description": "Use `POST /v1/vpc-routers` instead", "operationId": "vpc_router_create", "parameters": [ { @@ -3977,7 +3997,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}": { @@ -3986,6 +4007,7 @@ "vpcs" ], "summary": "Get a router", + "description": "Use `GET /v1/vpc-routers/{router}` instead", "operationId": "vpc_router_view", "parameters": [ { @@ -4038,7 +4060,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ @@ -4107,13 +4130,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a router", + "description": "Use `DELETE /v1/vpc-routers/{router}` instead", "operationId": "vpc_router_delete", "parameters": [ { @@ -4159,7 +4184,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes": { @@ -4295,7 +4321,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRouteCreateParams" + "$ref": "#/components/schemas/RouterRouteCreate" } } }, @@ -4327,6 +4353,7 @@ "vpcs" ], "summary": "Fetch a route", + "description": "Use `GET /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_view", "parameters": [ { @@ -4394,6 +4421,7 @@ "vpcs" ], "summary": "Update a route", + "description": "Use `PUT /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_update", "parameters": [ { @@ -4441,7 +4469,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouterRouteUpdateParams" + "$ref": "#/components/schemas/RouterRouteUpdate" } } }, @@ -4464,13 +4492,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a route", + "description": "Use `DELETE /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_delete", "parameters": [ { @@ -4524,7 +4554,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets": { @@ -4533,6 +4564,7 @@ "vpcs" ], "summary": "List subnets", + "description": "Use `GET /v1/vpc-subnets` instead", "operationId": "vpc_subnet_list", "parameters": [ { @@ -4605,6 +4637,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -4612,6 +4645,7 @@ "vpcs" ], "summary": "Create a subnet", + "description": "Use `POST /v1/vpc-subnets` instead", "operationId": "vpc_subnet_create", "parameters": [ { @@ -4666,7 +4700,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}": { @@ -4675,6 +4710,7 @@ "vpcs" ], "summary": "Fetch a subnet", + "description": "Use `GET /v1/vpc-subnets/{subnet}` instead", "operationId": "vpc_subnet_view", "parameters": [ { @@ -4727,13 +4763,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "vpcs" ], "summary": "Update a subnet", + "description": "Use `PUT /v1/vpc-subnets/{subnet}` instead", "operationId": "vpc_subnet_update", "parameters": [ { @@ -4796,13 +4834,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "vpcs" ], "summary": "Delete a subnet", + "description": "Use `DELETE /v1/vpc-subnets/{subnet}` instead", "operationId": "vpc_subnet_delete", "parameters": [ { @@ -4848,7 +4888,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/network-interfaces": { @@ -8663,14 +8704,21 @@ } } }, - "/v1/organizations": { + "/v1/network-interfaces": { "get": { "tags": [ - "organizations" + "instances" ], - "summary": "List organizations", - "operationId": "organization_list_v1", + "summary": "List network interfaces", + "operationId": "instance_network_interface_list_v1", "parameters": [ + { + "in": "query", + "name": "instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, { "in": "query", "name": "limit", @@ -8682,6 +8730,13 @@ "minimum": 1 } }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, { "in": "query", "name": "page_token", @@ -8691,6 +8746,13 @@ "type": "string" } }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, { "in": "query", "name": "sort_by", @@ -8705,7 +8767,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResultsPage" + "$ref": "#/components/schemas/NetworkInterfaceResultsPage" } } } @@ -8721,15 +8783,39 @@ }, "post": { "tags": [ - "organizations" + "instances" + ], + "summary": "Create a network interface", + "operationId": "instance_network_interface_create_v1", + "parameters": [ + { + "in": "query", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } ], - "summary": "Create an organization", - "operationId": "organization_create_v1", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationCreate" + "$ref": "#/components/schemas/NetworkInterfaceCreate" } } }, @@ -8741,7 +8827,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Organization" + "$ref": "#/components/schemas/NetworkInterface" } } } @@ -8755,21 +8841,42 @@ } } }, - "/v1/organizations/{organization}": { + "/v1/network-interfaces/{interface}": { "get": { "tags": [ - "organizations" + "instances" ], - "summary": "Fetch an organization", - "operationId": "organization_view_v1", + "summary": "Fetch a network interface", + "operationId": "instance_network_interface_view_v1", "parameters": [ { "in": "path", - "name": "organization", + "name": "interface", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } + }, + { + "in": "query", + "name": "instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } } ], "responses": { @@ -8778,7 +8885,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Organization" + "$ref": "#/components/schemas/NetworkInterface" } } } @@ -8793,25 +8900,46 @@ }, "put": { "tags": [ - "organizations" + "instances" ], - "summary": "Update an organization", - "operationId": "organization_update_v1", + "summary": "Update a network interface", + "operationId": "instance_network_interface_update_v1", "parameters": [ { "in": "path", - "name": "organization", + "name": "interface", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } + }, + { + "in": "query", + "name": "instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationUpdate" + "$ref": "#/components/schemas/NetworkInterfaceUpdate" } } }, @@ -8823,7 +8951,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Organization" + "$ref": "#/components/schemas/NetworkInterface" } } } @@ -8838,18 +8966,40 @@ }, "delete": { "tags": [ - "organizations" + "instances" ], - "summary": "Delete an organization", - "operationId": "organization_delete_v1", + "summary": "Delete a network interface", + "description": "Note that the primary interface for an instance cannot be deleted if there are any secondary interfaces. A new primary interface must be designated first. The primary interface can be deleted if there are no secondary interfaces.", + "operationId": "instance_network_interface_delete_v1", "parameters": [ { "in": "path", - "name": "organization", + "name": "interface", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } + }, + { + "in": "query", + "name": "instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } } ], "responses": { @@ -8865,20 +9015,39 @@ } } }, - "/v1/organizations/{organization}/policy": { + "/v1/organizations": { "get": { "tags": [ "organizations" ], - "summary": "Fetch an organization's IAM policy", - "operationId": "organization_policy_view_v1", + "summary": "List organizations", + "operationId": "organization_list_v1", "parameters": [ { - "in": "path", - "name": "organization", - "required": true, + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", "schema": { - "$ref": "#/components/schemas/NameOrId" + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" } } ], @@ -8888,7 +9057,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationRolePolicy" + "$ref": "#/components/schemas/OrganizationResultsPage" } } } @@ -8899,20 +9068,203 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "x-dropshot-pagination": true }, - "put": { + "post": { "tags": [ "organizations" ], - "summary": "Update an organization's IAM policy", - "operationId": "organization_policy_update_v1", - "parameters": [ - { - "in": "path", - "name": "organization", - "required": true, - "schema": { + "summary": "Create an organization", + "operationId": "organization_create_v1", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/organizations/{organization}": { + "get": { + "tags": [ + "organizations" + ], + "summary": "Fetch an organization", + "operationId": "organization_view_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "organizations" + ], + "summary": "Update an organization", + "operationId": "organization_update_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "organizations" + ], + "summary": "Delete an organization", + "operationId": "organization_delete_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/organizations/{organization}/policy": { + "get": { + "tags": [ + "organizations" + ], + "summary": "Fetch an organization's IAM policy", + "operationId": "organization_policy_view_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "organizations" + ], + "summary": "Update an organization's IAM policy", + "operationId": "organization_policy_update_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { "$ref": "#/components/schemas/NameOrId" } } @@ -9670,6 +10022,1249 @@ } } } + }, + "/v1/vpc-router-routes": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List the routes associated with a router in a particular VPC.", + "operationId": "vpc_router_route_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create a router", + "operationId": "vpc_router_route_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-router-routes/{route}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch a route", + "operationId": "vpc_router_route_view_v1", + "parameters": [ + { + "in": "path", + "name": "route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update a route", + "operationId": "vpc_router_route_update_v1", + "parameters": [ + { + "in": "path", + "name": "route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete a route", + "operationId": "vpc_router_route_delete_v1", + "parameters": [ + { + "in": "path", + "name": "route", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "router", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-routers": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List routers", + "operationId": "vpc_router_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create a VPC router", + "operationId": "vpc_router_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-routers/{router}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Get a router", + "operationId": "vpc_router_view_v1", + "parameters": [ + { + "in": "path", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update a router", + "operationId": "vpc_router_update_v1", + "parameters": [ + { + "in": "path", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouterUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete a router", + "operationId": "vpc_router_delete_v1", + "parameters": [ + { + "in": "path", + "name": "router", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-subnets": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch a subnet", + "operationId": "vpc_subnet_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create a subnet", + "operationId": "vpc_subnet_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpc-subnets/{subnet}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch a subnet", + "operationId": "vpc_subnet_view_v1", + "parameters": [ + { + "in": "path", + "name": "subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update a subnet", + "operationId": "vpc_subnet_update_v1", + "parameters": [ + { + "in": "path", + "name": "subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnetUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete a subnet", + "operationId": "vpc_subnet_delete_v1", + "parameters": [ + { + "in": "path", + "name": "subnet", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "vpc", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpcs": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "List VPCs", + "operationId": "vpc_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "vpcs" + ], + "summary": "Create a VPC", + "operationId": "vpc_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/vpcs/{vpc}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Fetch a VPC", + "operationId": "vpc_view_v1", + "parameters": [ + { + "in": "path", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "vpcs" + ], + "summary": "Update a VPC", + "operationId": "vpc_update_v1", + "parameters": [ + { + "in": "path", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "vpcs" + ], + "summary": "Delete a VPC", + "operationId": "vpc_delete_v1", + "parameters": [ + { + "in": "path", + "name": "vpc", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { @@ -13117,7 +14712,7 @@ "vpc_router_id" ] }, - "RouterRouteCreateParams": { + "RouterRouteCreate": { "description": "Create-time parameters for a [`RouterRoute`]", "type": "object", "properties": { @@ -13195,7 +14790,7 @@ "items" ] }, - "RouterRouteUpdateParams": { + "RouterRouteUpdate": { "description": "Updateable properties of a [`RouterRoute`]", "type": "object", "properties": { From 9985ba2d0dae9d8341119aa5e6965290ec2095e3 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 9 Feb 2023 11:54:18 -0500 Subject: [PATCH 35/45] Remove unused URLs --- nexus/tests/integration_tests/vpc_subnets.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nexus/tests/integration_tests/vpc_subnets.rs b/nexus/tests/integration_tests/vpc_subnets.rs index 36eb9acde02..f31e6da2f6d 100644 --- a/nexus/tests/integration_tests/vpc_subnets.rs +++ b/nexus/tests/integration_tests/vpc_subnets.rs @@ -43,12 +43,6 @@ async fn test_delete_vpc_subnet_with_interfaces_fails( let _ = create_project(&client, org_name, project_name).await; populate_ip_pool(client, "default", None).await; - let vpcs_url = - format!("/v1/vpcs?organization={}&project={}", org_name, project_name); - let vpc_url = format!( - "/v1/vpcs/default?organization={}&project={}", - org_name, project_name - ); let subnets_url = format!( "/v1/vpc-subnets?organization={}&project={}", org_name, project_name From d6acd22bacdb7bb15a9a087066771e68983e404f Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 9 Feb 2023 12:35:46 -0500 Subject: [PATCH 36/45] Fix doctest failure --- nexus/types/src/external_api/params.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index affd1f4feb5..ade3e35119b 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -1087,7 +1087,7 @@ pub struct VpcRouterUpdate { // VPC ROUTER ROUTES -/// Create-time parameters for a [`RouterRoute`] +/// Create-time parameters for a [`omicron_common::api::external::RouterRoute`] #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct RouterRouteCreate { #[serde(flatten)] @@ -1096,7 +1096,7 @@ pub struct RouterRouteCreate { pub destination: RouteDestination, } -/// Updateable properties of a [`RouterRoute`] +/// Updateable properties of a [`omicron_common::api::external::RouterRoute`] #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct RouterRouteUpdate { #[serde(flatten)] From b14dba6ae882a45eac386a8a861dfbfa91755b61 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 9 Feb 2023 13:29:41 -0500 Subject: [PATCH 37/45] Fix vpc subnets test --- nexus/tests/integration_tests/vpc_subnets.rs | 28 ++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/nexus/tests/integration_tests/vpc_subnets.rs b/nexus/tests/integration_tests/vpc_subnets.rs index f31e6da2f6d..bc922bd3fda 100644 --- a/nexus/tests/integration_tests/vpc_subnets.rs +++ b/nexus/tests/integration_tests/vpc_subnets.rs @@ -44,11 +44,11 @@ async fn test_delete_vpc_subnet_with_interfaces_fails( populate_ip_pool(client, "default", None).await; let subnets_url = format!( - "/v1/vpc-subnets?organization={}&project={}", + "/v1/vpc-subnets?organization={}&project={}&vpc=default", org_name, project_name ); let subnet_url = format!( - "/v1/vpc-subnets/default?organization={}&project={}", + "/v1/vpc-subnets/default?organization={}&project={}&vpc=default", org_name, project_name ); @@ -96,7 +96,7 @@ async fn test_delete_vpc_subnet_with_interfaces_fails( NexusRequest::object_delete( &client, &format!( - "/v1/vpc-subnets/{}?organization={org_name}&project={project_name}", + "/v1/vpc-subnets/{}?organization={org_name}&project={project_name}&vpc=default", subnets[0].identity.name ), ) @@ -124,8 +124,8 @@ async fn test_vpc_subnets(cptestctx: &ControlPlaneTestContext) { let vpc = create_vpc(&client, org_name, project_name, vpc_name).await; let subnets_url = format!( - "/v1/vpc-subnets?organization={}&project={}", - org_name, project_name + "/v1/vpc-subnets?organization={}&project={}&vpc={}", + org_name, project_name, vpc_name ); // get subnets should return the default subnet @@ -136,7 +136,10 @@ async fn test_vpc_subnets(cptestctx: &ControlPlaneTestContext) { // delete default subnet NexusRequest::object_delete( &client, - &format!("{}/{}", subnets_url, subnets[0].identity.name), + &format!( + "/v1/vpc-subnets/{}?organization={}&project={}&vpc={}", + subnets[0].identity.name, org_name, project_name, vpc_name + ), ) .authn_as(AuthnMode::PrivilegedUser) .execute() @@ -150,8 +153,8 @@ async fn test_vpc_subnets(cptestctx: &ControlPlaneTestContext) { let subnet_name = "subnet1"; let subnet_url = format!( - "/v1/vpc-subnets/{}?organization={}&project={}", - subnet_name, org_name, project_name + "/v1/vpc-subnets/{}?organization={}&project={}&vpc={}", + subnet_name, org_name, project_name, vpc_name ); // fetching a particular subnet should 404 @@ -271,8 +274,8 @@ async fn test_vpc_subnets(cptestctx: &ControlPlaneTestContext) { let subnet2_name = "subnet2"; let subnet2_url = format!( - "/v1/vpc-subnets/{}?organization={}&project={}", - subnet2_name, org_name, project_name + "/v1/vpc-subnets/{}?organization={}&project={}&vpc={}", + subnet2_name, org_name, project_name, vpc_name ); // second subnet 404s before it's created @@ -349,7 +352,10 @@ async fn test_vpc_subnets(cptestctx: &ControlPlaneTestContext) { .unwrap(); assert_eq!(error.message, "not found: vpc-subnet with name \"subnet1\""); - let subnet_url = format!("{}/{}", subnets_url, "new-name"); + let subnet_url = format!( + "/v1/vpc-subnets/new-name?organization={}&project={}&vpc={}", + org_name, project_name, vpc_name + ); // fetching by new name works let updated_subnet: VpcSubnet = From 267ecc4004575e677b2ade2ab3aeed97e04d2efc Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 9 Feb 2023 14:51:27 -0500 Subject: [PATCH 38/45] Fix authz test --- nexus/tests/integration_tests/endpoints.rs | 4 ++-- openapi/nexus.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 49f7e8a00a4..b73e563568a 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -171,7 +171,7 @@ lazy_static! { pub static ref DEMO_VPC_ROUTER_URL: String = format!("/v1/vpc-routers/{}?organization={}&project={}&vpc={}", *DEMO_VPC_ROUTER_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_ROUTER_URL_ROUTES: String = - format!("/v1/vpc-router-routes?organization={}&project={}&vpc={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); + format!("/v1/vpc-router-routes?organization={}&project={}&vpc={}&router={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_VPC_ROUTER_NAME); pub static ref DEMO_VPC_ROUTER_CREATE: params::VpcRouterCreate = params::VpcRouterCreate { identity: IdentityMetadataCreateParams { @@ -184,7 +184,7 @@ lazy_static! { pub static ref DEMO_ROUTER_ROUTE_NAME: Name = "demo-router-route".parse().unwrap(); pub static ref DEMO_ROUTER_ROUTE_URL: String = - format!("/v1/vpc-router-routes/{}?organization={}&project={}&vpc={}", *DEMO_ROUTER_ROUTE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); + format!("/v1/vpc-router-routes/{}?organization={}&project={}&vpc={}&router={}", *DEMO_ROUTER_ROUTE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_VPC_ROUTER_NAME); pub static ref DEMO_ROUTER_ROUTE_CREATE: params::RouterRouteCreate = params::RouterRouteCreate { identity: IdentityMetadataCreateParams { diff --git a/openapi/nexus.json b/openapi/nexus.json index 112e55c1534..727e6594385 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -14713,7 +14713,7 @@ ] }, "RouterRouteCreate": { - "description": "Create-time parameters for a [`RouterRoute`]", + "description": "Create-time parameters for a [`omicron_common::api::external::RouterRoute`]", "type": "object", "properties": { "description": { @@ -14791,7 +14791,7 @@ ] }, "RouterRouteUpdate": { - "description": "Updateable properties of a [`RouterRoute`]", + "description": "Updateable properties of a [`omicron_common::api::external::RouterRoute`]", "type": "object", "properties": { "description": { From dc7de200821d5b78b726c65204370cde4265beb0 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 9 Feb 2023 21:27:10 -0500 Subject: [PATCH 39/45] Add authz coverage for network interfaces --- nexus/tests/integration_tests/endpoints.rs | 13 ++----------- nexus/tests/output/uncovered-authz-endpoints.txt | 6 ++++++ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index b73e563568a..ee59d365318 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -257,7 +257,7 @@ lazy_static! { // To be migrated... pub static ref DEMO_INSTANCE_NICS_URL: String = - format!("/organizations/{}/projects/{}/instances/{}/network-interfaces", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); + format!("/v1/network-interfaces?organization={}&project={}&instance={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); pub static ref DEMO_INSTANCE_EXTERNAL_IPS_URL: String = format!("/organizations/{}/projects/{}/instances/{}/external-ips", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); pub static ref DEMO_INSTANCE_CREATE: params::InstanceCreate = @@ -283,7 +283,7 @@ lazy_static! { pub static ref DEMO_INSTANCE_NIC_NAME: Name = nexus_defaults::DEFAULT_PRIMARY_NIC_NAME.parse().unwrap(); pub static ref DEMO_INSTANCE_NIC_URL: String = - format!("{}/{}", *DEMO_INSTANCE_NICS_URL, *DEMO_INSTANCE_NIC_NAME); + format!("/v1/network-interfaces/{}?organization={}&project={}&instance={}", *DEMO_INSTANCE_NIC_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); pub static ref DEMO_INSTANCE_NIC_CREATE: params::NetworkInterfaceCreate = params::NetworkInterfaceCreate { identity: IdentityMetadataCreateParams { @@ -1376,15 +1376,6 @@ lazy_static! { ], }, - VerifyEndpoint { - url: "/by-id/network-interfaces/{id}", - visibility: Visibility::Protected, - unprivileged_access: UnprivilegedAccess::None, - allowed_methods: vec![ - AllowedMethod::Get, - ], - }, - VerifyEndpoint { url: &DEMO_INSTANCE_NIC_URL, visibility: Visibility::Protected, diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index d0035eee4dc..c8302269dc0 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -3,12 +3,14 @@ organization_delete (delete "/organizations/{organization_n project_delete (delete "/organizations/{organization_name}/projects/{project_name}") disk_delete (delete "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}") instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") +instance_network_interface_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}") vpc_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") vpc_router_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}") vpc_router_route_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}") vpc_subnet_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") disk_view_by_id (get "/by-id/disks/{id}") instance_view_by_id (get "/by-id/instances/{id}") +instance_network_interface_view_by_id (get "/by-id/network-interfaces/{id}") organization_view_by_id (get "/by-id/organizations/{id}") project_view_by_id (get "/by-id/projects/{id}") login_saml_begin (get "/login/{silo_name}/saml/{provider_name}") @@ -22,6 +24,8 @@ disk_view (get "/organizations/{organization_n instance_list (get "/organizations/{organization_name}/projects/{project_name}/instances") instance_view (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") instance_disk_list (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks") +instance_network_interface_list (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces") +instance_network_interface_view (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}") instance_serial_console (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console") instance_serial_console_stream (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console/stream") project_policy_view (get "/organizations/{organization_name}/projects/{project_name}/policy") @@ -47,6 +51,7 @@ instance_create (post "/organizations/{organization_n instance_disk_attach (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach") instance_disk_detach (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach") instance_migrate (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/migrate") +instance_network_interface_create (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces") instance_reboot (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/reboot") instance_start (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start") instance_stop (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop") @@ -57,6 +62,7 @@ vpc_subnet_create (post "/organizations/{organization_n organization_update (put "/organizations/{organization_name}") organization_policy_update (put "/organizations/{organization_name}/policy") project_update (put "/organizations/{organization_name}/projects/{project_name}") +instance_network_interface_update (put "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}") project_policy_update (put "/organizations/{organization_name}/projects/{project_name}/policy") vpc_update (put "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") vpc_router_update (put "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}") From e30a398c476d067b154c41938403d2330a484dd1 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 13 Feb 2023 12:45:00 -0500 Subject: [PATCH 40/45] Fix conflicts after merge --- nexus/src/external_api/http_entrypoints.rs | 3 +-- .../output/uncovered-authz-endpoints.txt | 12 +++++++++-- nexus/types/src/external_api/params.rs | 21 ------------------- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 33e1a53477c..299bfce997f 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2350,8 +2350,7 @@ async fn disk_list( Some(path.organization_name.into()), path.project_name.into(), ); - let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_project = nexus.project_lookup(&opctx, &project_selector)?; + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let disks = nexus .disk_list( &opctx, diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index cc55239d428..db04af70a54 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -4,11 +4,11 @@ project_delete (delete "/organizations/{organization_n disk_delete (delete "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}") instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") instance_network_interface_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}") +snapshot_delete (delete "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}") vpc_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") vpc_router_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}") vpc_router_route_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}") vpc_subnet_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") -snapshot_delete (delete "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}") disk_view_by_id (get "/by-id/disks/{id}") instance_view_by_id (get "/by-id/instances/{id}") instance_network_interface_view_by_id (get "/by-id/network-interfaces/{id}") @@ -33,6 +33,14 @@ instance_serial_console_stream (get "/organizations/{organization_n project_policy_view (get "/organizations/{organization_name}/projects/{project_name}/policy") snapshot_list (get "/organizations/{organization_name}/projects/{project_name}/snapshots") snapshot_view (get "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}") +vpc_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs") +vpc_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}") +vpc_router_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers") +vpc_router_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}") +vpc_router_route_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes") +vpc_router_route_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}") +vpc_subnet_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets") +vpc_subnet_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") device_auth_request (post "/device/auth") device_auth_confirm (post "/device/confirm") device_access_token (post "/device/token") @@ -51,11 +59,11 @@ instance_network_interface_create (post "/organizations/{organization_n instance_reboot (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/reboot") instance_start (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start") instance_stop (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop") +snapshot_create (post "/organizations/{organization_name}/projects/{project_name}/snapshots") vpc_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs") vpc_router_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers") vpc_router_route_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes") vpc_subnet_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets") -snapshot_create (post "/organizations/{organization_name}/projects/{project_name}/snapshots") organization_update (put "/organizations/{organization_name}") organization_policy_update (put "/organizations/{organization_name}/policy") project_update (put "/organizations/{organization_name}/projects/{project_name}") diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 3ec4f90d957..c4f2f467666 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -154,27 +154,6 @@ impl SnapshotSelector { } } -#[derive(Deserialize, JsonSchema)] -pub struct SnapshotSelector { - #[serde(flatten)] - pub project_selector: Option, - pub snapshot: NameOrId, -} - -impl SnapshotSelector { - pub fn new( - organization: Option, - project: Option, - snapshot: NameOrId, - ) -> Self { - SnapshotSelector { - project_selector: project - .map(|p| ProjectSelector::new(organization, p)), - snapshot, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct InstanceSelector { #[serde(flatten)] From c8323466981c0b413ff94ab332dab2cdff2b7dbc Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 15 Feb 2023 14:06:31 -0500 Subject: [PATCH 41/45] PR feedback --- nexus/src/app/vpc_router.rs | 4 ++-- nexus/types/src/external_api/params.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nexus/src/app/vpc_router.rs b/nexus/src/app/vpc_router.rs index f6d14fe2fa4..f6a8f36d320 100644 --- a/nexus/src/app/vpc_router.rs +++ b/nexus/src/app/vpc_router.rs @@ -55,10 +55,10 @@ impl super::Nexus { router: NameOrId::Id(_), vpc_selector: Some(_), } => Err(Error::invalid_request( - "when providing subnet as an ID, vpc should not be specified", + "when providing vpc_router as an ID, vpc should not be specified", )), _ => Err(Error::invalid_request( - "router should either be an ID or vpc should be specified", + "vpc_router should either be an ID or vpc should be specified", )), } } diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index c4f2f467666..fb3aafe6b9a 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -189,6 +189,7 @@ pub struct NetworkInterfaceSelector { pub network_interface: NameOrId, } +// TODO-v1: delete this post migration impl NetworkInterfaceSelector { pub fn new( organization: Option, From 388dcd8bfbc95c8552aa81253208c1b7958618c6 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 15 Feb 2023 14:07:40 -0500 Subject: [PATCH 42/45] Add missed docstring --- nexus/src/external_api/http_entrypoints.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index af9a639666b..69a3ebf619e 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -4454,6 +4454,7 @@ async fn instance_network_interface_view_v1( } /// Fetch a network interface +/// Use `/v1/network-interfaces/{interface}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}", From fd060337fa39abba9b05b555fc37803cbd9f1d9c Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 15 Feb 2023 14:08:42 -0500 Subject: [PATCH 43/45] Add missing deprecation --- nexus/src/external_api/http_entrypoints.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 69a3ebf619e..3c3ee60d3da 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -4454,7 +4454,7 @@ async fn instance_network_interface_view_v1( } /// Fetch a network interface -/// Use `/v1/network-interfaces/{interface}` instead +/// Use `GET /v1/network-interfaces/{interface}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}", @@ -4551,10 +4551,12 @@ async fn instance_network_interface_update_v1( } /// Update a network interface +/// Use `PUT /v1/network-interfaces/{interface}` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}", tags = ["instances"], + deprecated = true }] async fn instance_network_interface_update( rqctx: RequestContext>, From 735255d34d5bec2c74c5729f34c681e318d4fbe8 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 15 Feb 2023 14:10:53 -0500 Subject: [PATCH 44/45] Add other missing deprecations --- nexus/src/external_api/http_entrypoints.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 3c3ee60d3da..f1d14099b5f 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -6148,10 +6148,12 @@ async fn vpc_router_route_list_v1( /// List routes /// /// List the routes associated with a router in a particular VPC. +/// Use `GET /v1/vpc-router-routes` instead. #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes", tags = ["vpcs"], + deprecated = true }] async fn vpc_router_route_list( rqctx: RequestContext>, @@ -6237,6 +6239,7 @@ struct RouterRoutePathParam { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}", tags = ["vpcs"], + deprecated = true }] async fn vpc_router_route_view( rqctx: RequestContext>, @@ -6264,10 +6267,12 @@ async fn vpc_router_route_view( } /// Fetch a route by id +/// Use `GET /v1/vpc-router-routes/{route}` instead #[endpoint { method = GET, path = "/by-id/vpc-router-routes/{id}", - tags = ["vpcs"] + tags = ["vpcs"], + deprecated = true }] async fn vpc_router_route_view_by_id( rqctx: RequestContext>, @@ -6323,10 +6328,12 @@ async fn vpc_router_route_create_v1( } /// Create a router +/// Use `POST /v1/vpc-router-routes` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes", tags = ["vpcs"], + deprecated = true }] async fn vpc_router_route_create( rqctx: RequestContext>, From 0ec1eeb23cf4dfdb607afef9a93442a8241fd634 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 15 Feb 2023 14:13:18 -0500 Subject: [PATCH 45/45] Update spec --- openapi/nexus.json | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/openapi/nexus.json b/openapi/nexus.json index e883af9a763..3b5cbe8bbc8 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -290,6 +290,7 @@ "vpcs" ], "summary": "Fetch a route by id", + "description": "Use `GET /v1/vpc-router-routes/{route}` instead", "operationId": "vpc_router_route_view_by_id", "parameters": [ { @@ -319,7 +320,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/by-id/vpc-routers/{id}": { @@ -2634,6 +2636,7 @@ "instances" ], "summary": "Fetch a network interface", + "description": "Use `GET /v1/network-interfaces/{interface}` instead", "operationId": "instance_network_interface_view", "parameters": [ { @@ -2694,6 +2697,7 @@ "instances" ], "summary": "Update a network interface", + "description": "Use `PUT /v1/network-interfaces/{interface}` instead", "operationId": "instance_network_interface_update", "parameters": [ { @@ -2756,7 +2760,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ @@ -4203,7 +4208,7 @@ "vpcs" ], "summary": "List routes", - "description": "List the routes associated with a router in a particular VPC.", + "description": "List the routes associated with a router in a particular VPC. Use `GET /v1/vpc-router-routes` instead.", "operationId": "vpc_router_route_list", "parameters": [ { @@ -4284,6 +4289,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -4291,6 +4297,7 @@ "vpcs" ], "summary": "Create a router", + "description": "Use `POST /v1/vpc-router-routes` instead", "operationId": "vpc_router_route_create", "parameters": [ { @@ -4353,7 +4360,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}": { @@ -4423,7 +4431,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [