From 8d51dbb8a05ea6e45d86bed5409b2423efc9100a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 13 Dec 2022 10:08:34 -0500 Subject: [PATCH 01/13] 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 9660353033..11d3d6574c 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 ec4f0f574a..bab5e77cbd 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 d7783d2a7a..a1fae4997d 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 07c4c73cee..0166bde2a6 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 50b3435f5c..6d34217771 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 dd57a3935d..bf47b856ed 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/13] 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 0166bde2a6..5051bfb62c 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 121e73d05e..c4544d323c 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 6d34217771..db945cfae8 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 3828fb5bfb..5d6714c6b1 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 e6a4b225d1..0981635a1a 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/13] 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 db945cfae8..e14a557e92 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/13] 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 7ae7779449..6d988c6139 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 11d3d6574c..74ff715c8d 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 e14a557e92..9191651e14 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/13] 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 bab5e77cbd..5f30d3dc54 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 a1fae4997d..5c0d7cfdb0 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 5051bfb62c..0a20c5cc1c 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 9191651e14..aaaeed9094 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 da344d4065..ddc8e7374a 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 594c957364..2538954571 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 bf47b856ed..121fc291de 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/13] Update nexus.json --- openapi/nexus.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openapi/nexus.json b/openapi/nexus.json index 0981635a1a..df5124b796 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/13] 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 6051eeaf7a..d2dfe67a28 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 7f6a6c300f..a51326747d 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 64ce7eabc8..856dafe394 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 70576c0b21..1f6005f688 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/13] 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 aaaeed9094..1d00073fab 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/13] 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 121fc291de..470371d9ee 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 df5124b796..2c66ba8559 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/13] 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 470371d9ee..6ab7a07424 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/13] 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 5f30d3dc54..a08c0e1162 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 0a20c5cc1c..7ba52f34c3 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 11762116f12640bf172b9f652667f6f0cbb3f386 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 16 Dec 2022 13:16:54 -0500 Subject: [PATCH 12/13] 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 dfa8a2e40c..d1f5014452 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 3864a25b1c8073985a47866aea799acb4b0c7cc0 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 19 Dec 2022 19:26:07 -0500 Subject: [PATCH 13/13] Remove duplicates? --- nexus/src/external_api/http_entrypoints.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 4c93cbf654..b66f980d36 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -122,14 +122,6 @@ pub fn external_api() -> NexusApiDescription { api.register(project_policy_view_v1)?; api.register(project_policy_update_v1)?; - 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)?; - // Operator-Accessible IP Pools API api.register(ip_pool_list)?; api.register(ip_pool_create)?; @@ -167,6 +159,7 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_stop)?; api.register(instance_serial_console)?; api.register(instance_serial_console_stream)?; + api.register(instance_list_v1)?; api.register(instance_view_v1)?; api.register(instance_create_v1)?;