From 233dc6dafa30e811c1cfa4797473a9c7a1de6436 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 17 Nov 2022 15:33:15 -0500 Subject: [PATCH 01/72] Test impl of new endpoint format --- common/src/api/external/mod.rs | 41 ++++++++++++++ nexus/src/external_api/http_entrypoints.rs | 62 +++++++++++++++++++++ nexus/tests/output/nexus_tags.txt | 1 + openapi/nexus.json | 64 ++++++++++++++++++++++ 4 files changed, 168 insertions(+) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 3d8ee960b29..e89e82e2225 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -268,6 +268,47 @@ impl Name { } } +#[derive(Serialize, Deserialize)] +#[serde(try_from = "String")] +pub enum ResourceIdentifier { + Name(Name), + Id(Uuid), +} + +impl TryFrom for ResourceIdentifier { + type Error = String; + + fn try_from(value: String) -> Result { + if let Ok(id) = Uuid::parse_str(&value) { + Ok(ResourceIdentifier::Id(id)) + } else { + Ok(ResourceIdentifier::Name(Name::try_from(value)?)) + } + } +} + +impl JsonSchema for ResourceIdentifier { + fn schema_name() -> String { + "ResourceIdentifier".to_string() + } + + fn json_schema( + gen: &mut schemars::gen::SchemaGenerator, + ) -> schemars::schema::Schema { + schemars::schema::SchemaObject { + subschemas: Some(Box::new(schemars::schema::SubschemaValidation { + one_of: Some(vec![ + gen.subschema_for::(), + gen.subschema_for::(), + ]), + ..Default::default() + })), + ..Default::default() + } + .into() + } +} + /// Name for a built-in role #[derive( Clone, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index ee3a19ea565..4746e7827cd 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -61,6 +61,7 @@ use omicron_common::api::external::Error; use omicron_common::api::external::Instance; use omicron_common::api::external::InternalContext; use omicron_common::api::external::NetworkInterface; +use omicron_common::api::external::ResourceIdentifier; use omicron_common::api::external::RouterRoute; use omicron_common::api::external::RouterRouteCreateParams; use omicron_common::api::external::RouterRouteKind; @@ -137,6 +138,7 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_list)?; api.register(instance_create)?; api.register(instance_view)?; + api.register(instance_lookup)?; api.register(instance_view_by_id)?; api.register(instance_delete)?; api.register(instance_migrate)?; @@ -2078,6 +2080,66 @@ async fn instance_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Path parameters for Instance requests. Name is temporary. +#[derive(Deserialize, JsonSchema)] +struct InstanceLookupPathParam { + instance: ResourceIdentifier, +} + +#[derive(Deserialize, JsonSchema)] +struct InstanceLookupQueryParam { + organization_name: Option, + project_name: Option, +} + +#[endpoint { + method = GET, + path = "/instances/{instance}", + tags = ["instances"], +}] +async fn instance_lookup( + rqctx: Arc>>, + 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_ident = &path.instance; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance = match instance_ident { + ResourceIdentifier::Name(name) => { + let organization_name = + query.organization_name.ok_or_else(|| { + Error::InvalidRequest { message: "organization_name is required when using instance name".to_string() } + })?; + let project_name = query.project_name.ok_or_else(|| { + Error::InvalidRequest { + message: + "project_name is required when using instance name" + .to_string(), + } + })?; + nexus + .instance_fetch( + &opctx, + &organization_name, + &project_name, + &Name(name.clone()), + ) + .await? + } + ResourceIdentifier::Id(id) => { + nexus.instance_fetch_by_id(&opctx, id).await? + } + }; + Ok(HttpResponseOk(instance.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Instance requests #[derive(Deserialize, JsonSchema)] struct InstancePathParam { diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 86fc5e6b46f..4f6436ddb08 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -34,6 +34,7 @@ instance_disk_detach /organizations/{organization_name}/proj instance_disk_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks instance_external_ip_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/external-ips instance_list /organizations/{organization_name}/projects/{project_name}/instances +instance_lookup /instances/{instance} instance_migrate /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/migrate instance_network_interface_create /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces instance_network_interface_delete /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name} diff --git a/openapi/nexus.json b/openapi/nexus.json index ab1eb0e0b06..68ecd327437 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -590,6 +590,59 @@ "x-dropshot-pagination": true } }, + "/instances/{instance}": { + "get": { + "tags": [ + "instances" + ], + "operationId": "instance_lookup", + "parameters": [ + { + "in": "query", + "name": "organization_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/ResourceIdentifier" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/login": { "post": { "tags": [ @@ -12775,6 +12828,17 @@ } ] }, + "ResourceIdentifier": { + "oneOf": [ + { + "$ref": "#/components/schemas/Name" + }, + { + "type": "string", + "format": "uuid" + } + ] + }, "NameOrIdSortMode": { "description": "Supported set of sort modes for scanning by name or id", "oneOf": [ From 0be1778be7e52cc540bfcdb2effc6bacd60e894b Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 17 Nov 2022 15:54:59 -0500 Subject: [PATCH 02/72] ResourceIdentifier -> NameOrId --- common/src/api/external/mod.rs | 10 ++++----- nexus/src/external_api/http_entrypoints.rs | 25 ++++++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index e89e82e2225..fd708c717ad 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -270,24 +270,24 @@ impl Name { #[derive(Serialize, Deserialize)] #[serde(try_from = "String")] -pub enum ResourceIdentifier { +pub enum NameOrId { Name(Name), Id(Uuid), } -impl TryFrom for ResourceIdentifier { +impl TryFrom for NameOrId { type Error = String; fn try_from(value: String) -> Result { if let Ok(id) = Uuid::parse_str(&value) { - Ok(ResourceIdentifier::Id(id)) + Ok(NameOrId::Id(id)) } else { - Ok(ResourceIdentifier::Name(Name::try_from(value)?)) + Ok(NameOrId::Name(Name::try_from(value)?)) } } } -impl JsonSchema for ResourceIdentifier { +impl JsonSchema for NameOrId { fn schema_name() -> String { "ResourceIdentifier".to_string() } diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 4746e7827cd..ff75859b3d3 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -60,8 +60,8 @@ 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::ResourceIdentifier; use omicron_common::api::external::RouterRoute; use omicron_common::api::external::RouterRouteCreateParams; use omicron_common::api::external::RouterRouteKind; @@ -2083,7 +2083,7 @@ async fn instance_create( /// Path parameters for Instance requests. Name is temporary. #[derive(Deserialize, JsonSchema)] struct InstanceLookupPathParam { - instance: ResourceIdentifier, + instance: NameOrId, } #[derive(Deserialize, JsonSchema)] @@ -2107,10 +2107,25 @@ async fn instance_lookup( let path = path_params.into_inner(); let query = query_params.into_inner(); let instance_ident = &path.instance; + let instance_id = match &path.instance { + NameOrId::Id(id) => {} + NameOrId::Name(name) => { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance = nexus + .instance_lookup( + &opctx, + &query.organization_name, + &query.project_name, + &name, + ) + .await?; + instance.id() + } + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance = match instance_ident { - ResourceIdentifier::Name(name) => { + NameOrId::Name(name) => { let organization_name = query.organization_name.ok_or_else(|| { Error::InvalidRequest { message: "organization_name is required when using instance name".to_string() } @@ -2131,9 +2146,7 @@ async fn instance_lookup( ) .await? } - ResourceIdentifier::Id(id) => { - nexus.instance_fetch_by_id(&opctx, id).await? - } + NameOrId::Id(id) => nexus.instance_fetch_by_id(&opctx, id).await?, }; Ok(HttpResponseOk(instance.into())) }; From 6e3210f4df2fdda09e12eb7a463eb6d3799b62d7 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 17 Nov 2022 16:08:08 -0500 Subject: [PATCH 03/72] Add docs, remove unneeded chunk --- nexus/src/external_api/http_entrypoints.rs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index ff75859b3d3..2fd9962ef1d 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2083,12 +2083,16 @@ async fn instance_create( /// Path parameters for Instance requests. Name is temporary. #[derive(Deserialize, JsonSchema)] struct InstanceLookupPathParam { + /// If Name is provided `organization_name` and `project_name` query parameters must also be present. + /// Otherwise they should be omitted. instance: NameOrId, } #[derive(Deserialize, JsonSchema)] struct InstanceLookupQueryParam { + /// Should only be specified if `instance` path param is a name organization_name: Option, + /// Should only be specified if `instance` path param is a name project_name: Option, } @@ -2107,21 +2111,6 @@ async fn instance_lookup( let path = path_params.into_inner(); let query = query_params.into_inner(); let instance_ident = &path.instance; - let instance_id = match &path.instance { - NameOrId::Id(id) => {} - NameOrId::Name(name) => { - let opctx = OpContext::for_external_api(&rqctx).await?; - let instance = nexus - .instance_lookup( - &opctx, - &query.organization_name, - &query.project_name, - &name, - ) - .await?; - instance.id() - } - }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance = match instance_ident { From a2681b9d9bcffcaddfe56475f10637bdcba06feb Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 17 Nov 2022 17:07:09 -0500 Subject: [PATCH 04/72] Use v1 prefix, convert instance delete --- nexus/src/app/instance.rs | 31 +++++++---- nexus/src/external_api/http_entrypoints.rs | 65 +++++++++++++++++++--- 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index be8d83e1dac..057a9906bf9 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -53,6 +53,22 @@ use uuid::Uuid; const MAX_KEYS_PER_INSTANCE: u32 = 8; impl super::Nexus { + pub async fn instance_lookup_id( + &self, + opctx: &OpContext, + organization_name: &Name, + project_name: &Name, + instance_name: &Name, + ) -> LookupResult { + let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) + .organization_name(organization_name) + .project_name(project_name) + .instance_name(instance_name) + .lookup_for(authz::Action::Read) + .await?; + return Ok(authz_instance.id()); + } + pub async fn project_create_instance( self: &Arc, opctx: &OpContext, @@ -243,20 +259,15 @@ impl super::Nexus { pub async fn project_destroy_instance( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + instance_id: &Uuid, ) -> DeleteResult { // TODO-robustness We need to figure out what to do with Destroyed // instances? Presumably we need to clean them up at some point, but // not right away so that callers can see that they've been destroyed. - let (.., authz_instance, _) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) - .fetch() - .await?; + let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) + .instance_id(*instance_id) + .lookup_for(authz::Action::Delete) + .await?; self.db_datastore .project_delete_instance(opctx, &authz_instance) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 2fd9962ef1d..e8af77a592d 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -138,7 +138,7 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_list)?; api.register(instance_create)?; api.register(instance_view)?; - api.register(instance_lookup)?; + api.register(instance_view_v1)?; api.register(instance_view_by_id)?; api.register(instance_delete)?; api.register(instance_migrate)?; @@ -2098,10 +2098,10 @@ struct InstanceLookupQueryParam { #[endpoint { method = GET, - path = "/instances/{instance}", + path = "/v1/instances/{instance}", tags = ["instances"], }] -async fn instance_lookup( +async fn instance_view_v1( rqctx: Arc>>, query_params: Query, path_params: Path, @@ -2113,7 +2113,7 @@ async fn instance_lookup( let instance_ident = &path.instance; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance = match instance_ident { + let instance_id = match instance_ident { NameOrId::Name(name) => { let organization_name = query.organization_name.ok_or_else(|| { @@ -2127,7 +2127,7 @@ async fn instance_lookup( } })?; nexus - .instance_fetch( + .instance_lookup_id( &opctx, &organization_name, &project_name, @@ -2135,8 +2135,9 @@ async fn instance_lookup( ) .await? } - NameOrId::Id(id) => nexus.instance_fetch_by_id(&opctx, id).await?, + NameOrId::Id(id) => *id, }; + let instance = nexus.instance_fetch_by_id(&opctx, &instance_id).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2203,6 +2204,53 @@ async fn instance_view_by_id( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = DELETE, + path = "/v1/instances/{instance}", + tags = ["instances"], +}] +async fn instance_delete_v1( + rqctx: Arc>>, + 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_ident = &path.instance; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_id = match instance_ident { + NameOrId::Name(name) => { + let organization_name = + query.organization_name.ok_or_else(|| { + Error::InvalidRequest { message: "organization_name is required when using instance name".to_string() } + })?; + let project_name = query.project_name.ok_or_else(|| { + Error::InvalidRequest { + message: + "project_name is required when using instance name" + .to_string(), + } + })?; + nexus + .instance_lookup_id( + &opctx, + &organization_name, + &project_name, + &Name(name.clone()), + ) + .await? + } + NameOrId::Id(id) => *id, + }; + nexus.project_destroy_instance(&opctx, &instance_id).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete an instance #[endpoint { method = DELETE, @@ -2221,14 +2269,15 @@ async fn instance_delete( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus - .project_destroy_instance( + let instance_id = nexus + .instance_lookup_id( &opctx, &organization_name, &project_name, &instance_name, ) .await?; + nexus.project_destroy_instance(&opctx, &instance_id).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await From 18bc9cf9980432df97ffcdb423e86730f7beae0c Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 17 Nov 2022 17:58:41 -0500 Subject: [PATCH 05/72] Push up diplay error --- common/src/api/external/mod.rs | 6 +++--- nexus/src/external_api/http_entrypoints.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index fd708c717ad..93ccab8ab61 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -268,8 +268,8 @@ impl Name { } } -#[derive(Serialize, Deserialize)] -#[serde(try_from = "String")] +#[derive(Serialize, Deserialize, Display, Debug)] +#[display("{0}")] pub enum NameOrId { Name(Name), Id(Uuid), @@ -289,7 +289,7 @@ impl TryFrom for NameOrId { impl JsonSchema for NameOrId { fn schema_name() -> String { - "ResourceIdentifier".to_string() + "NameOrId".to_string() } fn json_schema( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index e8af77a592d..d5c2435f175 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -141,6 +141,7 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_view_v1)?; api.register(instance_view_by_id)?; api.register(instance_delete)?; + api.register(instance_delete_v1)?; api.register(instance_migrate)?; api.register(instance_reboot)?; api.register(instance_start)?; From 07ec450199d759b1bf7219f1ad493c88cbaa46db Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 17 Nov 2022 18:03:11 -0500 Subject: [PATCH 06/72] ReferenceIdentifer -> NameOrId in nexus.json --- openapi/nexus.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi/nexus.json b/openapi/nexus.json index 68ecd327437..6e73e910be1 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -618,7 +618,7 @@ "name": "instance", "required": true, "schema": { - "$ref": "#/components/schemas/ResourceIdentifier" + "$ref": "#/components/schemas/NameOrId" }, "style": "simple" } @@ -12828,7 +12828,7 @@ } ] }, - "ResourceIdentifier": { + "NameOrId": { "oneOf": [ { "$ref": "#/components/schemas/Name" From 5b2f2177d2ac94ea3f57630ed322e963a31a0ff8 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 17 Nov 2022 18:19:40 -0500 Subject: [PATCH 07/72] Update nexus.json to fix build issues --- common/src/api/external/mod.rs | 5 +- nexus/tests/output/nexus_tags.txt | 3 +- openapi/nexus.json | 178 +++++++++++++++++++----------- 3 files changed, 119 insertions(+), 67 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 93ccab8ab61..a490cba1eae 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -268,8 +268,9 @@ impl Name { } } -#[derive(Serialize, Deserialize, Display, Debug)] -#[display("{0}")] +#[derive(Serialize, Deserialize)] +#[serde(try_from = "String")] + pub enum NameOrId { Name(Name), Id(Uuid), diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 4f6436ddb08..ba32c977480 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -29,12 +29,12 @@ API operations found with tag "instances" OPERATION ID URL PATH instance_create /organizations/{organization_name}/projects/{project_name}/instances instance_delete /organizations/{organization_name}/projects/{project_name}/instances/{instance_name} +instance_delete_v1 /v1/instances/{instance} instance_disk_attach /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach instance_disk_detach /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach instance_disk_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks instance_external_ip_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/external-ips instance_list /organizations/{organization_name}/projects/{project_name}/instances -instance_lookup /instances/{instance} instance_migrate /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/migrate instance_network_interface_create /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces instance_network_interface_delete /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name} @@ -49,6 +49,7 @@ instance_start /organizations/{organization_name}/proj instance_stop /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop instance_view /organizations/{organization_name}/projects/{project_name}/instances/{instance_name} instance_view_by_id /by-id/instances/{id} +instance_view_v1 /v1/instances/{instance} API operations found with tag "login" OPERATION ID URL PATH diff --git a/openapi/nexus.json b/openapi/nexus.json index 6e73e910be1..2caceb03fff 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -590,59 +590,6 @@ "x-dropshot-pagination": true } }, - "/instances/{instance}": { - "get": { - "tags": [ - "instances" - ], - "operationId": "instance_lookup", - "parameters": [ - { - "in": "query", - "name": "organization_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" - }, - { - "in": "path", - "name": "instance", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Instance" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, "/login": { "post": { "tags": [ @@ -7706,6 +7653,109 @@ }, "x-dropshot-pagination": true } + }, + "/v1/instances/{instance}": { + "get": { + "tags": [ + "instances" + ], + "operationId": "instance_view_v1", + "parameters": [ + { + "in": "query", + "name": "organization_name", + "description": "Should only be specified if `instance` path param is a name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "description": "Should only be specified if `instance` path param is a name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "path", + "name": "instance", + "description": "If Name is provided `organization_name` and `project_name` query parameters must also be present. Otherwise they should be omitted.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "instances" + ], + "operationId": "instance_delete_v1", + "parameters": [ + { + "in": "query", + "name": "organization_name", + "description": "Should only be specified if `instance` path param is a name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "description": "Should only be specified if `instance` path param is a name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "path", + "name": "instance", + "description": "If Name is provided `organization_name` and `project_name` query parameters must also be present. Otherwise they should be omitted.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { @@ -12828,17 +12878,6 @@ } ] }, - "NameOrId": { - "oneOf": [ - { - "$ref": "#/components/schemas/Name" - }, - { - "type": "string", - "format": "uuid" - } - ] - }, "NameOrIdSortMode": { "description": "Supported set of sort modes for scanning by name or id", "oneOf": [ @@ -12887,6 +12926,17 @@ "write", "write_bytes" ] + }, + "NameOrId": { + "oneOf": [ + { + "$ref": "#/components/schemas/Name" + }, + { + "type": "string", + "format": "uuid" + } + ] } } }, From 8631e9f3f3e248f9b42ee282db37d30b73e5e120 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 17 Nov 2022 18:23:10 -0500 Subject: [PATCH 08/72] Change v1 to a prefix instead of postfix --- nexus/src/external_api/http_entrypoints.rs | 8 ++++---- nexus/tests/output/nexus_tags.txt | 4 ++-- openapi/nexus.json | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index d5c2435f175..ee57230a57a 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -138,16 +138,16 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_list)?; api.register(instance_create)?; api.register(instance_view)?; - api.register(instance_view_v1)?; api.register(instance_view_by_id)?; api.register(instance_delete)?; - api.register(instance_delete_v1)?; api.register(instance_migrate)?; api.register(instance_reboot)?; api.register(instance_start)?; api.register(instance_stop)?; api.register(instance_serial_console)?; api.register(instance_serial_console_stream)?; + api.register(v1_instance_view)?; + api.register(v1_instance_delete)?; // Project-scoped images API api.register(image_list)?; @@ -2102,7 +2102,7 @@ struct InstanceLookupQueryParam { path = "/v1/instances/{instance}", tags = ["instances"], }] -async fn instance_view_v1( +async fn v1_instance_view( rqctx: Arc>>, query_params: Query, path_params: Path, @@ -2210,7 +2210,7 @@ async fn instance_view_by_id( path = "/v1/instances/{instance}", tags = ["instances"], }] -async fn instance_delete_v1( +async fn v1_instance_delete( rqctx: Arc>>, query_params: Query, path_params: Path, diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index ba32c977480..94a463a26fe 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -29,7 +29,6 @@ API operations found with tag "instances" OPERATION ID URL PATH instance_create /organizations/{organization_name}/projects/{project_name}/instances instance_delete /organizations/{organization_name}/projects/{project_name}/instances/{instance_name} -instance_delete_v1 /v1/instances/{instance} instance_disk_attach /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach instance_disk_detach /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach instance_disk_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks @@ -49,7 +48,8 @@ instance_start /organizations/{organization_name}/proj instance_stop /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop instance_view /organizations/{organization_name}/projects/{project_name}/instances/{instance_name} instance_view_by_id /by-id/instances/{id} -instance_view_v1 /v1/instances/{instance} +v1_instance_delete /v1/instances/{instance} +v1_instance_view /v1/instances/{instance} API operations found with tag "login" OPERATION ID URL PATH diff --git a/openapi/nexus.json b/openapi/nexus.json index 2caceb03fff..9820214853c 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7659,7 +7659,7 @@ "tags": [ "instances" ], - "operationId": "instance_view_v1", + "operationId": "v1_instance_view", "parameters": [ { "in": "query", @@ -7713,7 +7713,7 @@ "tags": [ "instances" ], - "operationId": "instance_delete_v1", + "operationId": "v1_instance_delete", "parameters": [ { "in": "query", From 41672aa710a5d429681eeef11ef3635116dd5e10 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 19 Nov 2022 12:42:55 -0500 Subject: [PATCH 09/72] Normalize id lookups and path selection --- nexus/db-model/src/name.rs | 10 +++ nexus/src/app/instance.rs | 58 ++++++++++--- nexus/src/external_api/http_entrypoints.rs | 98 ++++++++-------------- nexus/tests/integration_tests/instances.rs | 59 +++++++++++++ nexus/types/src/external_api/params.rs | 17 ++++ openapi/nexus.json | 60 +++++++++++-- 6 files changed, 221 insertions(+), 81 deletions(-) diff --git a/nexus/db-model/src/name.rs b/nexus/db-model/src/name.rs index 96603530333..10d0819b5fb 100644 --- a/nexus/db-model/src/name.rs +++ b/nexus/db-model/src/name.rs @@ -63,3 +63,13 @@ where String::from_sql(bytes)?.parse().map(Name).map_err(|e| e.into()) } } + +/// Newtype wrapper around [external::NameOrId]. This type isn't actually +/// stored in the database, but exists as a convenience for the API. +#[derive(JsonSchema, Serialize, Deserialize, RefCast)] +#[serde(transparent)] +#[repr(transparent)] +pub struct NameOrId(pub external::NameOrId); + +NewtypeFrom! { () pub struct NameOrId(external::NameOrId); } +NewtypeDeref! { () pub struct NameOrId(external::NameOrId); } diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 057a9906bf9..39681995fb4 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -32,6 +32,7 @@ use omicron_common::api::external::InstanceState; use omicron_common::api::external::InternalContext; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; use omicron_common::api::external::Vni; use omicron_common::api::internal::nexus; @@ -56,17 +57,54 @@ impl super::Nexus { pub async fn instance_lookup_id( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + instance: NameOrId, + project_selector: params::ProjectSelector, ) -> LookupResult { - let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) - .lookup_for(authz::Action::Read) - .await?; - return Ok(authz_instance.id()); + match instance { + NameOrId::Id(id) => Ok(id), + NameOrId::Name(name) => match project_selector { + params::ProjectSelector::ProjectId { project_id } => { + let (.., authz_instance) = + LookupPath::new(opctx, &self.db_datastore) + .project_id(project_id) + .instance_name(&Name(name.clone())) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_instance.id()) + } + params::ProjectSelector::ProjectAndOrgId { + project_name, + organization_id, + } => { + let (.., authz_instance) = + LookupPath::new(opctx, &self.db_datastore) + .organization_id(organization_id) + .project_name(&Name(project_name)) + .instance_name(&Name(name.clone())) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_instance.id()) + } + params::ProjectSelector::ProjectAndOrg { + project_name, + organization_name, + } => { + let (.., authz_instance) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(&Name(organization_name)) + .project_name(&Name(project_name)) + .instance_name(&Name(name.clone())) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_instance.id()) + } + params::ProjectSelector::None {} => { + Err(Error::InvalidRequest { + message: "When looking up an instance by name, the path to the instance must be provided via query paramaters".to_string() + }) + } + }, + } } pub async fn project_create_instance( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index ee57230a57a..41f60c074da 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -18,6 +18,7 @@ use crate::authz; use crate::context::OpContext; use crate::db; use crate::db::model::Name; +use crate::db::model::NameOrId; use crate::external_api::shared; use crate::ServerContext; use dropshot::ApiDescription; @@ -60,7 +61,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; @@ -2042,6 +2042,7 @@ async fn instance_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } + /// Create an instance // TODO-correctness This is supposed to be async. Is that right? We can create // the instance immediately -- it's just not booted yet. Maybe the boot @@ -2090,11 +2091,9 @@ struct InstanceLookupPathParam { } #[derive(Deserialize, JsonSchema)] -struct InstanceLookupQueryParam { - /// Should only be specified if `instance` path param is a name - organization_name: Option, - /// Should only be specified if `instance` path param is a name - project_name: Option, +struct InstanceQueryParams { + #[serde(flatten)] + selector: params::ProjectSelector, } #[endpoint { @@ -2104,40 +2103,18 @@ struct InstanceLookupQueryParam { }] async fn v1_instance_view( rqctx: Arc>>, - query_params: Query, + 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_ident = &path.instance; - let handler = async { - let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = match instance_ident { - NameOrId::Name(name) => { - let organization_name = - query.organization_name.ok_or_else(|| { - Error::InvalidRequest { message: "organization_name is required when using instance name".to_string() } - })?; - let project_name = query.project_name.ok_or_else(|| { - Error::InvalidRequest { - message: - "project_name is required when using instance name" - .to_string(), - } - })?; - nexus - .instance_lookup_id( - &opctx, - &organization_name, - &project_name, - &Name(name.clone()), - ) - .await? - } - NameOrId::Id(id) => *id, - }; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_id = nexus + .instance_lookup_id(&opctx, path.instance.into(), query.selector) + .await?; let instance = nexus.instance_fetch_by_id(&opctx, &instance_id).await?; Ok(HttpResponseOk(instance.into())) }; @@ -2212,40 +2189,18 @@ async fn instance_view_by_id( }] async fn v1_instance_delete( rqctx: Arc>>, - query_params: Query, + 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_ident = &path.instance; - let handler = async { - let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = match instance_ident { - NameOrId::Name(name) => { - let organization_name = - query.organization_name.ok_or_else(|| { - Error::InvalidRequest { message: "organization_name is required when using instance name".to_string() } - })?; - let project_name = query.project_name.ok_or_else(|| { - Error::InvalidRequest { - message: - "project_name is required when using instance name" - .to_string(), - } - })?; - nexus - .instance_lookup_id( - &opctx, - &organization_name, - &project_name, - &Name(name.clone()), - ) - .await? - } - NameOrId::Id(id) => *id, - }; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_id = nexus + .instance_lookup_id(&opctx, path.instance.into(), query.selector) + .await?; nexus.project_destroy_instance(&opctx, &instance_id).await?; Ok(HttpResponseDeleted()) }; @@ -2270,12 +2225,25 @@ async fn instance_delete( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + + // TODO: Clean this up let instance_id = nexus .instance_lookup_id( &opctx, - &organization_name, - &project_name, - &instance_name, + omicron_common::api::external::NameOrId::Name( + omicron_common::api::external::Name::from( + instance_name.clone(), + ), + ), + params::ProjectSelector::ProjectAndOrg { + project_name: omicron_common::api::external::Name::from( + project_name.clone(), + ), + organization_name: + omicron_common::api::external::Name::from( + organization_name.clone(), + ), + }, ) .await?; nexus.project_destroy_instance(&opctx, &instance_id).await?; diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 29bc92416d8..5dec931188b 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -123,6 +123,65 @@ async fn test_instances_access_before_create_returns_not_found( ); } +#[nexus_test] +async fn test_v1_instance_access(cptestctx: &ControlPlaneTestContext) { + let client = &cptestctx.external_client; + + create_ip_pool(&client, "p0", None, None).await; + let org = create_organization(&client, ORGANIZATION_NAME).await; + let project = create_project(client, ORGANIZATION_NAME, PROJECT_NAME).await; + + // Create an instance. + let instance_name = "test-instance"; + let instance = + create_instance(client, ORGANIZATION_NAME, PROJECT_NAME, instance_name) + .await; + + // Fetch instance by id + let fetched_instance = instance_get( + &client, + format!("/v1/instances/{}", instance.identity.id).as_str(), + ) + .await; + assert_eq!(fetched_instance.identity.id, instance.identity.id); + + // Fetch instance by name and project_id + let fetched_instance = instance_get( + &client, + format!( + "/v1/instances/{}?project_id={}", + instance.identity.name, project.identity.id + ) + .as_str(), + ) + .await; + assert_eq!(fetched_instance.identity.id, instance.identity.id); + + // Fetch instance by name, project_name, and organization_id + let fetched_instance = instance_get( + &client, + format!( + "/v1/instances/{}?project_name={}&organization_id={}", + instance.identity.name, project.identity.name, org.identity.id + ) + .as_str(), + ) + .await; + assert_eq!(fetched_instance.identity.id, instance.identity.id); + + // Fetch instance by name, project_name, and organization_name + let fetched_instance = instance_get( + &client, + format!( + "/v1/instances/{}?project_name={}&organization_name={}", + instance.identity.name, project.identity.name, org.identity.name + ) + .as_str(), + ) + .await; + assert_eq!(fetched_instance.identity.id, instance.identity.id); +} + #[nexus_test] async fn test_instances_create_reboot_halt( cptestctx: &ControlPlaneTestContext, diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 765219752b6..4eb2b4fd214 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -18,6 +18,23 @@ use serde::{ use std::{net::IpAddr, str::FromStr}; use uuid::Uuid; +#[derive(Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum ProjectSelector { + ProjectId { project_id: Uuid }, + ProjectAndOrgId { project_name: Name, organization_id: Uuid }, + ProjectAndOrg { project_name: Name, organization_name: Name }, + None {}, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ProjectQuery { + /// Should only be specified if `instance` path param is a name + pub organization_name: Option, + /// Should only be specified if `instance` path param is a name + pub project_name: Option, +} + // Silos /// Create-time parameters for a [`Silo`](crate::external_api::views::Silo) diff --git a/openapi/nexus.json b/openapi/nexus.json index 9820214853c..1cbe48363d0 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7661,10 +7661,35 @@ ], "operationId": "v1_instance_view", "parameters": [ + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, { "in": "query", "name": "organization_name", - "description": "Should only be specified if `instance` path param is a name", "schema": { "$ref": "#/components/schemas/Name" }, @@ -7673,7 +7698,6 @@ { "in": "query", "name": "project_name", - "description": "Should only be specified if `instance` path param is a name", "schema": { "$ref": "#/components/schemas/Name" }, @@ -7682,7 +7706,7 @@ { "in": "path", "name": "instance", - "description": "If Name is provided `organization_name` and `project_name` query parameters must also be present. Otherwise they should be omitted.", + "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" @@ -7715,10 +7739,35 @@ ], "operationId": "v1_instance_delete", "parameters": [ + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, { "in": "query", "name": "organization_name", - "description": "Should only be specified if `instance` path param is a name", "schema": { "$ref": "#/components/schemas/Name" }, @@ -7727,7 +7776,6 @@ { "in": "query", "name": "project_name", - "description": "Should only be specified if `instance` path param is a name", "schema": { "$ref": "#/components/schemas/Name" }, @@ -7736,7 +7784,7 @@ { "in": "path", "name": "instance", - "description": "If Name is provided `organization_name` and `project_name` query parameters must also be present. Otherwise they should be omitted.", + "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 954ffdb64f63be67f200b7b849b740f79c710404 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sun, 20 Nov 2022 18:36:01 -0500 Subject: [PATCH 10/72] Add v1 create instance api --- nexus/src/app/instance.rs | 110 +++++++++++---------- nexus/src/app/project.rs | 42 ++++++++ nexus/src/app/sagas/instance_create.rs | 4 - nexus/src/external_api/http_entrypoints.rs | 67 ++++++++++--- nexus/tests/output/nexus_tags.txt | 1 + nexus/types/src/external_api/params.rs | 62 ++++++++++-- openapi/nexus.json | 80 +++++++++++++++ 7 files changed, 288 insertions(+), 78 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 39681995fb4..03db6467fa6 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -32,7 +32,6 @@ use omicron_common::api::external::InstanceState; use omicron_common::api::external::InternalContext; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; -use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; use omicron_common::api::external::Vni; use omicron_common::api::internal::nexus; @@ -57,66 +56,73 @@ impl super::Nexus { pub async fn instance_lookup_id( &self, opctx: &OpContext, - instance: NameOrId, - project_selector: params::ProjectSelector, + instance_selector: params::InstanceSelector, ) -> LookupResult { - match instance { - NameOrId::Id(id) => Ok(id), - NameOrId::Name(name) => match project_selector { - params::ProjectSelector::ProjectId { project_id } => { - let (.., authz_instance) = - LookupPath::new(opctx, &self.db_datastore) - .project_id(project_id) - .instance_name(&Name(name.clone())) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_instance.id()) - } - params::ProjectSelector::ProjectAndOrgId { - project_name, - organization_id, - } => { - let (.., authz_instance) = - LookupPath::new(opctx, &self.db_datastore) - .organization_id(organization_id) - .project_name(&Name(project_name)) - .instance_name(&Name(name.clone())) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_instance.id()) - } - params::ProjectSelector::ProjectAndOrg { - project_name, - organization_name, - } => { - let (.., authz_instance) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(&Name(organization_name)) - .project_name(&Name(project_name)) - .instance_name(&Name(name.clone())) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_instance.id()) - } - params::ProjectSelector::None {} => { - Err(Error::InvalidRequest { - message: "When looking up an instance by name, the path to the instance must be provided via query paramaters".to_string() - }) - } - }, + match instance_selector { + params::InstanceSelector::InstanceId { instance_id } => { + Ok(instance_id) + } + params::InstanceSelector::InstanceAndProjectId { + instance_name, + project_id, + } => { + let (.., authz_instance) = + LookupPath::new(opctx, &self.db_datastore) + .project_id(project_id) + .instance_name(&Name(instance_name.clone())) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_instance.id()) + } + params::InstanceSelector::InstanceProjectAndOrgId { + instance_name, + project_name, + organization_id, + } => { + let (.., authz_instance) = + LookupPath::new(opctx, &self.db_datastore) + .organization_id(organization_id) + .project_name(&Name(project_name)) + .instance_name(&Name(instance_name.clone())) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_instance.id()) + } + params::InstanceSelector::InstanceProjectAndOrg { + instance_name, + project_name, + organization_name, + } => { + let (.., authz_instance) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(&Name(organization_name)) + .project_name(&Name(project_name)) + .instance_name(&Name(instance_name.clone())) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_instance.id()) + } + params::InstanceSelector::None {} => Err(Error::InvalidRequest { + message: " + Unable to resolve instance. Expected one of + - instance_id + - instance_name, project_id + - instance_name, project_name, organization_id + - instance_name, project_name, organization_name + " + .to_string(), + }), } } pub async fn project_create_instance( self: &Arc, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_id: &Uuid, params: ¶ms::InstanceCreate, ) -> CreateResult { let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) + .project_id(*project_id) .lookup_for(authz::Action::CreateChild) .await?; @@ -187,8 +193,6 @@ impl super::Nexus { let saga_params = sagas::instance_create::Params { serialized_authn: authn::saga::Serialized::for_opctx(opctx), - organization_name: organization_name.clone().into(), - project_name: project_name.clone().into(), project_id: authz_project.id(), create_params: params.clone(), }; diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index a76262ca219..c0eeb4c95dd 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -24,6 +24,48 @@ use omicron_common::api::external::UpdateResult; use uuid::Uuid; impl super::Nexus { + pub async fn project_lookup_id( + &self, + opctx: &OpContext, + project_selector: params::ProjectSelector, + ) -> LookupResult { + match project_selector { + params::ProjectSelector::ProjectId { project_id } => Ok(project_id), + params::ProjectSelector::ProjectAndOrgId { + project_name, + organization_id, + } => { + let (.., authz_project) = + LookupPath::new(opctx, &self.db_datastore) + .organization_id(organization_id) + .project_name(&Name(project_name)) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_project.id()) + } + params::ProjectSelector::ProjectAndOrg { + project_name, + organization_name, + } => { + let (.., authz_project) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(&Name(organization_name)) + .project_name(&Name(project_name)) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_project.id()) + } + params::ProjectSelector::None {} => Err(Error::InvalidRequest { + message: " + Unable to resolve project. Expected one of + - project_id + - project_name, organization_id + - project_name, organization_name + " + .to_string(), + }), + } + } pub async fn project_create( &self, opctx: &OpContext, diff --git a/nexus/src/app/sagas/instance_create.rs b/nexus/src/app/sagas/instance_create.rs index 8f6e79662a3..0a65eba18fb 100644 --- a/nexus/src/app/sagas/instance_create.rs +++ b/nexus/src/app/sagas/instance_create.rs @@ -46,8 +46,6 @@ use uuid::Uuid; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Params { pub serialized_authn: authn::saga::Serialized, - pub organization_name: Name, - pub project_name: Name, pub project_id: Uuid, pub create_params: params::InstanceCreate, } @@ -1024,8 +1022,6 @@ mod test { fn new_test_params(opctx: &OpContext, project_id: Uuid) -> Params { Params { serialized_authn: Serialized::for_opctx(opctx), - organization_name: ORG_NAME.parse().unwrap(), - project_name: PROJECT_NAME.parse().unwrap(), project_id, create_params: params::InstanceCreate { identity: IdentityMetadataCreateParams { diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 41f60c074da..e19930ac8ab 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -147,6 +147,7 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_serial_console)?; api.register(instance_serial_console_stream)?; api.register(v1_instance_view)?; + api.register(v1_instance_create)?; api.register(v1_instance_delete)?; // Project-scoped images API @@ -2042,6 +2043,31 @@ async fn instance_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = POST, + path = "/v1/instances", + tags = ["instances"], +}] +async fn v1_instance_create( + rqctx: Arc>>, + query_params: Query, + new_instance: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = 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_id = + nexus.project_lookup_id(&opctx, query.selector).await?; + let instance = nexus + .project_create_instance(&opctx, &project_id, &new_instance_params) + .await?; + Ok(HttpResponseCreated(instance.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} /// Create an instance // TODO-correctness This is supposed to be async. Is that right? We can create @@ -2069,24 +2095,32 @@ async fn instance_create( let new_instance_params = &new_instance.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance = nexus - .project_create_instance( + let project_id = nexus + .project_lookup_id( &opctx, - &organization_name, - &project_name, - &new_instance_params, + params::ProjectSelector::ProjectAndOrg { + project_name: project_name.clone().into(), + organization_name: organization_name.clone().into(), + }, ) .await?; + let instance = nexus + .project_create_instance(&opctx, &project_id, &new_instance_params) + .await?; Ok(HttpResponseCreated(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Path parameters for Instance requests. Name is temporary. +/// Path parameters for Instance requests #[derive(Deserialize, JsonSchema)] struct InstanceLookupPathParam { - /// If Name is provided `organization_name` and `project_name` query parameters must also be present. - /// Otherwise they should be omitted. + /// 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, } @@ -2113,7 +2147,10 @@ async fn v1_instance_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_id = nexus - .instance_lookup_id(&opctx, path.instance.into(), query.selector) + .instance_lookup_id( + &opctx, + query.selector.to_instance_selector(path.instance.into()), + ) .await?; let instance = nexus.instance_fetch_by_id(&opctx, &instance_id).await?; Ok(HttpResponseOk(instance.into())) @@ -2199,7 +2236,10 @@ async fn v1_instance_delete( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_id = nexus - .instance_lookup_id(&opctx, path.instance.into(), query.selector) + .instance_lookup_id( + &opctx, + query.selector.to_instance_selector(path.instance.into()), + ) .await?; nexus.project_destroy_instance(&opctx, &instance_id).await?; Ok(HttpResponseDeleted()) @@ -2226,16 +2266,13 @@ async fn instance_delete( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - // TODO: Clean this up let instance_id = nexus .instance_lookup_id( &opctx, - omicron_common::api::external::NameOrId::Name( - omicron_common::api::external::Name::from( + params::InstanceSelector::InstanceProjectAndOrg { + instance_name: omicron_common::api::external::Name::from( instance_name.clone(), ), - ), - params::ProjectSelector::ProjectAndOrg { project_name: omicron_common::api::external::Name::from( project_name.clone(), ), diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 94a463a26fe..683d4d3779c 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -48,6 +48,7 @@ instance_start /organizations/{organization_name}/proj instance_stop /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop instance_view /organizations/{organization_name}/projects/{project_name}/instances/{instance_name} instance_view_by_id /by-id/instances/{id} +v1_instance_create /v1/instances v1_instance_delete /v1/instances/{instance} v1_instance_view /v1/instances/{instance} diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 4eb2b4fd214..b6c4b232b7f 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -8,7 +8,7 @@ use crate::external_api::shared; use chrono::{DateTime, Utc}; use omicron_common::api::external::{ ByteCount, IdentityMetadataCreateParams, IdentityMetadataUpdateParams, - InstanceCpuCount, Ipv4Net, Ipv6Net, Name, + InstanceCpuCount, Ipv4Net, Ipv6Net, Name, NameOrId, }; use schemars::JsonSchema; use serde::{ @@ -27,12 +27,62 @@ pub enum ProjectSelector { None {}, } +impl ProjectSelector { + pub fn to_instance_selector(self, instance: NameOrId) -> InstanceSelector { + match instance { + NameOrId::Id(instance_id) => { + InstanceSelector::InstanceId { instance_id } + } + NameOrId::Name(instance_name) => match self { + ProjectSelector::ProjectId { project_id } => { + InstanceSelector::InstanceAndProjectId { + instance_name, + project_id, + } + } + ProjectSelector::ProjectAndOrgId { + project_name, + organization_id, + } => InstanceSelector::InstanceProjectAndOrgId { + instance_name, + project_name, + organization_id, + }, + ProjectSelector::ProjectAndOrg { + project_name, + organization_name, + } => InstanceSelector::InstanceProjectAndOrg { + instance_name, + project_name, + organization_name, + }, + ProjectSelector::None {} => InstanceSelector::None {}, + }, + } + } +} + #[derive(Deserialize, JsonSchema)] -pub struct ProjectQuery { - /// Should only be specified if `instance` path param is a name - pub organization_name: Option, - /// Should only be specified if `instance` path param is a name - pub project_name: Option, +#[serde(untagged)] +pub enum InstanceSelector { + InstanceId { + instance_id: Uuid, + }, + InstanceAndProjectId { + instance_name: Name, + project_id: Uuid, + }, + InstanceProjectAndOrgId { + instance_name: Name, + project_name: Name, + organization_id: Uuid, + }, + InstanceProjectAndOrg { + instance_name: Name, + project_name: Name, + organization_name: Name, + }, + None {}, } // Silos diff --git a/openapi/nexus.json b/openapi/nexus.json index 1cbe48363d0..dec4ca65bff 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7654,6 +7654,86 @@ "x-dropshot-pagination": true } }, + "/v1/instances": { + "post": { + "tags": [ + "instances" + ], + "operationId": "v1_instance_create", + "parameters": [ + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/instances/{instance}": { "get": { "tags": [ From c8d191bf346e9c032a611ea650d4e65244306e2d Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 21 Nov 2022 12:02:58 -0500 Subject: [PATCH 11/72] Add instance list endpoint --- nexus/src/app/instance.rs | 6 +- nexus/src/external_api/http_entrypoints.rs | 57 ++++++++++++- nexus/tests/output/nexus_tags.txt | 1 + openapi/nexus.json | 99 ++++++++++++++++++++++ 4 files changed, 157 insertions(+), 6 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 03db6467fa6..74d5f9e558d 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -253,13 +253,11 @@ impl super::Nexus { pub async fn project_list_instances( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_id: Uuid, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) + .project_id(project_id) .lookup_for(authz::Action::ListChildren) .await?; self.db_datastore diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index e19930ac8ab..807c6523cf0 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -146,6 +146,7 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_stop)?; api.register(instance_serial_console)?; api.register(instance_serial_console_stream)?; + api.register(v1_instance_list)?; api.register(v1_instance_view)?; api.register(v1_instance_create)?; api.register(v1_instance_delete)?; @@ -2003,6 +2004,50 @@ async fn disk_metrics_list( // Instances +#[derive(Deserialize, JsonSchema)] +struct InstanceListQueryParams { + #[serde(flatten)] + pagination: PaginatedByName, + #[serde(flatten)] + selector: params::ProjectSelector, +} + +#[endpoint { + method = GET, + path = "/v1/instances", + tags = ["instances"], +}] +async fn v1_instance_list( + rqctx: Arc>>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_id = + nexus.project_lookup_id(&opctx, query.selector).await?; + let instances = nexus + .project_list_instances( + &opctx, + project_id, + &data_page_params_for(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)), + ) + .await? + .into_iter() + .map(|i| i.into()) + .collect(); + Ok(HttpResponseOk(ScanByName::results_page( + &query.pagination, + instances, + &marker_for_name, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// List instances #[endpoint { method = GET, @@ -2022,11 +2067,19 @@ async fn instance_list( let project_name = &path.project_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let project_id = nexus + .project_lookup_id( + &opctx, + params::ProjectSelector::ProjectAndOrg { + project_name: project_name.clone().into(), + organization_name: organization_name.clone().into(), + }, + ) + .await?; let instances = nexus .project_list_instances( &opctx, - &organization_name, - &project_name, + project_id, &data_page_params_for(&rqctx, &query)? .map_name(|n| Name::ref_cast(n)), ) diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 683d4d3779c..fe9eb1c8898 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -50,6 +50,7 @@ instance_view /organizations/{organization_name}/proj instance_view_by_id /by-id/instances/{id} v1_instance_create /v1/instances v1_instance_delete /v1/instances/{instance} +v1_instance_list /v1/instances v1_instance_view /v1/instances/{instance} API operations found with tag "login" diff --git a/openapi/nexus.json b/openapi/nexus.json index dec4ca65bff..009e01513e2 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7655,6 +7655,105 @@ } }, "/v1/instances": { + "get": { + "tags": [ + "instances" + ], + "operationId": "v1_instance_list", + "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/NameSortMode" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, "post": { "tags": [ "instances" From 6708bacf2eb0a7416e98d822828ec78465d5c9df Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 22 Nov 2022 14:09:29 -0500 Subject: [PATCH 12/72] Add instance migrate endpoint --- nexus/src/app/instance.rs | 8 +- nexus/src/external_api/http_entrypoints.rs | 59 +++++++++++++- nexus/tests/output/nexus_tags.txt | 1 + openapi/nexus.json | 90 ++++++++++++++++++++++ 4 files changed, 149 insertions(+), 9 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 74d5f9e558d..6139be7ab57 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -325,15 +325,11 @@ impl super::Nexus { pub async fn project_instance_migrate( self: &Arc, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + instance_id: Uuid, params: params::InstanceMigrate, ) -> UpdateResult { let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) + .instance_id(instance_id) .lookup_for(authz::Action::Modify) .await?; diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 807c6523cf0..2f2f5c1188f 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -150,6 +150,7 @@ pub fn external_api() -> NexusApiDescription { api.register(v1_instance_view)?; api.register(v1_instance_create)?; api.register(v1_instance_delete)?; + api.register(v1_instance_migrate)?; // Project-scoped images API api.register(image_list)?; @@ -2342,6 +2343,43 @@ async fn instance_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +// TODO should this be in the public API? +#[endpoint { + method = POST, + path = "/v1/instances/{instance}/migrate", + tags = ["instances"], +}] +async fn v1_instance_migrate( + rqctx: Arc>>, + query_params: Query, + path_params: Path, + migrate_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let migrate_instance_params = migrate_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_id = nexus + .instance_lookup_id( + &opctx, + query.selector.to_instance_selector(path.instance.into()), + ) + .await?; + let instance = nexus + .project_instance_migrate( + &opctx, + instance_id, + migrate_instance_params, + ) + .await?; + Ok(HttpResponseOk(instance.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + // TODO should this be in the public API? /// Migrate an instance #[endpoint { @@ -2363,12 +2401,27 @@ async fn instance_migrate( let migrate_instance_params = migrate_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_id = nexus + .instance_lookup_id( + &opctx, + params::InstanceSelector::InstanceProjectAndOrg { + instance_name: omicron_common::api::external::Name::from( + instance_name.clone(), + ), + project_name: omicron_common::api::external::Name::from( + project_name.clone(), + ), + organization_name: + omicron_common::api::external::Name::from( + organization_name.clone(), + ), + }, + ) + .await?; let instance = nexus .project_instance_migrate( &opctx, - &organization_name, - &project_name, - &instance_name, + instance_id, migrate_instance_params, ) .await?; diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index fe9eb1c8898..362486f492e 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -51,6 +51,7 @@ instance_view_by_id /by-id/instances/{id} v1_instance_create /v1/instances v1_instance_delete /v1/instances/{instance} v1_instance_list /v1/instances +v1_instance_migrate /v1/instances/{instance}/migrate v1_instance_view /v1/instances/{instance} API operations found with tag "login" diff --git a/openapi/nexus.json b/openapi/nexus.json index 009e01513e2..2f8b3953ee8 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7983,6 +7983,96 @@ } } } + }, + "/v1/instances/{instance}/migrate": { + "post": { + "tags": [ + "instances" + ], + "operationId": "v1_instance_migrate", + "parameters": [ + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "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" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMigrate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { From 26b5651bcd87ec6a46858df3c58213d3759c4c80 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 22 Nov 2022 14:26:25 -0500 Subject: [PATCH 13/72] Add instance reboot endpoint --- nexus/src/app/instance.rs | 8 +-- nexus/src/external_api/http_entrypoints.rs | 49 +++++++++++-- nexus/tests/output/nexus_tags.txt | 1 + openapi/nexus.json | 80 ++++++++++++++++++++++ 4 files changed, 127 insertions(+), 11 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 6139be7ab57..4e7cb03075c 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -385,9 +385,7 @@ impl super::Nexus { pub async fn instance_reboot( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + instance_id: Uuid, ) -> UpdateResult { // To implement reboot, we issue a call to the sled agent to set a // runtime state of "reboot". We cannot simply stop the Instance and @@ -402,9 +400,7 @@ impl super::Nexus { // running. let (.., authz_instance, db_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) + .instance_id(instance_id) .fetch() .await?; let requested = InstanceRuntimeStateRequested { diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 2f2f5c1188f..e1d5000229c 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -151,6 +151,7 @@ pub fn external_api() -> NexusApiDescription { api.register(v1_instance_create)?; api.register(v1_instance_delete)?; api.register(v1_instance_migrate)?; + api.register(v1_instance_reboot)?; // Project-scoped images API api.register(image_list)?; @@ -2430,6 +2431,34 @@ async fn instance_migrate( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = POST, + path = "/v1/instances/{instance}/reboot", + tags = ["instances"], +}] +async fn v1_instance_reboot( + rqctx: Arc>>, + 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 handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_id = nexus + .instance_lookup_id( + &opctx, + query.selector.to_instance_selector(path.instance.into()), + ) + .await?; + let instance = nexus.instance_reboot(&opctx, instance_id).await?; + Ok(HttpResponseOk(instance.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Reboot an instance #[endpoint { method = POST, @@ -2448,14 +2477,24 @@ async fn instance_reboot( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance = nexus - .instance_reboot( + let instance_id = nexus + .instance_lookup_id( &opctx, - &organization_name, - &project_name, - &instance_name, + params::InstanceSelector::InstanceProjectAndOrg { + instance_name: omicron_common::api::external::Name::from( + instance_name.clone(), + ), + project_name: omicron_common::api::external::Name::from( + project_name.clone(), + ), + organization_name: + omicron_common::api::external::Name::from( + organization_name.clone(), + ), + }, ) .await?; + let instance = nexus.instance_reboot(&opctx, instance_id).await?; Ok(HttpResponseAccepted(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 362486f492e..0cd178d68de 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -52,6 +52,7 @@ v1_instance_create /v1/instances v1_instance_delete /v1/instances/{instance} v1_instance_list /v1/instances v1_instance_migrate /v1/instances/{instance}/migrate +v1_instance_reboot /v1/instances/{instance}/reboot v1_instance_view /v1/instances/{instance} API operations found with tag "login" diff --git a/openapi/nexus.json b/openapi/nexus.json index 2f8b3953ee8..3c748a805c5 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8073,6 +8073,86 @@ } } } + }, + "/v1/instances/{instance}/reboot": { + "post": { + "tags": [ + "instances" + ], + "operationId": "v1_instance_reboot", + "parameters": [ + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "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" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { From f70f84fc00b1fad2a684cbcb9dde46971824f1f8 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 22 Nov 2022 14:34:05 -0500 Subject: [PATCH 14/72] Add instance start endpoint --- nexus/src/app/instance.rs | 8 +-- nexus/src/external_api/http_entrypoints.rs | 51 ++++++++++++-- nexus/tests/output/nexus_tags.txt | 1 + openapi/nexus.json | 81 ++++++++++++++++++++++ 4 files changed, 129 insertions(+), 12 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 4e7cb03075c..daefe1f8f73 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -421,15 +421,11 @@ impl super::Nexus { pub async fn instance_start( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + instance_id: Uuid, ) -> UpdateResult { let (.., authz_instance, db_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) + .instance_id(instance_id) .fetch() .await?; let requested = InstanceRuntimeStateRequested { diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index e1d5000229c..d27a96f8533 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -152,6 +152,7 @@ pub fn external_api() -> NexusApiDescription { api.register(v1_instance_delete)?; api.register(v1_instance_migrate)?; api.register(v1_instance_reboot)?; + api.register(v1_instance_start)?; // Project-scoped images API api.register(image_list)?; @@ -2320,7 +2321,6 @@ async fn instance_delete( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus .instance_lookup_id( &opctx, @@ -2500,6 +2500,35 @@ async fn instance_reboot( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Boot an instance +#[endpoint { + method = POST, + path = "/v1/instances/{instance}/start", + tags = ["instances"], +}] +async fn v1_instance_start( + rqctx: Arc>>, + 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 handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_id = nexus + .instance_lookup_id( + &opctx, + query.selector.to_instance_selector(path.instance.into()), + ) + .await?; + let instance = nexus.instance_start(&opctx, instance_id).await?; + Ok(HttpResponseOk(instance.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Boot an instance #[endpoint { method = POST, @@ -2518,14 +2547,24 @@ async fn instance_start( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance = nexus - .instance_start( + let instance_id = nexus + .instance_lookup_id( &opctx, - &organization_name, - &project_name, - &instance_name, + params::InstanceSelector::InstanceProjectAndOrg { + instance_name: omicron_common::api::external::Name::from( + instance_name.clone(), + ), + project_name: omicron_common::api::external::Name::from( + project_name.clone(), + ), + organization_name: + omicron_common::api::external::Name::from( + organization_name.clone(), + ), + }, ) .await?; + let instance = nexus.instance_start(&opctx, instance_id).await?; Ok(HttpResponseAccepted(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 0cd178d68de..41927a2723a 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -53,6 +53,7 @@ v1_instance_delete /v1/instances/{instance} v1_instance_list /v1/instances v1_instance_migrate /v1/instances/{instance}/migrate v1_instance_reboot /v1/instances/{instance}/reboot +v1_instance_start /v1/instances/{instance}/start v1_instance_view /v1/instances/{instance} API operations found with tag "login" diff --git a/openapi/nexus.json b/openapi/nexus.json index 3c748a805c5..fc80986d960 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8153,6 +8153,87 @@ } } } + }, + "/v1/instances/{instance}/start": { + "post": { + "tags": [ + "instances" + ], + "summary": "Boot an instance", + "operationId": "v1_instance_start", + "parameters": [ + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "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" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { From e6374fe618a0fe1fa1e899778414f41c7335b0cf Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 22 Nov 2022 14:42:37 -0500 Subject: [PATCH 15/72] Add instance stop endpoint --- nexus/src/app/instance.rs | 8 +-- nexus/src/external_api/http_entrypoints.rs | 49 +++++++++++-- nexus/tests/output/nexus_tags.txt | 1 + openapi/nexus.json | 80 ++++++++++++++++++++++ 4 files changed, 127 insertions(+), 11 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index daefe1f8f73..8c448cffc05 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -446,15 +446,11 @@ impl super::Nexus { pub async fn instance_stop( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + instance_id: Uuid, ) -> UpdateResult { let (.., authz_instance, db_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) + .instance_id(instance_id) .fetch() .await?; let requested = InstanceRuntimeStateRequested { diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index d27a96f8533..17426cae88c 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -153,6 +153,7 @@ pub fn external_api() -> NexusApiDescription { api.register(v1_instance_migrate)?; api.register(v1_instance_reboot)?; api.register(v1_instance_start)?; + api.register(v1_instance_stop)?; // Project-scoped images API api.register(image_list)?; @@ -2570,6 +2571,34 @@ async fn instance_start( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = POST, + path = "/v1/instances/{instance}/stop", + tags = ["instances"], +}] +async fn v1_instance_stop( + rqctx: Arc>>, + 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 handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_id = nexus + .instance_lookup_id( + &opctx, + query.selector.to_instance_selector(path.instance.into()), + ) + .await?; + let instance = nexus.instance_stop(&opctx, instance_id).await?; + Ok(HttpResponseOk(instance.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Halt an instance #[endpoint { method = POST, @@ -2588,14 +2617,24 @@ async fn instance_stop( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance = nexus - .instance_stop( + let instance_id = nexus + .instance_lookup_id( &opctx, - &organization_name, - &project_name, - &instance_name, + params::InstanceSelector::InstanceProjectAndOrg { + instance_name: omicron_common::api::external::Name::from( + instance_name.clone(), + ), + project_name: omicron_common::api::external::Name::from( + project_name.clone(), + ), + organization_name: + omicron_common::api::external::Name::from( + organization_name.clone(), + ), + }, ) .await?; + let instance = nexus.instance_stop(&opctx, instance_id).await?; Ok(HttpResponseAccepted(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 41927a2723a..4a4f649be66 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -54,6 +54,7 @@ v1_instance_list /v1/instances v1_instance_migrate /v1/instances/{instance}/migrate v1_instance_reboot /v1/instances/{instance}/reboot v1_instance_start /v1/instances/{instance}/start +v1_instance_stop /v1/instances/{instance}/stop v1_instance_view /v1/instances/{instance} API operations found with tag "login" diff --git a/openapi/nexus.json b/openapi/nexus.json index fc80986d960..1304dc01cc0 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8234,6 +8234,86 @@ } } } + }, + "/v1/instances/{instance}/stop": { + "post": { + "tags": [ + "instances" + ], + "operationId": "v1_instance_stop", + "parameters": [ + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "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" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Instance" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { From cdaa97744f067db524802386c31eb7b92c285703 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 22 Nov 2022 15:27:34 -0500 Subject: [PATCH 16/72] Add serial console endpoints --- nexus/src/app/instance.rs | 42 +---- nexus/src/external_api/http_entrypoints.rs | 136 +++++++++++++-- nexus/tests/output/nexus_tags.txt | 2 + openapi/nexus.json | 189 +++++++++++++++++++++ 4 files changed, 315 insertions(+), 54 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 8c448cffc05..c8b933d58f0 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -266,22 +266,6 @@ impl super::Nexus { } pub async fn instance_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, - ) -> LookupResult { - let (.., db_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) - .fetch() - .await?; - Ok(db_instance) - } - - pub async fn instance_fetch_by_id( &self, opctx: &OpContext, instance_id: &Uuid, @@ -1147,19 +1131,10 @@ impl super::Nexus { pub(crate) async fn instance_serial_console_data( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + instance_id: &Uuid, params: ¶ms::InstanceSerialConsoleRequest, ) -> Result { - let db_instance = self - .instance_fetch( - opctx, - organization_name, - project_name, - instance_name, - ) - .await?; + let db_instance = self.instance_fetch(opctx, instance_id).await?; let sa = self.instance_sled(&db_instance).await?; let data = sa @@ -1183,18 +1158,9 @@ impl super::Nexus { &self, opctx: &OpContext, conn: dropshot::WebsocketConnection, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + instance_id: &Uuid, ) -> Result<(), Error> { - let instance = self - .instance_fetch( - opctx, - organization_name, - project_name, - instance_name, - ) - .await?; + let instance = self.instance_fetch(opctx, instance_id).await?; let ip_addr = instance .runtime_state .propolis_ip diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 17426cae88c..3199914d6f9 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -154,6 +154,8 @@ pub fn external_api() -> NexusApiDescription { api.register(v1_instance_reboot)?; api.register(v1_instance_start)?; api.register(v1_instance_stop)?; + api.register(v1_instance_serial_console)?; + api.register(v1_instance_serial_console_stream)?; // Project-scoped images API api.register(image_list)?; @@ -2209,7 +2211,7 @@ async fn v1_instance_view( query.selector.to_instance_selector(path.instance.into()), ) .await?; - let instance = nexus.instance_fetch_by_id(&opctx, &instance_id).await?; + let instance = nexus.instance_fetch(&opctx, &instance_id).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2241,14 +2243,24 @@ async fn instance_view( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance = nexus - .instance_fetch( + let instance_id = nexus + .instance_lookup_id( &opctx, - &organization_name, - &project_name, - &instance_name, + params::InstanceSelector::InstanceProjectAndOrg { + instance_name: omicron_common::api::external::Name::from( + instance_name.clone(), + ), + project_name: omicron_common::api::external::Name::from( + project_name.clone(), + ), + organization_name: + omicron_common::api::external::Name::from( + organization_name.clone(), + ), + }, ) .await?; + let instance = nexus.instance_fetch(&opctx, &instance_id).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2270,7 +2282,7 @@ async fn instance_view_by_id( let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance = nexus.instance_fetch_by_id(&opctx, id).await?; + let instance = nexus.instance_fetch(&opctx, id).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2640,6 +2652,49 @@ async fn instance_stop( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +pub struct InstanceSerialConsoleParams { + #[serde(flatten)] + selector: params::ProjectSelector, + + #[serde(flatten)] + pub console_params: params::InstanceSerialConsoleRequest, +} + +#[endpoint { + method = POST, + path = "/v1/instances/{instance}/serial-console", + tags = ["instances"], +}] +async fn v1_instance_serial_console( + 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 instance_id = nexus + .instance_lookup_id( + &opctx, + query.selector.to_instance_selector(path.instance.into()), + ) + .await?; + let console = nexus + .instance_serial_console_data( + &opctx, + &instance_id, + &query.console_params, + ) + .await?; + Ok(HttpResponseOk(console.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Fetch an instance's serial console #[endpoint { method = GET, @@ -2659,12 +2714,27 @@ async fn instance_serial_console( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_id = nexus + .instance_lookup_id( + &opctx, + params::InstanceSelector::InstanceProjectAndOrg { + instance_name: omicron_common::api::external::Name::from( + instance_name.clone(), + ), + project_name: omicron_common::api::external::Name::from( + project_name.clone(), + ), + organization_name: + omicron_common::api::external::Name::from( + organization_name.clone(), + ), + }, + ) + .await?; let data = nexus .instance_serial_console_data( &opctx, - &organization_name, - &project_name, - &instance_name, + &instance_id, &query_params.into_inner(), ) .await?; @@ -2673,6 +2743,32 @@ async fn instance_serial_console( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[channel { + protocol = WEBSOCKETS, + path = "/v1/instances/{instance}/serial-console/stream", + tags = ["instances"], +}] +async fn v1_instance_serial_console_stream( + rqctx: Arc>>, + conn: WebsocketConnection, + 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_id = nexus + .instance_lookup_id( + &opctx, + query.selector.to_instance_selector(path.instance.into()), + ) + .await?; + nexus.instance_serial_console_stream(&opctx, conn, &instance_id).await?; + Ok(()) +} + /// Connect to an instance's serial console #[channel { protocol = WEBSOCKETS, @@ -2691,15 +2787,23 @@ async fn instance_serial_console_stream( let project_name = &path.project_name; let instance_name = &path.instance_name; let opctx = OpContext::for_external_api(&rqctx).await?; - nexus - .instance_serial_console_stream( + let instance_id = nexus + .instance_lookup_id( &opctx, - conn, - organization_name, - project_name, - instance_name, + params::InstanceSelector::InstanceProjectAndOrg { + instance_name: omicron_common::api::external::Name::from( + instance_name.clone(), + ), + project_name: omicron_common::api::external::Name::from( + project_name.clone(), + ), + organization_name: omicron_common::api::external::Name::from( + organization_name.clone(), + ), + }, ) .await?; + nexus.instance_serial_console_stream(&opctx, conn, &instance_id).await?; Ok(()) } diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 4a4f649be66..a631e59da38 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -53,6 +53,8 @@ v1_instance_delete /v1/instances/{instance} v1_instance_list /v1/instances v1_instance_migrate /v1/instances/{instance}/migrate v1_instance_reboot /v1/instances/{instance}/reboot +v1_instance_serial_console /v1/instances/{instance}/serial-console +v1_instance_serial_console_stream /v1/instances/{instance}/serial-console/stream v1_instance_start /v1/instances/{instance}/start v1_instance_stop /v1/instances/{instance}/stop v1_instance_view /v1/instances/{instance} diff --git a/openapi/nexus.json b/openapi/nexus.json index 1304dc01cc0..1e692676033 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8154,6 +8154,195 @@ } } }, + "/v1/instances/{instance}/serial-console": { + "post": { + "tags": [ + "instances" + ], + "operationId": "v1_instance_serial_console", + "parameters": [ + { + "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" + }, + "style": "simple" + }, + { + "in": "query", + "name": "from_start", + "description": "Character index in the serial buffer from which to read, counting the bytes output since instance start. If this is not provided, `most_recent` must be provided, and if this *is* provided, `most_recent` must *not* be provided.", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "style": "form" + }, + { + "in": "query", + "name": "max_bytes", + "description": "Maximum number of bytes of buffered serial console contents to return. If the requested range runs to the end of the available buffer, the data returned will be shorter than `max_bytes`.", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "style": "form" + }, + { + "in": "query", + "name": "most_recent", + "description": "Character index in the serial buffer from which to read, counting *backward* from the most recently buffered data retrieved from the instance. (See note on `from_start` about mutual exclusivity)", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "style": "form" + }, + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceSerialConsoleData" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/serial-console/stream": { + "get": { + "tags": [ + "instances" + ], + "operationId": "v1_instance_serial_console_stream", + "parameters": [ + { + "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" + }, + "style": "simple" + }, + { + "in": "query", + "name": "project_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "organization_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + }, + { + "in": "query", + "name": "project_name", + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "form" + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + }, + "x-dropshot-websocket": {} + } + }, "/v1/instances/{instance}/start": { "post": { "tags": [ From d0a19c6669e6427c24989a4d79fe84ac687a9bf4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 22 Nov 2022 17:03:40 -0500 Subject: [PATCH 17/72] Workaround for multiple emits --- nexus/types/src/external_api/params.rs | 21 ++++++- openapi/nexus.json | 80 -------------------------- 2 files changed, 18 insertions(+), 83 deletions(-) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index b6c4b232b7f..151a71f0a5a 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -21,9 +21,19 @@ use uuid::Uuid; #[derive(Deserialize, JsonSchema)] #[serde(untagged)] pub enum ProjectSelector { - ProjectId { project_id: Uuid }, - ProjectAndOrgId { project_name: Name, organization_id: Uuid }, - ProjectAndOrg { project_name: Name, organization_name: Name }, + ProjectId { + project_id: Uuid, + }, + ProjectAndOrgId { + project_name: Name, + organization_id: Uuid, + }, + ProjectAndOrg { + // FIXME: There's a bug in schemars or serde which causes project_name to be emitted twice + #[schemars(skip)] + project_name: Name, + organization_name: Name, + }, None {}, } @@ -73,12 +83,17 @@ pub enum InstanceSelector { project_id: Uuid, }, InstanceProjectAndOrgId { + // FIXME: There's a bug in schemars or serde which causes instance_name to be emitted multiple times + #[schemars(skip)] instance_name: Name, project_name: Name, organization_id: Uuid, }, InstanceProjectAndOrg { + #[schemars(skip)] instance_name: Name, + // FIXME: There's a bug in schemars or serde which causes project_name to be emitted multiple times + #[schemars(skip)] project_name: Name, organization_name: Name, }, diff --git a/openapi/nexus.json b/openapi/nexus.json index 1e692676033..aae6eea4267 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7724,14 +7724,6 @@ "$ref": "#/components/schemas/Name" }, "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" } ], "responses": { @@ -7793,14 +7785,6 @@ "$ref": "#/components/schemas/Name" }, "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" } ], "requestBody": { @@ -7874,14 +7858,6 @@ }, "style": "form" }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" - }, { "in": "path", "name": "instance", @@ -7952,14 +7928,6 @@ }, "style": "form" }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" - }, { "in": "path", "name": "instance", @@ -8025,14 +7993,6 @@ }, "style": "form" }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" - }, { "in": "path", "name": "instance", @@ -8115,14 +8075,6 @@ }, "style": "form" }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" - }, { "in": "path", "name": "instance", @@ -8240,14 +8192,6 @@ "$ref": "#/components/schemas/Name" }, "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" } ], "responses": { @@ -8320,14 +8264,6 @@ "$ref": "#/components/schemas/Name" }, "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" } ], "responses": { @@ -8385,14 +8321,6 @@ }, "style": "form" }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" - }, { "in": "path", "name": "instance", @@ -8465,14 +8393,6 @@ }, "style": "form" }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "form" - }, { "in": "path", "name": "instance", From f439f592f8abe46dcd33fd15f2ff8d2e7d82f1b8 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 22 Nov 2022 17:22:19 -0500 Subject: [PATCH 18/72] Implement NameOrId for display to try resolve dropshot error --- common/src/api/external/mod.rs | 5 ++--- nexus/db-model/src/name.rs | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index a490cba1eae..6a95f85cb4d 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -268,9 +268,8 @@ impl Name { } } -#[derive(Serialize, Deserialize)] -#[serde(try_from = "String")] - +#[derive(Serialize, Deserialize, Display)] +#[display("{0}")] pub enum NameOrId { Name(Name), Id(Uuid), diff --git a/nexus/db-model/src/name.rs b/nexus/db-model/src/name.rs index 10d0819b5fb..57235bdf011 100644 --- a/nexus/db-model/src/name.rs +++ b/nexus/db-model/src/name.rs @@ -66,9 +66,10 @@ where /// Newtype wrapper around [external::NameOrId]. This type isn't actually /// stored in the database, but exists as a convenience for the API. -#[derive(JsonSchema, Serialize, Deserialize, RefCast)] +#[derive(JsonSchema, Serialize, Deserialize, RefCast, Display)] #[serde(transparent)] #[repr(transparent)] +#[display("{0}")] pub struct NameOrId(pub external::NameOrId); NewtypeFrom! { () pub struct NameOrId(external::NameOrId); } From b4e818a8d52642bf1522f645112d65856ec36fce Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 22 Nov 2022 19:02:00 -0500 Subject: [PATCH 19/72] Flip the order of id/name in the NameOrId enum --- common/src/api/external/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 6a95f85cb4d..84870942582 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -271,8 +271,8 @@ impl Name { #[derive(Serialize, Deserialize, Display)] #[display("{0}")] pub enum NameOrId { - Name(Name), Id(Uuid), + Name(Name), } impl TryFrom for NameOrId { From 7bdfeb09f31a2d9046612baad0c0092608d2de84 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 25 Nov 2022 11:08:49 -0500 Subject: [PATCH 20/72] Add untagged to NameOrId --- common/src/api/external/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 84870942582..0a8212b4272 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -270,6 +270,7 @@ impl Name { #[derive(Serialize, Deserialize, Display)] #[display("{0}")] +#[serde(untagged)] pub enum NameOrId { Id(Uuid), Name(Name), From cf5f54076c66c8a1798f68d6a2256dca0fcddd18 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 29 Nov 2022 18:45:41 -0500 Subject: [PATCH 21/72] Update NameOrId to be labeled --- common/src/api/external/mod.rs | 4 ++-- openapi/nexus.json | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 0a8212b4272..bb2d0b2e788 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -299,8 +299,8 @@ impl JsonSchema for NameOrId { schemars::schema::SchemaObject { subschemas: Some(Box::new(schemars::schema::SubschemaValidation { one_of: Some(vec![ - gen.subschema_for::(), - gen.subschema_for::(), + label_schema("id", gen.subschema_for::()), + label_schema("name", gen.subschema_for::()), ]), ..Default::default() })), diff --git a/openapi/nexus.json b/openapi/nexus.json index aae6eea4267..3f6ad4d8e31 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -13597,11 +13597,21 @@ "NameOrId": { "oneOf": [ { - "$ref": "#/components/schemas/Name" + "title": "id", + "allOf": [ + { + "type": "string", + "format": "uuid" + } + ] }, { - "type": "string", - "format": "uuid" + "title": "name", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] } ] } From 8c7ce212c7c5b8d94d320ccf6b89e37a50e87447 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 30 Nov 2022 10:38:57 -0500 Subject: [PATCH 22/72] RFD-322 PoC Revision 2 (#1995) --- common/src/api/external/mod.rs | 2 +- nexus/db-model/src/name.rs | 11 - nexus/src/app/instance.rs | 45 ++-- nexus/src/app/project.rs | 26 +- nexus/src/external_api/http_entrypoints.rs | 181 ++++++-------- nexus/tests/integration_tests/instances.rs | 6 +- nexus/types/src/external_api/params.rs | 96 ++------ openapi/nexus.json | 274 ++++----------------- 8 files changed, 189 insertions(+), 452 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index bb2d0b2e788..7ae77794494 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -268,7 +268,7 @@ impl Name { } } -#[derive(Serialize, Deserialize, Display)] +#[derive(Serialize, Deserialize, Display, Clone)] #[display("{0}")] #[serde(untagged)] pub enum NameOrId { diff --git a/nexus/db-model/src/name.rs b/nexus/db-model/src/name.rs index 57235bdf011..96603530333 100644 --- a/nexus/db-model/src/name.rs +++ b/nexus/db-model/src/name.rs @@ -63,14 +63,3 @@ where String::from_sql(bytes)?.parse().map(Name).map_err(|e| e.into()) } } - -/// Newtype wrapper around [external::NameOrId]. This type isn't actually -/// stored in the database, but exists as a convenience for the API. -#[derive(JsonSchema, Serialize, Deserialize, RefCast, Display)] -#[serde(transparent)] -#[repr(transparent)] -#[display("{0}")] -pub struct NameOrId(pub external::NameOrId); - -NewtypeFrom! { () pub struct NameOrId(external::NameOrId); } -NewtypeDeref! { () pub struct NameOrId(external::NameOrId); } diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index c8b933d58f0..1289f9183c4 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -32,6 +32,7 @@ use omicron_common::api::external::InstanceState; use omicron_common::api::external::InternalContext; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; use omicron_common::api::external::Vni; use omicron_common::api::internal::nexus; @@ -59,13 +60,16 @@ impl super::Nexus { instance_selector: params::InstanceSelector, ) -> LookupResult { match instance_selector { - params::InstanceSelector::InstanceId { instance_id } => { - Ok(instance_id) + params::InstanceSelector { instance: NameOrId::Id(id), .. } => { + // TODO: 400 if project or organization are present + Ok(id) } - params::InstanceSelector::InstanceAndProjectId { - instance_name, - project_id, + params::InstanceSelector { + instance: NameOrId::Name(instance_name), + project: Some(NameOrId::Id(project_id)), + .. } => { + // TODO: 400 if organization is present let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) .project_id(project_id) @@ -74,10 +78,10 @@ impl super::Nexus { .await?; Ok(authz_instance.id()) } - params::InstanceSelector::InstanceProjectAndOrgId { - instance_name, - project_name, - organization_id, + params::InstanceSelector { + instance: NameOrId::Name(instance_name), + project: Some(NameOrId::Name(project_name)), + organization: Some(NameOrId::Id(organization_id)), } => { let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) @@ -88,10 +92,10 @@ impl super::Nexus { .await?; Ok(authz_instance.id()) } - params::InstanceSelector::InstanceProjectAndOrg { - instance_name, - project_name, - organization_name, + params::InstanceSelector { + instance: NameOrId::Name(instance_name), + project: Some(NameOrId::Name(project_name)), + organization: Some(NameOrId::Name(organization_name)), } => { let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) @@ -102,14 +106,15 @@ impl super::Nexus { .await?; Ok(authz_instance.id()) } - params::InstanceSelector::None {} => Err(Error::InvalidRequest { + // TODO: Add a better error message + _ => Err(Error::InvalidRequest { message: " - Unable to resolve instance. Expected one of - - instance_id - - instance_name, project_id - - instance_name, project_name, organization_id - - instance_name, project_name, organization_name - " + 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/project.rs b/nexus/src/app/project.rs index c0eeb4c95dd..e5178a6537e 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -20,6 +20,7 @@ use omicron_common::api::external::Error; use omicron_common::api::external::IdentityMetadataCreateParams; 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 uuid::Uuid; @@ -30,10 +31,13 @@ impl super::Nexus { project_selector: params::ProjectSelector, ) -> LookupResult { match project_selector { - params::ProjectSelector::ProjectId { project_id } => Ok(project_id), - params::ProjectSelector::ProjectAndOrgId { - project_name, - organization_id, + params::ProjectSelector { project: NameOrId::Id(id), .. } => { + // TODO: 400 if organization is present + Ok(id) + } + params::ProjectSelector { + project: NameOrId::Name(project_name), + organization: Some(NameOrId::Id(organization_id)), } => { let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) @@ -43,9 +47,9 @@ impl super::Nexus { .await?; Ok(authz_project.id()) } - params::ProjectSelector::ProjectAndOrg { - project_name, - organization_name, + params::ProjectSelector { + project: NameOrId::Name(project_name), + organization: Some(NameOrId::Name(organization_name)), } => { let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) @@ -55,12 +59,12 @@ impl super::Nexus { .await?; Ok(authz_project.id()) } - params::ProjectSelector::None {} => Err(Error::InvalidRequest { + _ => Err(Error::InvalidRequest { message: " Unable to resolve project. Expected one of - - project_id - - project_name, organization_id - - project_name, organization_name + - project: Uuid + - project: Name, organization: Uuid + - project: Name, organization: Name " .to_string(), }), diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 3199914d6f9..4cbc23ad709 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -18,7 +18,6 @@ use crate::authz; use crate::context::OpContext; use crate::db; use crate::db::model::Name; -use crate::db::model::NameOrId; use crate::external_api::shared; use crate::ServerContext; use dropshot::ApiDescription; @@ -61,6 +60,7 @@ 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; @@ -2076,9 +2076,11 @@ async fn instance_list( let project_id = nexus .project_lookup_id( &opctx, - params::ProjectSelector::ProjectAndOrg { - project_name: project_name.clone().into(), - organization_name: organization_name.clone().into(), + params::ProjectSelector { + project: NameOrId::Name(project_name.clone().into()), + organization: Some(NameOrId::Name( + organization_name.clone().into(), + )), }, ) .await?; @@ -2102,6 +2104,12 @@ async fn instance_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +struct InstanceCreateParams { + #[serde(flatten)] + selector: params::ProjectSelector, +} + #[endpoint { method = POST, path = "/v1/instances", @@ -2109,7 +2117,7 @@ async fn instance_list( }] async fn v1_instance_create( rqctx: Arc>>, - query_params: Query, + query_params: Query, new_instance: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -2157,9 +2165,11 @@ async fn instance_create( let project_id = nexus .project_lookup_id( &opctx, - params::ProjectSelector::ProjectAndOrg { - project_name: project_name.clone().into(), - organization_name: organization_name.clone().into(), + params::ProjectSelector { + project: NameOrId::Name(project_name.clone().into()), + organization: Some(NameOrId::Name( + organization_name.clone().into(), + )), }, ) .await?; @@ -2186,7 +2196,7 @@ struct InstanceLookupPathParam { #[derive(Deserialize, JsonSchema)] struct InstanceQueryParams { #[serde(flatten)] - selector: params::ProjectSelector, + selector: Option, } #[endpoint { @@ -2208,7 +2218,7 @@ async fn v1_instance_view( let instance_id = nexus .instance_lookup_id( &opctx, - query.selector.to_instance_selector(path.instance.into()), + params::InstanceSelector::new(path.instance, &query.selector), ) .await?; let instance = nexus.instance_fetch(&opctx, &instance_id).await?; @@ -2246,17 +2256,12 @@ async fn instance_view( let instance_id = nexus .instance_lookup_id( &opctx, - params::InstanceSelector::InstanceProjectAndOrg { - instance_name: omicron_common::api::external::Name::from( - instance_name.clone(), - ), - project_name: omicron_common::api::external::Name::from( - project_name.clone(), - ), - organization_name: - omicron_common::api::external::Name::from( - organization_name.clone(), - ), + 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(), + )), }, ) .await?; @@ -2307,7 +2312,7 @@ async fn v1_instance_delete( let instance_id = nexus .instance_lookup_id( &opctx, - query.selector.to_instance_selector(path.instance.into()), + params::InstanceSelector::new(path.instance, &query.selector), ) .await?; nexus.project_destroy_instance(&opctx, &instance_id).await?; @@ -2337,17 +2342,12 @@ async fn instance_delete( let instance_id = nexus .instance_lookup_id( &opctx, - params::InstanceSelector::InstanceProjectAndOrg { - instance_name: omicron_common::api::external::Name::from( - instance_name.clone(), - ), - project_name: omicron_common::api::external::Name::from( - project_name.clone(), - ), - organization_name: - omicron_common::api::external::Name::from( - organization_name.clone(), - ), + 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(), + )), }, ) .await?; @@ -2379,7 +2379,7 @@ async fn v1_instance_migrate( let instance_id = nexus .instance_lookup_id( &opctx, - query.selector.to_instance_selector(path.instance.into()), + params::InstanceSelector::new(path.instance, &query.selector), ) .await?; let instance = nexus @@ -2418,17 +2418,12 @@ async fn instance_migrate( let instance_id = nexus .instance_lookup_id( &opctx, - params::InstanceSelector::InstanceProjectAndOrg { - instance_name: omicron_common::api::external::Name::from( - instance_name.clone(), - ), - project_name: omicron_common::api::external::Name::from( - project_name.clone(), - ), - organization_name: - omicron_common::api::external::Name::from( - organization_name.clone(), - ), + 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(), + )), }, ) .await?; @@ -2463,7 +2458,7 @@ async fn v1_instance_reboot( let instance_id = nexus .instance_lookup_id( &opctx, - query.selector.to_instance_selector(path.instance.into()), + params::InstanceSelector::new(path.instance, &query.selector), ) .await?; let instance = nexus.instance_reboot(&opctx, instance_id).await?; @@ -2493,17 +2488,12 @@ async fn instance_reboot( let instance_id = nexus .instance_lookup_id( &opctx, - params::InstanceSelector::InstanceProjectAndOrg { - instance_name: omicron_common::api::external::Name::from( - instance_name.clone(), - ), - project_name: omicron_common::api::external::Name::from( - project_name.clone(), - ), - organization_name: - omicron_common::api::external::Name::from( - organization_name.clone(), - ), + 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(), + )), }, ) .await?; @@ -2533,7 +2523,7 @@ async fn v1_instance_start( let instance_id = nexus .instance_lookup_id( &opctx, - query.selector.to_instance_selector(path.instance.into()), + params::InstanceSelector::new(path.instance, &query.selector), ) .await?; let instance = nexus.instance_start(&opctx, instance_id).await?; @@ -2563,17 +2553,12 @@ async fn instance_start( let instance_id = nexus .instance_lookup_id( &opctx, - params::InstanceSelector::InstanceProjectAndOrg { - instance_name: omicron_common::api::external::Name::from( - instance_name.clone(), - ), - project_name: omicron_common::api::external::Name::from( - project_name.clone(), - ), - organization_name: - omicron_common::api::external::Name::from( - organization_name.clone(), - ), + 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(), + )), }, ) .await?; @@ -2602,7 +2587,7 @@ async fn v1_instance_stop( let instance_id = nexus .instance_lookup_id( &opctx, - query.selector.to_instance_selector(path.instance.into()), + params::InstanceSelector::new(path.instance, &query.selector), ) .await?; let instance = nexus.instance_stop(&opctx, instance_id).await?; @@ -2632,17 +2617,12 @@ async fn instance_stop( let instance_id = nexus .instance_lookup_id( &opctx, - params::InstanceSelector::InstanceProjectAndOrg { - instance_name: omicron_common::api::external::Name::from( - instance_name.clone(), - ), - project_name: omicron_common::api::external::Name::from( - project_name.clone(), - ), - organization_name: - omicron_common::api::external::Name::from( - organization_name.clone(), - ), + 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(), + )), }, ) .await?; @@ -2655,7 +2635,7 @@ async fn instance_stop( #[derive(Deserialize, JsonSchema)] pub struct InstanceSerialConsoleParams { #[serde(flatten)] - selector: params::ProjectSelector, + selector: Option, #[serde(flatten)] pub console_params: params::InstanceSerialConsoleRequest, @@ -2680,7 +2660,7 @@ async fn v1_instance_serial_console( let instance_id = nexus .instance_lookup_id( &opctx, - query.selector.to_instance_selector(path.instance.into()), + params::InstanceSelector::new(path.instance, &query.selector), ) .await?; let console = nexus @@ -2717,17 +2697,12 @@ async fn instance_serial_console( let instance_id = nexus .instance_lookup_id( &opctx, - params::InstanceSelector::InstanceProjectAndOrg { - instance_name: omicron_common::api::external::Name::from( - instance_name.clone(), - ), - project_name: omicron_common::api::external::Name::from( - project_name.clone(), - ), - organization_name: - omicron_common::api::external::Name::from( - organization_name.clone(), - ), + 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(), + )), }, ) .await?; @@ -2762,7 +2737,7 @@ async fn v1_instance_serial_console_stream( let instance_id = nexus .instance_lookup_id( &opctx, - query.selector.to_instance_selector(path.instance.into()), + params::InstanceSelector::new(path.instance, &query.selector), ) .await?; nexus.instance_serial_console_stream(&opctx, conn, &instance_id).await?; @@ -2790,16 +2765,12 @@ async fn instance_serial_console_stream( let instance_id = nexus .instance_lookup_id( &opctx, - params::InstanceSelector::InstanceProjectAndOrg { - instance_name: omicron_common::api::external::Name::from( - instance_name.clone(), - ), - project_name: omicron_common::api::external::Name::from( - project_name.clone(), - ), - organization_name: omicron_common::api::external::Name::from( - organization_name.clone(), - ), + 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(), + )), }, ) .await?; diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 5dec931188b..594c9573640 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -149,7 +149,7 @@ async fn test_v1_instance_access(cptestctx: &ControlPlaneTestContext) { let fetched_instance = instance_get( &client, format!( - "/v1/instances/{}?project_id={}", + "/v1/instances/{}?project={}", instance.identity.name, project.identity.id ) .as_str(), @@ -161,7 +161,7 @@ async fn test_v1_instance_access(cptestctx: &ControlPlaneTestContext) { let fetched_instance = instance_get( &client, format!( - "/v1/instances/{}?project_name={}&organization_id={}", + "/v1/instances/{}?project={}&organization={}", instance.identity.name, project.identity.name, org.identity.id ) .as_str(), @@ -173,7 +173,7 @@ async fn test_v1_instance_access(cptestctx: &ControlPlaneTestContext) { let fetched_instance = instance_get( &client, format!( - "/v1/instances/{}?project_name={}&organization_name={}", + "/v1/instances/{}?project={}&organization={}", instance.identity.name, project.identity.name, org.identity.name ) .as_str(), diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 151a71f0a5a..dd57a3935dc 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -19,87 +19,33 @@ use std::{net::IpAddr, str::FromStr}; use uuid::Uuid; #[derive(Deserialize, JsonSchema)] -#[serde(untagged)] -pub enum ProjectSelector { - ProjectId { - project_id: Uuid, - }, - ProjectAndOrgId { - project_name: Name, - organization_id: Uuid, - }, - ProjectAndOrg { - // FIXME: There's a bug in schemars or serde which causes project_name to be emitted twice - #[schemars(skip)] - project_name: Name, - organization_name: Name, - }, - None {}, +pub struct ProjectSelector { + pub project: NameOrId, + pub organization: Option, } -impl ProjectSelector { - pub fn to_instance_selector(self, instance: NameOrId) -> InstanceSelector { - match instance { - NameOrId::Id(instance_id) => { - InstanceSelector::InstanceId { instance_id } - } - NameOrId::Name(instance_name) => match self { - ProjectSelector::ProjectId { project_id } => { - InstanceSelector::InstanceAndProjectId { - instance_name, - project_id, - } - } - ProjectSelector::ProjectAndOrgId { - project_name, - organization_id, - } => InstanceSelector::InstanceProjectAndOrgId { - instance_name, - project_name, - organization_id, - }, - ProjectSelector::ProjectAndOrg { - project_name, - organization_name, - } => InstanceSelector::InstanceProjectAndOrg { - instance_name, - project_name, - organization_name, - }, - ProjectSelector::None {} => InstanceSelector::None {}, - }, +#[derive(Deserialize, JsonSchema)] +pub struct InstanceSelector { + pub instance: NameOrId, + pub project: Option, + pub organization: Option, +} + +impl InstanceSelector { + pub fn new( + instance: NameOrId, + project_selector: &Option, + ) -> InstanceSelector { + InstanceSelector { + instance, + organization: project_selector + .as_ref() + .and_then(|s| s.organization.clone()), + project: project_selector.as_ref().map(|s| s.project.clone()), } } } -#[derive(Deserialize, JsonSchema)] -#[serde(untagged)] -pub enum InstanceSelector { - InstanceId { - instance_id: Uuid, - }, - InstanceAndProjectId { - instance_name: Name, - project_id: Uuid, - }, - InstanceProjectAndOrgId { - // FIXME: There's a bug in schemars or serde which causes instance_name to be emitted multiple times - #[schemars(skip)] - instance_name: Name, - project_name: Name, - organization_id: Uuid, - }, - InstanceProjectAndOrg { - #[schemars(skip)] - instance_name: Name, - // FIXME: There's a bug in schemars or serde which causes project_name to be emitted multiple times - #[schemars(skip)] - project_name: Name, - organization_name: Name, - }, - None {}, -} - // Silos /// Create-time parameters for a [`Silo`](crate::external_api::views::Silo) diff --git a/openapi/nexus.json b/openapi/nexus.json index 3f6ad4d8e31..8e730cb17be 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7675,53 +7675,36 @@ }, { "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/NameSortMode" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_id", + "name": "organization", "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_id", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", "schema": { - "type": "string", - "format": "uuid" + "nullable": true, + "type": "string" }, "style": "form" }, { "in": "query", - "name": "project_name", + "name": "project", + "required": true, "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "sort_by", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameSortMode" }, "style": "form" } @@ -7754,35 +7737,18 @@ "parameters": [ { "in": "query", - "name": "project_id", + "name": "organization", "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "project", + "required": true, "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" } @@ -7826,35 +7792,17 @@ "parameters": [ { "in": "query", - "name": "project_id", + "name": "organization", "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "project", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, @@ -7896,35 +7844,17 @@ "parameters": [ { "in": "query", - "name": "project_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "organization_id", + "name": "organization", "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "project", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, @@ -7961,35 +7891,17 @@ "parameters": [ { "in": "query", - "name": "project_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", + "name": "organization", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "project", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, @@ -8043,35 +7955,17 @@ "parameters": [ { "in": "query", - "name": "project_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "organization_id", + "name": "organization", "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "project", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, @@ -8161,35 +8055,17 @@ }, { "in": "query", - "name": "project_id", + "name": "organization", "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "project", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" } @@ -8233,35 +8109,17 @@ }, { "in": "query", - "name": "project_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "organization_id", + "name": "organization", "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "project", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" } @@ -8289,35 +8147,17 @@ "parameters": [ { "in": "query", - "name": "project_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", + "name": "organization", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "project", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, @@ -8361,35 +8201,17 @@ "parameters": [ { "in": "query", - "name": "project_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "organization_id", + "name": "organization", "schema": { - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "project_name", - "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "organization_name", + "name": "project", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, From 87f4d2522101ae2760da43d1d474de1b9b14e85a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 30 Nov 2022 13:31:22 -0500 Subject: [PATCH 23/72] Move version to postfix --- nexus/src/external_api/http_entrypoints.rs | 40 +++++++++++----------- nexus/tests/output/nexus_tags.txt | 20 +++++------ openapi/nexus.json | 20 +++++------ 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 4cbc23ad709..9a73863bd1c 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -146,16 +146,16 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_stop)?; api.register(instance_serial_console)?; api.register(instance_serial_console_stream)?; - api.register(v1_instance_list)?; - api.register(v1_instance_view)?; - api.register(v1_instance_create)?; - api.register(v1_instance_delete)?; - api.register(v1_instance_migrate)?; - api.register(v1_instance_reboot)?; - api.register(v1_instance_start)?; - api.register(v1_instance_stop)?; - api.register(v1_instance_serial_console)?; - api.register(v1_instance_serial_console_stream)?; + api.register(instance_list_v1)?; + api.register(instance_view_v1)?; + api.register(instance_create_v1)?; + api.register(instance_delete_v1)?; + api.register(instance_migrate_v1)?; + api.register(instance_reboot_v1)?; + api.register(instance_start_v1)?; + api.register(instance_stop_v1)?; + api.register(instance_serial_console_v1)?; + api.register(instance_serial_console_stream_v1)?; // Project-scoped images API api.register(image_list)?; @@ -2023,7 +2023,7 @@ struct InstanceListQueryParams { path = "/v1/instances", tags = ["instances"], }] -async fn v1_instance_list( +async fn instance_list_v1( rqctx: Arc>>, query_params: Query, ) -> Result>, HttpError> { @@ -2115,7 +2115,7 @@ struct InstanceCreateParams { path = "/v1/instances", tags = ["instances"], }] -async fn v1_instance_create( +async fn instance_create_v1( rqctx: Arc>>, query_params: Query, new_instance: TypedBody, @@ -2204,7 +2204,7 @@ struct InstanceQueryParams { path = "/v1/instances/{instance}", tags = ["instances"], }] -async fn v1_instance_view( +async fn instance_view_v1( rqctx: Arc>>, query_params: Query, path_params: Path, @@ -2298,7 +2298,7 @@ async fn instance_view_by_id( path = "/v1/instances/{instance}", tags = ["instances"], }] -async fn v1_instance_delete( +async fn instance_delete_v1( rqctx: Arc>>, query_params: Query, path_params: Path, @@ -2363,7 +2363,7 @@ async fn instance_delete( path = "/v1/instances/{instance}/migrate", tags = ["instances"], }] -async fn v1_instance_migrate( +async fn instance_migrate_v1( rqctx: Arc>>, query_params: Query, path_params: Path, @@ -2444,7 +2444,7 @@ async fn instance_migrate( path = "/v1/instances/{instance}/reboot", tags = ["instances"], }] -async fn v1_instance_reboot( +async fn instance_reboot_v1( rqctx: Arc>>, query_params: Query, path_params: Path, @@ -2509,7 +2509,7 @@ async fn instance_reboot( path = "/v1/instances/{instance}/start", tags = ["instances"], }] -async fn v1_instance_start( +async fn instance_start_v1( rqctx: Arc>>, query_params: Query, path_params: Path, @@ -2573,7 +2573,7 @@ async fn instance_start( path = "/v1/instances/{instance}/stop", tags = ["instances"], }] -async fn v1_instance_stop( +async fn instance_stop_v1( rqctx: Arc>>, query_params: Query, path_params: Path, @@ -2646,7 +2646,7 @@ pub struct InstanceSerialConsoleParams { path = "/v1/instances/{instance}/serial-console", tags = ["instances"], }] -async fn v1_instance_serial_console( +async fn instance_serial_console_v1( rqctx: Arc>>, path_params: Path, query_params: Query, @@ -2723,7 +2723,7 @@ async fn instance_serial_console( path = "/v1/instances/{instance}/serial-console/stream", tags = ["instances"], }] -async fn v1_instance_serial_console_stream( +async fn instance_serial_console_stream_v1( rqctx: Arc>>, conn: WebsocketConnection, path_params: Path, diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index a631e59da38..3828fb5bfb7 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -28,13 +28,17 @@ image_view_by_id /by-id/images/{id} API operations found with tag "instances" OPERATION ID URL PATH instance_create /organizations/{organization_name}/projects/{project_name}/instances +instance_create_v1 /v1/instances instance_delete /organizations/{organization_name}/projects/{project_name}/instances/{instance_name} +instance_delete_v1 /v1/instances/{instance} instance_disk_attach /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach instance_disk_detach /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach instance_disk_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks instance_external_ip_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/external-ips instance_list /organizations/{organization_name}/projects/{project_name}/instances +instance_list_v1 /v1/instances instance_migrate /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/migrate +instance_migrate_v1 /v1/instances/{instance}/migrate instance_network_interface_create /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces instance_network_interface_delete /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name} instance_network_interface_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces @@ -42,22 +46,18 @@ instance_network_interface_update /organizations/{organization_name}/proj instance_network_interface_view /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name} instance_network_interface_view_by_id /by-id/network-interfaces/{id} instance_reboot /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/reboot +instance_reboot_v1 /v1/instances/{instance}/reboot instance_serial_console /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console instance_serial_console_stream /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console/stream +instance_serial_console_stream_v1 /v1/instances/{instance}/serial-console/stream +instance_serial_console_v1 /v1/instances/{instance}/serial-console instance_start /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start +instance_start_v1 /v1/instances/{instance}/start instance_stop /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop +instance_stop_v1 /v1/instances/{instance}/stop instance_view /organizations/{organization_name}/projects/{project_name}/instances/{instance_name} instance_view_by_id /by-id/instances/{id} -v1_instance_create /v1/instances -v1_instance_delete /v1/instances/{instance} -v1_instance_list /v1/instances -v1_instance_migrate /v1/instances/{instance}/migrate -v1_instance_reboot /v1/instances/{instance}/reboot -v1_instance_serial_console /v1/instances/{instance}/serial-console -v1_instance_serial_console_stream /v1/instances/{instance}/serial-console/stream -v1_instance_start /v1/instances/{instance}/start -v1_instance_stop /v1/instances/{instance}/stop -v1_instance_view /v1/instances/{instance} +instance_view_v1 /v1/instances/{instance} API operations found with tag "login" OPERATION ID URL PATH diff --git a/openapi/nexus.json b/openapi/nexus.json index 8e730cb17be..5f23b984097 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7659,7 +7659,7 @@ "tags": [ "instances" ], - "operationId": "v1_instance_list", + "operationId": "instance_list_v1", "parameters": [ { "in": "query", @@ -7733,7 +7733,7 @@ "tags": [ "instances" ], - "operationId": "v1_instance_create", + "operationId": "instance_create_v1", "parameters": [ { "in": "query", @@ -7788,7 +7788,7 @@ "tags": [ "instances" ], - "operationId": "v1_instance_view", + "operationId": "instance_view_v1", "parameters": [ { "in": "query", @@ -7840,7 +7840,7 @@ "tags": [ "instances" ], - "operationId": "v1_instance_delete", + "operationId": "instance_delete_v1", "parameters": [ { "in": "query", @@ -7887,7 +7887,7 @@ "tags": [ "instances" ], - "operationId": "v1_instance_migrate", + "operationId": "instance_migrate_v1", "parameters": [ { "in": "query", @@ -7951,7 +7951,7 @@ "tags": [ "instances" ], - "operationId": "v1_instance_reboot", + "operationId": "instance_reboot_v1", "parameters": [ { "in": "query", @@ -8005,7 +8005,7 @@ "tags": [ "instances" ], - "operationId": "v1_instance_serial_console", + "operationId": "instance_serial_console_v1", "parameters": [ { "in": "path", @@ -8095,7 +8095,7 @@ "tags": [ "instances" ], - "operationId": "v1_instance_serial_console_stream", + "operationId": "instance_serial_console_stream_v1", "parameters": [ { "in": "path", @@ -8143,7 +8143,7 @@ "instances" ], "summary": "Boot an instance", - "operationId": "v1_instance_start", + "operationId": "instance_start_v1", "parameters": [ { "in": "query", @@ -8197,7 +8197,7 @@ "tags": [ "instances" ], - "operationId": "v1_instance_stop", + "operationId": "instance_stop_v1", "parameters": [ { "in": "query", From 1596ed297749bb0ed560e2d832fb214cdc37608f Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Dec 2022 16:08:41 -0500 Subject: [PATCH 24/72] Add v1 disk list endpoint --- nexus/src/app/disk.rs | 6 +- nexus/src/external_api/http_entrypoints.rs | 64 +++++++++++++++++++++- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index d8c0f60556b..55b48f99938 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -228,13 +228,11 @@ impl super::Nexus { pub async fn project_list_disks( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_id: &Uuid, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) + .project_id(*project_id) .lookup_for(authz::Action::ListChildren) .await?; self.db_datastore diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 9a73863bd1c..1147525073c 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -135,6 +135,12 @@ pub fn external_api() -> NexusApiDescription { api.register(disk_delete)?; api.register(disk_metrics_list)?; + api.register(disk_list_v1)?; + // api.register(disk_create_v1)?; + // api.register(disk_view_v1)?; + // api.register(disk_delete_v1)?; + // api.register(disk_metrics_list_v1)?; + api.register(instance_list)?; api.register(instance_create)?; api.register(instance_view)?; @@ -1789,6 +1795,50 @@ async fn ip_pool_service_range_remove( // Disks +#[derive(Deserialize, JsonSchema)] +pub struct DiskQueryParams { + #[serde(flatten)] + selector: params::ProjectSelector, + #[serde(flatten)] + pagination: PaginatedByName, +} + +#[endpoint { + method = GET, + path = "/disks", + tags = ["disks"], +}] +async fn disk_list_v1( + rqctx: Arc>>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_id = + nexus.project_lookup_id(&opctx, query.selector).await?; + let disks = nexus + .project_list_disks( + &opctx, + &project_id, + &data_page_params_for(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)), + ) + .await? + .into_iter() + .map(|disk| disk.into()) + .collect(); + Ok(HttpResponseOk(ScanByName::results_page( + &query.pagination, + disks, + &marker_for_name, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// List disks #[endpoint { method = GET, @@ -1808,11 +1858,21 @@ async fn disk_list( let project_name = &path.project_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let project_id = nexus + .project_lookup_id( + &opctx, + params::ProjectSelector { + project: NameOrId::Name(project_name.clone().into()), + organization: Some(NameOrId::Name( + organization_name.clone().into(), + )), + }, + ) + .await?; let disks = nexus .project_list_disks( &opctx, - organization_name, - project_name, + &project_id, &data_page_params_for(&rqctx, &query)? .map_name(|n| Name::ref_cast(n)), ) From 393283d0a21d29b4498ee76ee0fd76ad9a13216c Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Dec 2022 16:16:35 -0500 Subject: [PATCH 25/72] Merge main int rfd-322-poc --- Cargo.lock | 186 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 128 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f2cd138475..9d4bb3ef769 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", "quote", @@ -207,7 +207,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -505,9 +505,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" dependencies = [ "serde", ] @@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.24" +version = "4.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60494cedb60cb47462c0ff7be53de32c0e42a6fc2c772184554fa12bd9489c03" +checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" dependencies = [ - "atty", "bitflags", "clap_derive 4.0.21", "clap_lex 0.3.0", + "is-terminal", "once_cell", "strsim", "termcolor", @@ -1521,9 +1521,9 @@ dependencies = [ [[package]] name = "dtrace-parser" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bbb93fb1a0c517bf20f37caaf5d1f7d20f144c6c35a7d751ecad077b6c042e8" +checksum = "bed110893a7f9f4ceb072e166354a09f6cb4cc416eec5b5e5e8ee367442d434b" dependencies = [ "pest", "pest_derive", @@ -1675,6 +1675,27 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "expectorate" version = "1.0.5" @@ -1948,7 +1969,7 @@ name = "gateway-cli" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.0.24", + "clap 4.0.29", "futures", "gateway-client", "libc", @@ -2205,6 +2226,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -2530,7 +2560,7 @@ name = "internal-dns" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.0.24", + "clap 4.0.29", "dropshot", "expectorate", "internal-dns-client", @@ -2579,6 +2609,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + [[package]] name = "ipconfig" version = "0.3.0" @@ -2606,6 +2646,18 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" +dependencies = [ + "hermit-abi 0.2.6", + "io-lifetimes", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2755,6 +2807,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" + [[package]] name = "lock_api" version = "0.4.7" @@ -3164,7 +3222,7 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", ] @@ -3281,7 +3339,7 @@ name = "omicron-deploy" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.0.24", + "clap 4.0.29", "crossbeam", "omicron-package", "omicron-sled-agent", @@ -3297,7 +3355,7 @@ dependencies = [ name = "omicron-gateway" version = "0.1.0" dependencies = [ - "clap 4.0.24", + "clap 4.0.29", "dropshot", "expectorate", "futures", @@ -3335,7 +3393,7 @@ dependencies = [ "base64", "bb8", "chrono", - "clap 4.0.24", + "clap 4.0.29", "cookie", "criterion", "crucible-agent-client", @@ -3419,7 +3477,7 @@ name = "omicron-package" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.0.24", + "clap 4.0.29", "futures", "hex", "indicatif", @@ -3457,7 +3515,7 @@ dependencies = [ "bytes", "cfg-if 1.0.0", "chrono", - "clap 4.0.24", + "clap 4.0.29", "crucible-agent-client", "crucible-client-types", "ddm-admin-client", @@ -3520,7 +3578,7 @@ name = "omicron-test-utils" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.0.24", + "clap 4.0.29", "dropshot", "expectorate", "futures", @@ -3780,7 +3838,7 @@ dependencies = [ name = "oximeter-collector" version = "0.1.0" dependencies = [ - "clap 4.0.24", + "clap 4.0.29", "dropshot", "expectorate", "internal-dns-client", @@ -3811,7 +3869,7 @@ dependencies = [ "async-trait", "bytes", "chrono", - "clap 4.0.24", + "clap 4.0.29", "dropshot", "itertools", "omicron-test-utils", @@ -4401,7 +4459,7 @@ version = "0.2.1-dev" source = "git+https://github.com/oxidecomputer/progenitor?branch=main#4b5cef4b105f351c74d14acae5e0b4d69313afb2" dependencies = [ "anyhow", - "clap 4.0.24", + "clap 4.0.29", "openapiv3", "progenitor-client 0.2.1-dev (git+https://github.com/oxidecomputer/progenitor?branch=main)", "progenitor-impl 0.2.1-dev (git+https://github.com/oxidecomputer/progenitor?branch=main)", @@ -4416,7 +4474,7 @@ version = "0.2.1-dev" source = "git+https://github.com/oxidecomputer/progenitor#4b5cef4b105f351c74d14acae5e0b4d69313afb2" dependencies = [ "anyhow", - "clap 4.0.24", + "clap 4.0.29", "openapiv3", "progenitor-client 0.2.1-dev (git+https://github.com/oxidecomputer/progenitor)", "progenitor-impl 0.2.1-dev (git+https://github.com/oxidecomputer/progenitor)", @@ -4744,11 +4802,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" dependencies = [ - "autocfg", "crossbeam-deque", "either", "rayon-core", @@ -4756,9 +4813,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -4858,9 +4915,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ "base64", "bytes", @@ -5089,6 +5146,20 @@ dependencies = [ "toolchain_find", ] +[[package]] +name = "rustix" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.42.0", +] + [[package]] name = "rustls" version = "0.20.7" @@ -5398,9 +5469,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -5452,9 +5523,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f2d60d049ea019a84dcd6687b0d1e0030fe663ae105039bdf967ed5e6a9a7" +checksum = "25bf4a5a814902cd1014dbccfa4d4560fb8432c779471e96e035602519f82eef" dependencies = [ "base64", "chrono", @@ -5468,9 +5539,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ccadfacf6cf10faad22bbadf55986bdd0856edfb5d9210aa1dcf1f516e84e93" +checksum = "e3452b4c0f6c1e357f73fdb87cd1efabaa12acf328c7a528e252893baeb3f4aa" dependencies = [ "darling", "proc-macro2", @@ -5506,9 +5577,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -5841,7 +5912,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "clap 4.0.24", + "clap 4.0.29", "dropshot", "futures", "gateway-messages", @@ -6097,9 +6168,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", @@ -6333,9 +6404,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.21.2" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" dependencies = [ "autocfg", "bytes", @@ -6627,9 +6698,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "trybuild" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea496675d71016e9bc76aa42d87f16aefd95447cc5818e671e12b2d7e269075d" +checksum = "db29f438342820400f2d9acfec0d363e987a38b2950bdb50a7069ed17b2148ee" dependencies = [ "glob", "once_cell", @@ -6813,11 +6884,10 @@ dependencies = [ [[package]] name = "usdt" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5409e16f35fdba646ff4290e6f96f0bb958b43e0bd09d790c3714456a76c11ac" +checksum = "39bf7190754941ac252f6fe9c1ff008c09c5fd0292b1732f319900c7fce365d0" dependencies = [ - "dof", "dtrace-parser", "serde", "usdt-attr-macro", @@ -6827,9 +6897,9 @@ dependencies = [ [[package]] name = "usdt-attr-macro" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d203cad87b8424e1fbd0fef29b6df2113e68be5dd75f4043427eb0ce6601041f" +checksum = "f6c80eed594ef75117f363ee2c109b45e13507bdc4729f9d7aea434604ad1777" dependencies = [ "dtrace-parser", "proc-macro2", @@ -6841,9 +6911,9 @@ dependencies = [ [[package]] name = "usdt-impl" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c146394183fcea90d93e2d4602e1a7eb6cab8e807b58f99d4f343720d4e9d69" +checksum = "f7c8b459b1b7997d655cf1bb142551a5b216a6b0e56e51ebd76ecbc0ff5fd1de" dependencies = [ "byteorder", "dof", @@ -6861,9 +6931,9 @@ dependencies = [ [[package]] name = "usdt-macro" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dddab3586eaf539e2daddc425261daef6a6d2f85131bac7345caa8e6a423b27" +checksum = "a13f229fd5cde35ccca2c0151c67a880f3cc13ba2992a05db55b47cc77a5ef3f" dependencies = [ "dtrace-parser", "proc-macro2", @@ -6881,9 +6951,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ "getrandom 0.2.8", "serde", @@ -7099,7 +7169,7 @@ name = "wicket" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.0.24", + "clap 4.0.29", "crossterm", "futures", "hex", @@ -7125,7 +7195,7 @@ dependencies = [ name = "wicketd" version = "0.1.0" dependencies = [ - "clap 4.0.24", + "clap 4.0.29", "dropshot", "expectorate", "futures", From 3d696946baa61dbff18a5190b10be10eaa81ed37 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Dec 2022 17:04:16 -0500 Subject: [PATCH 26/72] Add new disk create endpoint --- nexus/src/app/disk.rs | 6 +-- nexus/src/external_api/http_entrypoints.rs | 54 ++++++++++++++++++---- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 55b48f99938..92f53740937 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -31,13 +31,11 @@ impl super::Nexus { pub async fn project_create_disk( self: &Arc, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_id: &Uuid, params: ¶ms::DiskCreate, ) -> CreateResult { let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) + .project_id(*project_id) .lookup_for(authz::Action::CreateChild) .await?; diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 1147525073c..41d8459805d 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -136,7 +136,7 @@ pub fn external_api() -> NexusApiDescription { api.register(disk_metrics_list)?; api.register(disk_list_v1)?; - // api.register(disk_create_v1)?; + api.register(disk_create_v1)?; // api.register(disk_view_v1)?; // api.register(disk_delete_v1)?; // api.register(disk_metrics_list_v1)?; @@ -1796,7 +1796,7 @@ async fn ip_pool_service_range_remove( // Disks #[derive(Deserialize, JsonSchema)] -pub struct DiskQueryParams { +pub struct DiskListParams { #[serde(flatten)] selector: params::ProjectSelector, #[serde(flatten)] @@ -1810,7 +1810,7 @@ pub struct DiskQueryParams { }] async fn disk_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1889,7 +1889,39 @@ async fn disk_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +pub struct DiskCreateParams { + #[serde(flatten)] + selector: params::ProjectSelector, +} + /// Create a disk +#[endpoint { + method = POST, + path = "/disks", + tags = ["disks"] +}] +async fn disk_create_v1( + rqctx: Arc>>, + query_params: Query, + new_disk: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let new_disk_params = &new_disk.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_id = + nexus.project_lookup_id(&opctx, query.selector).await?; + let disk = nexus + .project_create_disk(&opctx, &project_id, new_disk_params) + .await?; + Ok(HttpResponseCreated(disk.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + // TODO-correctness See note about instance create. This should be async. #[endpoint { method = POST, @@ -1909,14 +1941,20 @@ async fn disk_create( let new_disk_params = &new_disk.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk = nexus - .project_create_disk( + let project_id = nexus + .project_lookup_id( &opctx, - &organization_name, - &project_name, - &new_disk_params, + params::ProjectSelector { + project: NameOrId::Name(project_name.clone().into()), + organization: Some(NameOrId::Name( + organization_name.clone().into(), + )), + }, ) .await?; + let disk = nexus + .project_create_disk(&opctx, &project_id, &new_disk_params) + .await?; Ok(HttpResponseCreated(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await From 57549c74917a68123b1dfc2aaf1b0dceb478bf0c Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Dec 2022 13:07:53 -0500 Subject: [PATCH 27/72] Add disk view --- nexus/src/app/disk.rs | 84 +++++++++++++++++----- nexus/src/external_api/http_entrypoints.rs | 83 ++++++++++++++++++--- nexus/types/src/external_api/params.rs | 22 ++++++ 3 files changed, 163 insertions(+), 26 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 92f53740937..78ce8a95335 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -20,6 +20,7 @@ use omicron_common::api::external::Error; use omicron_common::api::external::InternalContext; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; use omicron_common::api::internal::nexus::DiskRuntimeState; use sled_agent_client::Client as SledAgentClient; use std::sync::Arc; @@ -27,6 +28,73 @@ use uuid::Uuid; impl super::Nexus { // Disks + pub async fn disk_lookup_id( + &self, + opctx: &OpContext, + disk_selector: params::DiskSelector, + ) -> LookupResult { + match disk_selector { + params::DiskSelector { disk: NameOrId::Id(id), .. } => { + let (.., authz_disk) = + LookupPath::new(opctx, &self.db_datastore) + .disk_id(id) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_disk.id()) + } + params::DiskSelector { + disk: NameOrId::Name(disk_name), + project: Some(NameOrId::Id(project_id)), + .. + } => { + let (.., authz_disk) = + LookupPath::new(opctx, &self.db_datastore) + .project_id(project_id) + .disk_name(&Name(disk_name.clone())) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_disk.id()) + } + params::DiskSelector { + disk: NameOrId::Name(disk_name), + project: Some(NameOrId::Name(project_name)), + organization: Some(NameOrId::Id(organization_id)), + } => { + let (.., authz_disk) = + LookupPath::new(opctx, &self.db_datastore) + .organization_id(organization_id) + .project_name(&Name(project_name.clone())) + .disk_name(&Name(disk_name.clone())) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_disk.id()) + } + params::DiskSelector { + disk: NameOrId::Name(disk_name), + project: Some(NameOrId::Name(project_name)), + organization: Some(NameOrId::Name(organization_name)), + } => { + let (.., authz_disk) = + LookupPath::new(opctx, &self.db_datastore) + .organization_name(&Name(organization_name.clone())) + .project_name(&Name(project_name.clone())) + .disk_name(&Name(disk_name.clone())) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_disk.id()) + } + _ => Err(Error::InvalidRequest { + message: " + Unable to resolve disk. Expected one of + - disk: Uuid + - disk: Name, project: Uuid + - disk: Name, project: Name, organization: Uuid + - disk: Name, project: Name, organization: Name + " + .to_string(), + }), + } + } pub async fn project_create_disk( self: &Arc, @@ -239,22 +307,6 @@ impl super::Nexus { } pub async fn disk_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - disk_name: &Name, - ) -> LookupResult { - let (.., db_disk) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .disk_name(disk_name) - .fetch() - .await?; - Ok(db_disk) - } - - pub async fn disk_fetch_by_id( &self, opctx: &OpContext, disk_id: &Uuid, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 41d8459805d..3ab5491fe38 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -137,7 +137,7 @@ pub fn external_api() -> NexusApiDescription { api.register(disk_list_v1)?; api.register(disk_create_v1)?; - // api.register(disk_view_v1)?; + api.register(disk_view_v1)?; // api.register(disk_delete_v1)?; // api.register(disk_metrics_list_v1)?; @@ -1960,6 +1960,52 @@ async fn disk_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Path parameters for Disk requests +#[derive(Deserialize, JsonSchema)] +struct DiskLookupPathParam { + /// If Name is used to reference the disk 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 + disk: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +struct DiskViewParams { + #[serde(flatten)] + selector: Option, +} + +#[endpoint { + method = GET, + path = "/disks/{disk_name}", + tags = ["disks"] +}] +async fn disk_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 disk_id = nexus + .disk_lookup_id( + &opctx, + params::DiskSelector::new(path.disk, &query.selector), + ) + .await?; + let disk = nexus.disk_fetch(&opctx, &disk_id).await?; + Ok(HttpResponseOk(disk.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Disk requests #[derive(Deserialize, JsonSchema)] struct DiskPathParam { @@ -1986,9 +2032,19 @@ async fn disk_view( let disk_name = &path.disk_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk = nexus - .disk_fetch(&opctx, &organization_name, &project_name, &disk_name) + let disk_id = nexus + .disk_lookup_id( + &opctx, + params::DiskSelector { + disk: NameOrId::Name(disk_name.clone().into()), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name( + organization_name.clone().into(), + )), + }, + ) .await?; + let disk = nexus.disk_fetch(&opctx, &disk_id).await?; Ok(HttpResponseOk(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2010,7 +2066,7 @@ async fn disk_view_by_id( let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk = nexus.disk_fetch_by_id(&opctx, id).await?; + let disk = nexus.disk_fetch(&opctx, id).await?; Ok(HttpResponseOk(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2086,16 +2142,23 @@ async fn disk_metrics_list( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - - // This ensures the user is authorized on Action::Read for this disk - let disk = nexus - .disk_fetch(&opctx, organization_name, project_name, disk_name) + let disk_id = nexus + .disk_lookup_id( + &opctx, + params::DiskSelector { + disk: NameOrId::Name(disk_name.clone().into()), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name( + organization_name.clone().into(), + )), + }, + ) .await?; - let upstairs_uuid = disk.id(); + let result = nexus .select_timeseries( &format!("crucible_upstairs:{}", metric_name), - &[&format!("upstairs_uuid=={}", upstairs_uuid)], + &[&format!("upstairs_uuid=={}", disk_id)], query, limit, ) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index dd57a3935dc..3a59c9d318e 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -46,6 +46,28 @@ impl InstanceSelector { } } +#[derive(Deserialize, JsonSchema)] +pub struct DiskSelector { + pub disk: NameOrId, + pub project: Option, + pub organization: Option, +} + +impl DiskSelector { + pub fn new( + disk: NameOrId, + project_selector: &Option, + ) -> DiskSelector { + DiskSelector { + disk, + organization: project_selector + .as_ref() + .and_then(|s| s.organization.clone()), + project: project_selector.as_ref().map(|s| s.project.clone()), + } + } +} + // Silos /// Create-time parameters for a [`Silo`](crate::external_api::views::Silo) From 2a87558bfdc64304a80d30e1dabf6027da68d337 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Dec 2022 13:31:05 -0500 Subject: [PATCH 28/72] Always do a lookup, even when only handling UUID --- nexus/src/app/instance.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 1289f9183c4..8477cabb924 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -62,7 +62,12 @@ impl super::Nexus { match instance_selector { params::InstanceSelector { instance: NameOrId::Id(id), .. } => { // TODO: 400 if project or organization are present - Ok(id) + let (.., authz_instance) = + LookupPath::new(opctx, &self.db_datastore) + .instance_id(id) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_instance.id()) } params::InstanceSelector { instance: NameOrId::Name(instance_name), From df8f5bfc07cd9da359c16c6af5b4cadd2b6fcd68 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Dec 2022 15:21:22 -0500 Subject: [PATCH 29/72] Add disk delete --- nexus/src/app/disk.rs | 8 +--- nexus/src/external_api/http_entrypoints.rs | 45 +++++++++++++++++++--- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 78ce8a95335..e704e44cf43 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -421,14 +421,10 @@ impl super::Nexus { pub async fn project_delete_disk( self: &Arc, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - disk_name: &Name, + disk_id: &Uuid, ) -> DeleteResult { let (.., authz_disk) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .disk_name(disk_name) + .disk_id(disk_id) .lookup_for(authz::Action::Delete) .await?; diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 3ab5491fe38..72f4d7d0466 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -138,7 +138,7 @@ pub fn external_api() -> NexusApiDescription { api.register(disk_list_v1)?; api.register(disk_create_v1)?; api.register(disk_view_v1)?; - // api.register(disk_delete_v1)?; + api.register(disk_delete_v1)?; // api.register(disk_metrics_list_v1)?; api.register(instance_list)?; @@ -2073,6 +2073,34 @@ async fn disk_view_by_id( } /// Delete a disk +#[endpoint { + method = DELETE, + path = "/disks/{disk}", + tags = ["disks"], +}] +async fn disk_delete_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 disk_id = nexus + .disk_lookup_id( + &opctx, + params::DiskSelector::new(path.disk, &query.selector), + ) + .await?; + nexus.project_delete_disk(&opctx, &disk_id).await?; + Ok(HttpResponseOk(())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + #[endpoint { method = DELETE, path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}", @@ -2090,14 +2118,19 @@ async fn disk_delete( let disk_name = &path.disk_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus - .project_delete_disk( + let disk_id = nexus + .disk_lookup_id( &opctx, - &organization_name, - &project_name, - &disk_name, + params::DiskSelector { + disk: NameOrId::Name(disk_name.clone().into()), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name( + organization_name.clone().into(), + )), + }, ) .await?; + nexus.project_delete_disk(&opctx, &disk_id).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await From 548a4d0b9486d514df08d8579c4c75a2e1e54ae6 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 5 Dec 2022 15:40:08 -0500 Subject: [PATCH 30/72] Add disk metrics --- nexus/src/app/disk.rs | 2 +- nexus/src/external_api/http_entrypoints.rs | 53 +++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index e704e44cf43..0228faccbdc 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -424,7 +424,7 @@ impl super::Nexus { disk_id: &Uuid, ) -> DeleteResult { let (.., authz_disk) = LookupPath::new(opctx, &self.db_datastore) - .disk_id(disk_id) + .disk_id(*disk_id) .lookup_for(authz::Action::Delete) .await?; diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 72f4d7d0466..8910c19cc7a 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -139,7 +139,7 @@ pub fn external_api() -> NexusApiDescription { api.register(disk_create_v1)?; api.register(disk_view_v1)?; api.register(disk_delete_v1)?; - // api.register(disk_metrics_list_v1)?; + api.register(disk_metrics_list_v1)?; api.register(instance_list)?; api.register(instance_create)?; @@ -2148,6 +2148,57 @@ pub enum DiskMetricName { WriteBytes, } +#[derive(Deserialize, JsonSchema)] +pub struct DiskMetricsPaginationParams { + #[serde(flatten)] + pagination: + PaginationParams, + + #[serde(flatten)] + selector: Option, +} + +#[endpoint { + method = GET, + path = "/disks/{disk}/metrics/{metric_name}", + tags = ["disks"], +}] +async fn disk_metrics_list_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 metric_name = path.metric_name; + let query = query_params.into_inner(); + let limit = rqctx.page_limit(&query.pagination)?; + + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let disk_id = nexus + .disk_lookup_id( + &opctx, + params::DiskSelector::new(path.inner.disk, &query.selector), + ) + .await?; + + let result = nexus + .select_timeseries( + &format!("crucible_upstairs:{}", metric_name), + &[&format!("upstairs_uuid=={}", disk_id)], + query.pagination, + limit, + ) + .await?; + + Ok(HttpResponseOk(result)) + }; + + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Fetch disk metrics #[endpoint { method = GET, From ce7da01f346ebb33c1988b8c9a9b740c0781ac42 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 6 Dec 2022 11:08:22 -0500 Subject: [PATCH 31/72] Fix clippy failures --- nexus/src/external_api/http_entrypoints.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 9a73863bd1c..24e1f784af8 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2663,14 +2663,14 @@ async fn instance_serial_console_v1( params::InstanceSelector::new(path.instance, &query.selector), ) .await?; - let console = nexus + let data = nexus .instance_serial_console_data( &opctx, &instance_id, &query.console_params, ) .await?; - Ok(HttpResponseOk(console.into())) + Ok(HttpResponseOk(data)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } From 96165f55a1deb1b3f19b48e333c2129cdd42a302 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 6 Dec 2022 15:45:02 -0500 Subject: [PATCH 32/72] Update unauthz coverage --- nexus/src/external_api/http_entrypoints.rs | 2 +- nexus/tests/integration_tests/endpoints.rs | 30 +++++++++++-------- nexus/tests/integration_tests/unauthorized.rs | 2 +- .../unauthorized_coverage.rs | 17 +++++++++-- .../output/uncovered-authz-endpoints.txt | 12 ++++++++ openapi/nexus.json | 2 +- 6 files changed, 47 insertions(+), 18 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 24e1f784af8..d50195bcd6f 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2642,7 +2642,7 @@ pub struct InstanceSerialConsoleParams { } #[endpoint { - method = POST, + method = GET, path = "/v1/instances/{instance}/serial-console", tags = ["instances"], }] diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 6dfb59200d7..b11218c39c0 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -104,6 +104,8 @@ lazy_static! { 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); + 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); pub static ref DEMO_PROJECT_URL_DISKS: String = @@ -111,7 +113,7 @@ lazy_static! { pub static ref DEMO_PROJECT_URL_IMAGES: String = format!("{}/images", *DEMO_PROJECT_URL); pub static ref DEMO_PROJECT_URL_INSTANCES: String = - format!("{}/instances", *DEMO_PROJECT_URL); + "/v1/instances".to_string(); pub static ref DEMO_PROJECT_URL_SNAPSHOTS: String = format!("{}/snapshots", *DEMO_PROJECT_URL); pub static ref DEMO_PROJECT_URL_VPCS: String = @@ -224,29 +226,31 @@ lazy_static! { // Instance used for testing pub static ref DEMO_INSTANCE_NAME: Name = "demo-instance".parse().unwrap(); pub static ref DEMO_INSTANCE_URL: String = - format!("{}/{}", *DEMO_PROJECT_URL_INSTANCES, *DEMO_INSTANCE_NAME); + format!("/v1/instances/{}?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_INSTANCE_START_URL: String = - format!("{}/start", *DEMO_INSTANCE_URL); + format!("/v1/instances/{}/start?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_INSTANCE_STOP_URL: String = - format!("{}/stop", *DEMO_INSTANCE_URL); + format!("/v1/instances/{}/stop?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_INSTANCE_REBOOT_URL: String = - format!("{}/reboot", *DEMO_INSTANCE_URL); + format!("/v1/instances/{}/reboot?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_INSTANCE_MIGRATE_URL: String = - format!("{}/migrate", *DEMO_INSTANCE_URL); + format!("/v1/instances/{}/migrate?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + pub static ref DEMO_INSTANCE_SERIAL_URL: String = + format!("/v1/instances/{}/serial-console?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + pub static ref DEMO_INSTANCE_SERIAL_STREAM_URL: String = + format!("/v1/instances/{}/serial-console/stream?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + + // To be migrated... pub static ref DEMO_INSTANCE_DISKS_URL: String = - format!("{}/disks", *DEMO_INSTANCE_URL); + format!("/organizations/{}/projects/{}/instances/{}/disks", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); pub static ref DEMO_INSTANCE_DISKS_ATTACH_URL: String = format!("{}/attach", *DEMO_INSTANCE_DISKS_URL); pub static ref DEMO_INSTANCE_DISKS_DETACH_URL: String = format!("{}/detach", *DEMO_INSTANCE_DISKS_URL); pub static ref DEMO_INSTANCE_NICS_URL: String = - format!("{}/network-interfaces", *DEMO_INSTANCE_URL); + format!("/organizations/{}/projects/{}/instances/{}/network-interfaces", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); pub static ref DEMO_INSTANCE_EXTERNAL_IPS_URL: String = - format!("{}/external-ips", *DEMO_INSTANCE_URL); - pub static ref DEMO_INSTANCE_SERIAL_URL: String = - format!("{}/serial-console", *DEMO_INSTANCE_URL); - pub static ref DEMO_INSTANCE_SERIAL_STREAM_URL: String = - format!("{}/serial-console/stream", *DEMO_INSTANCE_URL); + format!("/organizations/{}/projects/{}/instances/{}/external-ips", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); pub static ref DEMO_INSTANCE_CREATE: params::InstanceCreate = params::InstanceCreate { identity: IdentityMetadataCreateParams { diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 134256a6ce9..7f6a6c300f1 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -258,7 +258,7 @@ lazy_static! { SetupReq::Post { url: &*DEMO_PROJECT_URL_INSTANCES, body: serde_json::to_value(&*DEMO_INSTANCE_CREATE).unwrap(), - id_routes: vec!["/by-id/instances/{id}"], + id_routes: vec!["/v1/instances/{id}"], }, // Lookup the previously created NIC SetupReq::Get { diff --git a/nexus/tests/integration_tests/unauthorized_coverage.rs b/nexus/tests/integration_tests/unauthorized_coverage.rs index bbaebe6f79d..64ce7eabc82 100644 --- a/nexus/tests/integration_tests/unauthorized_coverage.rs +++ b/nexus/tests/integration_tests/unauthorized_coverage.rs @@ -86,7 +86,7 @@ fn test_unauthorized_coverage() { let method_string = m.http_method().to_string().to_uppercase(); let found = spec_operations.iter().find(|(op, regex)| { op.method.to_uppercase() == method_string - && regex.is_match(v.url) + && regex.is_match(v.url.split('?').next().unwrap_or(v.url)) }); if let Some((op, _)) = found { println!( @@ -138,7 +138,20 @@ fn test_unauthorized_coverage() { let expected_uncovered_endpoints = std::fs::read_to_string("tests/output/uncovered-authz-endpoints.txt") .expect("failed to load file of allowed uncovered endpoints"); - assert_eq!(expected_uncovered_endpoints, 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 + ) } #[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 3f161678dee..810d372e96d 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -7,3 +7,15 @@ 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") +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") diff --git a/openapi/nexus.json b/openapi/nexus.json index 5f23b984097..e6a4b225d1d 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8001,7 +8001,7 @@ } }, "/v1/instances/{instance}/serial-console": { - "post": { + "get": { "tags": [ "instances" ], From e616c0352d80263ac339629e52afae605ee67f65 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 6 Dec 2022 16:34:31 -0500 Subject: [PATCH 33/72] Fix up some failed testcases --- nexus/tests/integration_tests/endpoints.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index b11218c39c0..340515d4f21 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -112,8 +112,7 @@ lazy_static! { format!("{}/disks", *DEMO_PROJECT_URL); pub static ref DEMO_PROJECT_URL_IMAGES: String = format!("{}/images", *DEMO_PROJECT_URL); - pub static ref DEMO_PROJECT_URL_INSTANCES: String = - "/v1/instances".to_string(); + 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); pub static ref DEMO_PROJECT_URL_VPCS: String = @@ -1285,11 +1284,12 @@ lazy_static! { }, VerifyEndpoint { - url: "/by-id/instances/{id}", + url: "/v1/instances/{id}", visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ AllowedMethod::Get, + AllowedMethod::Delete, ], }, From 3c13edcfb53820d61c824d094996503e2ffdb46e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 6 Dec 2022 21:20:09 -0500 Subject: [PATCH 34/72] Add view by id to skip list --- nexus/tests/integration_tests/endpoints.rs | 10 ---------- nexus/tests/output/uncovered-authz-endpoints.txt | 1 + 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 340515d4f21..6051eeaf7a0 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -1283,16 +1283,6 @@ lazy_static! { ], }, - VerifyEndpoint { - url: "/v1/instances/{id}", - visibility: Visibility::Protected, - unprivileged_access: UnprivilegedAccess::None, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Delete, - ], - }, - VerifyEndpoint { url: &*DEMO_INSTANCE_URL, visibility: Visibility::Protected, diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index 810d372e96d..70576c0b21c 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -19,3 +19,4 @@ instance_migrate (post "/organizations/{organization_n instance_reboot (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/reboot") instance_start (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start") instance_stop (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop") +instance_view_by_id (get "/by-id/instances/{id}") \ No newline at end of file From 12bc36552c2cdf90b3751a0f6c28a55c62001df2 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 7 Dec 2022 13:36:17 -0500 Subject: [PATCH 35/72] Update lookups to return authz refs instead of uuids --- nexus/src/app/instance.rs | 64 +++++------ nexus/src/app/project.rs | 15 ++- nexus/src/external_api/http_entrypoints.rs | 125 +++++++++++---------- 3 files changed, 103 insertions(+), 101 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 906855e5e2b..6a767408ddf 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -54,11 +54,11 @@ use uuid::Uuid; const MAX_KEYS_PER_INSTANCE: u32 = 8; impl super::Nexus { - pub async fn instance_lookup_id( + pub async fn instance_lookup( &self, opctx: &OpContext, instance_selector: params::InstanceSelector, - ) -> LookupResult { + ) -> LookupResult { match instance_selector { params::InstanceSelector { instance: NameOrId::Id(id), .. } => { // TODO: 400 if project or organization are present @@ -67,7 +67,7 @@ impl super::Nexus { .instance_id(id) .lookup_for(authz::Action::Read) .await?; - Ok(authz_instance.id()) + Ok(authz_instance) } params::InstanceSelector { instance: NameOrId::Name(instance_name), @@ -81,7 +81,7 @@ impl super::Nexus { .instance_name(&Name(instance_name.clone())) .lookup_for(authz::Action::Read) .await?; - Ok(authz_instance.id()) + Ok(authz_instance) } params::InstanceSelector { instance: NameOrId::Name(instance_name), @@ -95,7 +95,7 @@ impl super::Nexus { .instance_name(&Name(instance_name.clone())) .lookup_for(authz::Action::Read) .await?; - Ok(authz_instance.id()) + Ok(authz_instance) } params::InstanceSelector { instance: NameOrId::Name(instance_name), @@ -109,7 +109,7 @@ impl super::Nexus { .instance_name(&Name(instance_name.clone())) .lookup_for(authz::Action::Read) .await?; - Ok(authz_instance.id()) + Ok(authz_instance) } // TODO: Add a better error message _ => Err(Error::InvalidRequest { @@ -128,13 +128,10 @@ impl super::Nexus { pub async fn project_create_instance( self: &Arc, opctx: &OpContext, - project_id: &Uuid, + authz_project: &authz::Project, params: ¶ms::InstanceCreate, ) -> CreateResult { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .project_id(*project_id) - .lookup_for(authz::Action::CreateChild) - .await?; + opctx.authorize(authz::Action::CreateChild, authz_project).await?; // Validate parameters if params.disks.len() > MAX_DISKS_PER_INSTANCE as usize { @@ -263,13 +260,10 @@ impl super::Nexus { pub async fn project_list_instances( &self, opctx: &OpContext, - project_id: Uuid, + authz_project: &authz::Project, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .project_id(project_id) - .lookup_for(authz::Action::ListChildren) - .await?; + opctx.authorize(authz::Action::ListChildren, authz_project).await?; self.db_datastore .project_list_instances(opctx, &authz_project, pagparams) .await @@ -278,10 +272,10 @@ impl super::Nexus { pub async fn instance_fetch( &self, opctx: &OpContext, - instance_id: &Uuid, + authz_instance: &authz::Instance, ) -> LookupResult { let (.., db_instance) = LookupPath::new(opctx, &self.db_datastore) - .instance_id(*instance_id) + .instance_id(authz_instance.id()) .fetch() .await?; Ok(db_instance) @@ -293,15 +287,12 @@ impl super::Nexus { pub async fn project_destroy_instance( &self, opctx: &OpContext, - instance_id: &Uuid, + authz_instance: &authz::Instance, ) -> DeleteResult { // TODO-robustness We need to figure out what to do with Destroyed // instances? Presumably we need to clean them up at some point, but // not right away so that callers can see that they've been destroyed. - let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) - .instance_id(*instance_id) - .lookup_for(authz::Action::Delete) - .await?; + opctx.authorize(authz::Action::Delete, authz_instance).await?; self.db_datastore .project_delete_instance(opctx, &authz_instance) @@ -319,13 +310,10 @@ impl super::Nexus { pub async fn project_instance_migrate( self: &Arc, opctx: &OpContext, - instance_id: Uuid, + authz_instance: &authz::Instance, params: params::InstanceMigrate, ) -> UpdateResult { - let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) - .instance_id(instance_id) - .lookup_for(authz::Action::Modify) - .await?; + opctx.authorize(authz::Action::Modify, authz_instance).await?; // Kick off the migration saga let saga_params = sagas::instance_migrate::Params { @@ -379,7 +367,7 @@ impl super::Nexus { pub async fn instance_reboot( &self, opctx: &OpContext, - instance_id: Uuid, + authz_instance: &authz::Instance, ) -> UpdateResult { // To implement reboot, we issue a call to the sled agent to set a // runtime state of "reboot". We cannot simply stop the Instance and @@ -394,7 +382,7 @@ impl super::Nexus { // running. let (.., authz_instance, db_instance) = LookupPath::new(opctx, &self.db_datastore) - .instance_id(instance_id) + .instance_id(authz_instance.id()) .fetch() .await?; let requested = InstanceRuntimeStateRequested { @@ -415,11 +403,11 @@ impl super::Nexus { pub async fn instance_start( &self, opctx: &OpContext, - instance_id: Uuid, + authz_instance: &authz::Instance, ) -> UpdateResult { let (.., authz_instance, db_instance) = LookupPath::new(opctx, &self.db_datastore) - .instance_id(instance_id) + .instance_id(authz_instance.id()) .fetch() .await?; let requested = InstanceRuntimeStateRequested { @@ -440,11 +428,11 @@ impl super::Nexus { pub async fn instance_stop( &self, opctx: &OpContext, - instance_id: Uuid, + authz_instance: &authz::Instance, ) -> UpdateResult { let (.., authz_instance, db_instance) = LookupPath::new(opctx, &self.db_datastore) - .instance_id(instance_id) + .instance_id(authz_instance.id()) .fetch() .await?; let requested = InstanceRuntimeStateRequested { @@ -1139,10 +1127,10 @@ impl super::Nexus { pub(crate) async fn instance_serial_console_data( &self, opctx: &OpContext, - instance_id: &Uuid, + authz_instance: &authz::Instance, params: ¶ms::InstanceSerialConsoleRequest, ) -> Result { - let db_instance = self.instance_fetch(opctx, instance_id).await?; + let db_instance = self.instance_fetch(opctx, authz_instance).await?; let sa = self.instance_sled(&db_instance).await?; let data = sa @@ -1166,9 +1154,9 @@ impl super::Nexus { &self, opctx: &OpContext, conn: dropshot::WebsocketConnection, - instance_id: &Uuid, + authz_instance: &authz::Instance, ) -> Result<(), Error> { - let instance = self.instance_fetch(opctx, instance_id).await?; + let instance = self.instance_fetch(opctx, authz_instance).await?; let ip_addr = instance .runtime_state .propolis_ip diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index e5178a6537e..917fc93dfb6 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -25,15 +25,20 @@ use omicron_common::api::external::UpdateResult; use uuid::Uuid; impl super::Nexus { - pub async fn project_lookup_id( + pub async fn project_lookup( &self, opctx: &OpContext, project_selector: params::ProjectSelector, - ) -> LookupResult { + ) -> LookupResult { match project_selector { params::ProjectSelector { project: NameOrId::Id(id), .. } => { // TODO: 400 if organization is present - Ok(id) + let (.., authz_project) = + LookupPath::new(opctx, &self.db_datastore) + .project_id(id) + .lookup_for(authz::Action::Read) + .await?; + Ok(authz_project) } params::ProjectSelector { project: NameOrId::Name(project_name), @@ -45,7 +50,7 @@ impl super::Nexus { .project_name(&Name(project_name)) .lookup_for(authz::Action::Read) .await?; - Ok(authz_project.id()) + Ok(authz_project) } params::ProjectSelector { project: NameOrId::Name(project_name), @@ -57,7 +62,7 @@ impl super::Nexus { .project_name(&Name(project_name)) .lookup_for(authz::Action::Read) .await?; - Ok(authz_project.id()) + Ok(authz_project) } _ => Err(Error::InvalidRequest { message: " diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index d50195bcd6f..8da76a36770 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2032,12 +2032,12 @@ async fn instance_list_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_id = - nexus.project_lookup_id(&opctx, query.selector).await?; + let authz_project = + nexus.project_lookup(&opctx, query.selector).await?; let instances = nexus .project_list_instances( &opctx, - project_id, + &authz_project, &data_page_params_for(&rqctx, &query.pagination)? .map_name(|n| Name::ref_cast(n)), ) @@ -2073,8 +2073,8 @@ async fn instance_list( let project_name = &path.project_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_id = nexus - .project_lookup_id( + let authz_project = nexus + .project_lookup( &opctx, params::ProjectSelector { project: NameOrId::Name(project_name.clone().into()), @@ -2087,7 +2087,7 @@ async fn instance_list( let instances = nexus .project_list_instances( &opctx, - project_id, + &authz_project, &data_page_params_for(&rqctx, &query)? .map_name(|n| Name::ref_cast(n)), ) @@ -2126,8 +2126,7 @@ 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_id = - nexus.project_lookup_id(&opctx, query.selector).await?; + let project_id = nexus.project_lookup(&opctx, query.selector).await?; let instance = nexus .project_create_instance(&opctx, &project_id, &new_instance_params) .await?; @@ -2163,7 +2162,7 @@ async fn instance_create( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_id = nexus - .project_lookup_id( + .project_lookup( &opctx, params::ProjectSelector { project: NameOrId::Name(project_name.clone().into()), @@ -2215,13 +2214,13 @@ async fn instance_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector::new(path.instance, &query.selector), ) .await?; - let instance = nexus.instance_fetch(&opctx, &instance_id).await?; + let instance = nexus.instance_fetch(&opctx, &authz_instance).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2253,8 +2252,8 @@ async fn instance_view( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector { instance: NameOrId::Name(instance_name.clone().into()), @@ -2265,7 +2264,7 @@ async fn instance_view( }, ) .await?; - let instance = nexus.instance_fetch(&opctx, &instance_id).await?; + let instance = nexus.instance_fetch(&opctx, &authz_instance).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2287,7 +2286,17 @@ async fn instance_view_by_id( let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance = nexus.instance_fetch(&opctx, id).await?; + let authz_instance = nexus + .instance_lookup( + &opctx, + params::InstanceSelector { + instance: NameOrId::Id(*id), + project: None, + organization: None, + }, + ) + .await?; + let instance = nexus.instance_fetch(&opctx, &authz_instance).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2309,13 +2318,13 @@ async fn instance_delete_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector::new(path.instance, &query.selector), ) .await?; - nexus.project_destroy_instance(&opctx, &instance_id).await?; + nexus.project_destroy_instance(&opctx, &authz_instance).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2339,8 +2348,8 @@ async fn instance_delete( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector { instance: NameOrId::Name(instance_name.clone().into()), @@ -2351,7 +2360,7 @@ async fn instance_delete( }, ) .await?; - nexus.project_destroy_instance(&opctx, &instance_id).await?; + nexus.project_destroy_instance(&opctx, &authz_instance).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2376,8 +2385,8 @@ async fn instance_migrate_v1( let migrate_instance_params = migrate_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector::new(path.instance, &query.selector), ) @@ -2385,7 +2394,7 @@ async fn instance_migrate_v1( let instance = nexus .project_instance_migrate( &opctx, - instance_id, + &authz_instance, migrate_instance_params, ) .await?; @@ -2415,8 +2424,8 @@ async fn instance_migrate( let migrate_instance_params = migrate_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector { instance: NameOrId::Name(instance_name.clone().into()), @@ -2430,7 +2439,7 @@ async fn instance_migrate( let instance = nexus .project_instance_migrate( &opctx, - instance_id, + &authz_instance, migrate_instance_params, ) .await?; @@ -2455,13 +2464,13 @@ async fn instance_reboot_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector::new(path.instance, &query.selector), ) .await?; - let instance = nexus.instance_reboot(&opctx, instance_id).await?; + let instance = nexus.instance_reboot(&opctx, &authz_instance).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2485,8 +2494,8 @@ async fn instance_reboot( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector { instance: NameOrId::Name(instance_name.clone().into()), @@ -2497,7 +2506,7 @@ async fn instance_reboot( }, ) .await?; - let instance = nexus.instance_reboot(&opctx, instance_id).await?; + let instance = nexus.instance_reboot(&opctx, &authz_instance).await?; Ok(HttpResponseAccepted(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2520,13 +2529,13 @@ async fn instance_start_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector::new(path.instance, &query.selector), ) .await?; - let instance = nexus.instance_start(&opctx, instance_id).await?; + let instance = nexus.instance_start(&opctx, &authz_instance).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2550,8 +2559,8 @@ async fn instance_start( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector { instance: NameOrId::Name(instance_name.clone().into()), @@ -2562,7 +2571,7 @@ async fn instance_start( }, ) .await?; - let instance = nexus.instance_start(&opctx, instance_id).await?; + let instance = nexus.instance_start(&opctx, &authz_instance).await?; Ok(HttpResponseAccepted(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2584,13 +2593,13 @@ async fn instance_stop_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector::new(path.instance, &query.selector), ) .await?; - let instance = nexus.instance_stop(&opctx, instance_id).await?; + let instance = nexus.instance_stop(&opctx, &authz_instance).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2614,8 +2623,8 @@ async fn instance_stop( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector { instance: NameOrId::Name(instance_name.clone().into()), @@ -2626,7 +2635,7 @@ async fn instance_stop( }, ) .await?; - let instance = nexus.instance_stop(&opctx, instance_id).await?; + let instance = nexus.instance_stop(&opctx, &authz_instance).await?; Ok(HttpResponseAccepted(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2657,8 +2666,8 @@ async fn instance_serial_console_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector::new(path.instance, &query.selector), ) @@ -2666,7 +2675,7 @@ async fn instance_serial_console_v1( let data = nexus .instance_serial_console_data( &opctx, - &instance_id, + &authz_instance, &query.console_params, ) .await?; @@ -2694,8 +2703,8 @@ async fn instance_serial_console( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector { instance: NameOrId::Name(instance_name.clone().into()), @@ -2709,7 +2718,7 @@ async fn instance_serial_console( let data = nexus .instance_serial_console_data( &opctx, - &instance_id, + &authz_instance, &query_params.into_inner(), ) .await?; @@ -2734,13 +2743,13 @@ 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_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector::new(path.instance, &query.selector), ) .await?; - nexus.instance_serial_console_stream(&opctx, conn, &instance_id).await?; + nexus.instance_serial_console_stream(&opctx, conn, &authz_instance).await?; Ok(()) } @@ -2762,8 +2771,8 @@ async fn instance_serial_console_stream( let project_name = &path.project_name; let instance_name = &path.instance_name; let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_id = nexus - .instance_lookup_id( + let authz_instance = nexus + .instance_lookup( &opctx, params::InstanceSelector { instance: NameOrId::Name(instance_name.clone().into()), @@ -2774,7 +2783,7 @@ async fn instance_serial_console_stream( }, ) .await?; - nexus.instance_serial_console_stream(&opctx, conn, &instance_id).await?; + nexus.instance_serial_console_stream(&opctx, conn, &authz_instance).await?; Ok(()) } From c94d0d941b2b23219324d499ee6b593c653ee41a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 7 Dec 2022 14:09:57 -0500 Subject: [PATCH 36/72] Update disk lookups to use authz objects --- nexus/src/app/disk.rs | 38 +++++--------- nexus/src/external_api/http_entrypoints.rs | 60 ++++++++++++---------- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 0228faccbdc..52c02532ba1 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -28,11 +28,11 @@ use uuid::Uuid; impl super::Nexus { // Disks - pub async fn disk_lookup_id( + pub async fn disk_lookup( &self, opctx: &OpContext, disk_selector: params::DiskSelector, - ) -> LookupResult { + ) -> LookupResult { match disk_selector { params::DiskSelector { disk: NameOrId::Id(id), .. } => { let (.., authz_disk) = @@ -40,7 +40,7 @@ impl super::Nexus { .disk_id(id) .lookup_for(authz::Action::Read) .await?; - Ok(authz_disk.id()) + Ok(authz_disk) } params::DiskSelector { disk: NameOrId::Name(disk_name), @@ -53,7 +53,7 @@ impl super::Nexus { .disk_name(&Name(disk_name.clone())) .lookup_for(authz::Action::Read) .await?; - Ok(authz_disk.id()) + Ok(authz_disk) } params::DiskSelector { disk: NameOrId::Name(disk_name), @@ -67,7 +67,7 @@ impl super::Nexus { .disk_name(&Name(disk_name.clone())) .lookup_for(authz::Action::Read) .await?; - Ok(authz_disk.id()) + Ok(authz_disk) } params::DiskSelector { disk: NameOrId::Name(disk_name), @@ -81,7 +81,7 @@ impl super::Nexus { .disk_name(&Name(disk_name.clone())) .lookup_for(authz::Action::Read) .await?; - Ok(authz_disk.id()) + Ok(authz_disk) } _ => Err(Error::InvalidRequest { message: " @@ -99,14 +99,10 @@ impl super::Nexus { pub async fn project_create_disk( self: &Arc, opctx: &OpContext, - project_id: &Uuid, + authz_project: &authz::Project, params: ¶ms::DiskCreate, ) -> CreateResult { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .project_id(*project_id) - .lookup_for(authz::Action::CreateChild) - .await?; - + opctx.authorize(authz::Action::CreateChild, authz_project).await?; match ¶ms.disk_source { params::DiskSource::Blank { block_size } => { // Reject disks where the block size doesn't evenly divide the @@ -294,13 +290,10 @@ impl super::Nexus { pub async fn project_list_disks( &self, opctx: &OpContext, - project_id: &Uuid, + authz_project: &authz::Project, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .project_id(*project_id) - .lookup_for(authz::Action::ListChildren) - .await?; + opctx.authorize(authz::Action::ListChildren, authz_project).await?; self.db_datastore .project_list_disks(opctx, &authz_project, pagparams) .await @@ -309,10 +302,10 @@ impl super::Nexus { pub async fn disk_fetch( &self, opctx: &OpContext, - disk_id: &Uuid, + authz_disk: &authz::Disk, ) -> LookupResult { let (.., db_disk) = LookupPath::new(opctx, &self.db_datastore) - .disk_id(*disk_id) + .disk_id(authz_disk.id()) .fetch() .await?; Ok(db_disk) @@ -421,12 +414,9 @@ impl super::Nexus { pub async fn project_delete_disk( self: &Arc, opctx: &OpContext, - disk_id: &Uuid, + authz_disk: &authz::Disk, ) -> DeleteResult { - let (.., authz_disk) = LookupPath::new(opctx, &self.db_datastore) - .disk_id(*disk_id) - .lookup_for(authz::Action::Delete) - .await?; + opctx.authorize(authz::Action::Delete, authz_disk).await?; let saga_params = sagas::disk_delete::Params { disk_id: authz_disk.id() }; diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index ea250d154aa..73dbe2e9135 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1817,12 +1817,12 @@ async fn disk_list_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_id = - nexus.project_lookup_id(&opctx, query.selector).await?; + let authz_project = + nexus.project_lookup(&opctx, query.selector).await?; let disks = nexus .project_list_disks( &opctx, - &project_id, + &authz_project, &data_page_params_for(&rqctx, &query.pagination)? .map_name(|n| Name::ref_cast(n)), ) @@ -1858,8 +1858,8 @@ async fn disk_list( let project_name = &path.project_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_id = nexus - .project_lookup_id( + let authz_project = nexus + .project_lookup( &opctx, params::ProjectSelector { project: NameOrId::Name(project_name.clone().into()), @@ -1872,7 +1872,7 @@ async fn disk_list( let disks = nexus .project_list_disks( &opctx, - &project_id, + &authz_project, &data_page_params_for(&rqctx, &query)? .map_name(|n| Name::ref_cast(n)), ) @@ -1912,10 +1912,10 @@ async fn disk_create_v1( let new_disk_params = &new_disk.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_id = - nexus.project_lookup_id(&opctx, query.selector).await?; + let authz_project = + nexus.project_lookup(&opctx, query.selector).await?; let disk = nexus - .project_create_disk(&opctx, &project_id, new_disk_params) + .project_create_disk(&opctx, &authz_project, new_disk_params) .await?; Ok(HttpResponseCreated(disk.into())) }; @@ -1941,8 +1941,8 @@ async fn disk_create( let new_disk_params = &new_disk.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_id = nexus - .project_lookup_id( + let authz_project = nexus + .project_lookup( &opctx, params::ProjectSelector { project: NameOrId::Name(project_name.clone().into()), @@ -1953,7 +1953,7 @@ async fn disk_create( ) .await?; let disk = nexus - .project_create_disk(&opctx, &project_id, &new_disk_params) + .project_create_disk(&opctx, &authz_project, &new_disk_params) .await?; Ok(HttpResponseCreated(disk.into())) }; @@ -1994,13 +1994,13 @@ async fn disk_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk_id = nexus - .disk_lookup_id( + let authz_disk = nexus + .disk_lookup( &opctx, params::DiskSelector::new(path.disk, &query.selector), ) .await?; - let disk = nexus.disk_fetch(&opctx, &disk_id).await?; + let disk = nexus.disk_fetch(&opctx, &authz_disk).await?; Ok(HttpResponseOk(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2033,7 +2033,7 @@ async fn disk_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let disk_id = nexus - .disk_lookup_id( + .disk_lookup( &opctx, params::DiskSelector { disk: NameOrId::Name(disk_name.clone().into()), @@ -2066,7 +2066,13 @@ async fn disk_view_by_id( let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk = nexus.disk_fetch(&opctx, id).await?; + let authz_disk = nexus + .disk_lookup( + &opctx, + params::DiskSelector::new(NameOrId::Id(*id), &None), + ) + .await?; + let disk = nexus.disk_fetch(&opctx, &authz_disk).await?; Ok(HttpResponseOk(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2089,13 +2095,13 @@ async fn disk_delete_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk_id = nexus - .disk_lookup_id( + let authz_disk = nexus + .disk_lookup( &opctx, params::DiskSelector::new(path.disk, &query.selector), ) .await?; - nexus.project_delete_disk(&opctx, &disk_id).await?; + nexus.project_delete_disk(&opctx, &authz_disk).await?; Ok(HttpResponseOk(())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2119,7 +2125,7 @@ async fn disk_delete( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let disk_id = nexus - .disk_lookup_id( + .disk_lookup( &opctx, params::DiskSelector { disk: NameOrId::Name(disk_name.clone().into()), @@ -2177,8 +2183,8 @@ async fn disk_metrics_list_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk_id = nexus - .disk_lookup_id( + let authz_disk = nexus + .disk_lookup( &opctx, params::DiskSelector::new(path.inner.disk, &query.selector), ) @@ -2187,7 +2193,7 @@ async fn disk_metrics_list_v1( let result = nexus .select_timeseries( &format!("crucible_upstairs:{}", metric_name), - &[&format!("upstairs_uuid=={}", disk_id)], + &[&format!("upstairs_uuid=={}", authz_disk.id())], query.pagination, limit, ) @@ -2226,8 +2232,8 @@ async fn disk_metrics_list( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk_id = nexus - .disk_lookup_id( + let authz_disk = nexus + .disk_lookup( &opctx, params::DiskSelector { disk: NameOrId::Name(disk_name.clone().into()), @@ -2242,7 +2248,7 @@ async fn disk_metrics_list( let result = nexus .select_timeseries( &format!("crucible_upstairs:{}", metric_name), - &[&format!("upstairs_uuid=={}", disk_id)], + &[&format!("upstairs_uuid=={}", authz_disk.id())], query, limit, ) From 31cc6b1bc9c62a754ec38fee63746e2057de09df Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 7 Dec 2022 14:18:38 -0500 Subject: [PATCH 37/72] Fix invalid path name --- nexus/src/external_api/http_entrypoints.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 73dbe2e9135..28ba1cd1b82 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1980,7 +1980,7 @@ struct DiskViewParams { #[endpoint { method = GET, - path = "/disks/{disk_name}", + path = "/disks/{disk}", tags = ["disks"] }] async fn disk_view_v1( From 39fcf37cb9dcc6cf5f78b3255f49260322e8d1ef Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 8 Dec 2022 12:51:06 -0500 Subject: [PATCH 38/72] Add the ability to filter disks by instance --- nexus/src/app/instance.rs | 13 +--- nexus/src/external_api/http_entrypoints.rs | 77 ++++++++++++++++------ 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 23fbdebb3f3..877ff6c4abf 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -742,19 +742,12 @@ impl super::Nexus { pub async fn instance_list_disks( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, + authz_instance: &authz::Instance, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .instance_name(instance_name) - .lookup_for(authz::Action::ListChildren) - .await?; + opctx.authorize(authz::Action::ListChildren, authz_instance).await?; self.db_datastore - .instance_list_disks(opctx, &authz_instance, pagparams) + .instance_list_disks(opctx, authz_instance, pagparams) .await } diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 28ba1cd1b82..95bb0200fac 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1797,8 +1797,10 @@ async fn ip_pool_service_range_remove( #[derive(Deserialize, JsonSchema)] pub struct DiskListParams { + /// Optional filter to narrow disks returned to those attached to the given instance + instance: Option, #[serde(flatten)] - selector: params::ProjectSelector, + selector: Option, #[serde(flatten)] pagination: PaginatedByName, } @@ -1815,21 +1817,48 @@ async fn disk_list_v1( 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 authz_project = - nexus.project_lookup(&opctx, query.selector).await?; - let disks = nexus - .project_list_disks( - &opctx, - &authz_project, - &data_page_params_for(&rqctx, &query.pagination)? - .map_name(|n| Name::ref_cast(n)), - ) - .await? - .into_iter() - .map(|disk| disk.into()) - .collect(); + let instance = query.instance; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let disks = if instance.is_some() { + let instance = instance.unwrap(); + let authz_instance = nexus + .instance_lookup( + &opctx, + params::InstanceSelector::new(instance, &query.selector), + ) + .await?; + nexus + .instance_list_disks( + &opctx, + &authz_instance, + &data_page_params_for(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)), + ) + .await? + .into_iter() + .map(|disk| disk.into()) + .collect() + } else if query.selector.is_some() { + let authz_project = + nexus.project_lookup(&opctx, query.selector.unwrap()).await?; + nexus + .project_list_disks( + &opctx, + &authz_project, + &data_page_params_for(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)), + ) + .await? + .into_iter() + .map(|disk| disk.into()) + .collect() + } else { + Err(Error::InvalidRequest { + // TODO: Improve this error message + message: "instance or project selector required".to_string(), + })? + }; Ok(HttpResponseOk(ScanByName::results_page( &query.pagination, disks, @@ -3059,12 +3088,22 @@ async fn instance_disk_list( let instance_name = &path.instance_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let authz_instance = nexus + .instance_lookup( + &opctx, + 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(), + )), + }, + ) + .await?; let disks = nexus .instance_list_disks( &opctx, - &organization_name, - &project_name, - &instance_name, + &authz_instance, &data_page_params_for(&rqctx, &query)? .map_name(|n| Name::ref_cast(n)), ) From 9a4c6dacf8f1e0e17bd505b89766e4af3b54a10a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 8 Dec 2022 13:59:28 -0500 Subject: [PATCH 39/72] Fix clippy issues --- nexus/src/external_api/http_entrypoints.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 95bb0200fac..795fcb1f19a 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1820,8 +1820,7 @@ async fn disk_list_v1( let instance = query.instance; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disks = if instance.is_some() { - let instance = instance.unwrap(); + let disks = if let Some(instance) = instance { let authz_instance = nexus .instance_lookup( &opctx, @@ -1839,9 +1838,8 @@ async fn disk_list_v1( .into_iter() .map(|disk| disk.into()) .collect() - } else if query.selector.is_some() { - let authz_project = - nexus.project_lookup(&opctx, query.selector.unwrap()).await?; + } else if let Some(selector) = query.selector { + let authz_project = nexus.project_lookup(&opctx, selector).await?; nexus .project_list_disks( &opctx, From 9187fccb4a15c1a2ea900b474ca41ef537736284 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 8 Dec 2022 14:11:54 -0500 Subject: [PATCH 40/72] Update nexus.json --- nexus/src/external_api/http_entrypoints.rs | 4 +- nexus/tests/output/nexus_tags.txt | 5 + openapi/nexus.json | 428 ++++++++++++++++++--- 3 files changed, 392 insertions(+), 45 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 795fcb1f19a..f2b96f12dd7 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2115,7 +2115,7 @@ async fn disk_delete_v1( rqctx: Arc>>, path_params: Path, query_params: Query, -) -> Result, HttpError> { +) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); @@ -2129,7 +2129,7 @@ async fn disk_delete_v1( ) .await?; nexus.project_delete_disk(&opctx, &authz_disk).await?; - Ok(HttpResponseOk(())) + Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 3828fb5bfb7..6deaef373ff 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -1,11 +1,16 @@ API operations found with tag "disks" OPERATION ID URL PATH disk_create /organizations/{organization_name}/projects/{project_name}/disks +disk_create_v1 /disks disk_delete /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} +disk_delete_v1 /disks/{disk} disk_list /organizations/{organization_name}/projects/{project_name}/disks +disk_list_v1 /disks disk_metrics_list /organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name} +disk_metrics_list_v1 /disks/{disk}/metrics/{metric_name} disk_view /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} disk_view_by_id /by-id/disks/{id} +disk_view_v1 /disks/{disk} API operations found with tag "hidden" OPERATION ID URL PATH diff --git a/openapi/nexus.json b/openapi/nexus.json index e6a4b225d1d..064e72f6154 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -530,6 +530,350 @@ } } }, + "/disks": { + "get": { + "tags": [ + "disks" + ], + "operationId": "disk_list_v1", + "parameters": [ + { + "in": "query", + "name": "instance", + "description": "Optional filter to narrow disks returned to those attached to the given instance", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "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", + "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": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameSortMode" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "disks" + ], + "summary": "Create a disk", + "operationId": "disk_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/disks/{disk}": { + "get": { + "tags": [ + "disks" + ], + "operationId": "disk_view_v1", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "If Name is used to reference the disk 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" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "disks" + ], + "summary": "Delete a disk", + "operationId": "disk_delete_v1", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "If Name is used to reference the disk 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" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/disks/{disk}/metrics/{metric_name}": { + "get": { + "tags": [ + "disks" + ], + "operationId": "disk_metrics_list_v1", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "If Name is used to reference the disk 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" + }, + "style": "simple" + }, + { + "in": "path", + "name": "metric_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/DiskMetricName" + }, + "style": "simple" + }, + { + "in": "query", + "name": "end_time", + "description": "An exclusive end time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + }, + "style": "form" + }, + { + "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", + "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": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "start_time", + "description": "An inclusive start time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasurementResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, "/groups": { "get": { "tags": [ @@ -1442,7 +1786,6 @@ "tags": [ "disks" ], - "summary": "Create a disk", "operationId": "disk_create", "parameters": [ { @@ -1555,7 +1898,6 @@ "tags": [ "disks" ], - "summary": "Delete a disk", "operationId": "disk_delete", "parameters": [ { @@ -13355,40 +13697,23 @@ } } }, - "IdSortMode": { - "description": "Supported set of sort modes for scanning by id only.\n\nCurrently, we only support scanning in ascending order.", - "oneOf": [ - { - "description": "sort in increasing order of \"id\"", - "type": "string", - "enum": [ - "id_ascending" - ] - } - ] - }, - "NameOrIdSortMode": { - "description": "Supported set of sort modes for scanning by name or id", + "NameOrId": { "oneOf": [ { - "description": "sort in increasing order of \"name\"", - "type": "string", - "enum": [ - "name_ascending" - ] - }, - { - "description": "sort in decreasing order of \"name\"", - "type": "string", - "enum": [ - "name_descending" + "title": "id", + "allOf": [ + { + "type": "string", + "format": "uuid" + } ] }, { - "description": "sort in increasing order of \"id\"", - "type": "string", - "enum": [ - "id_ascending" + "title": "name", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } ] } ] @@ -13416,23 +13741,40 @@ "write_bytes" ] }, - "NameOrId": { + "IdSortMode": { + "description": "Supported set of sort modes for scanning by id only.\n\nCurrently, we only support scanning in ascending order.", "oneOf": [ { - "title": "id", - "allOf": [ - { - "type": "string", - "format": "uuid" - } + "description": "sort in increasing order of \"id\"", + "type": "string", + "enum": [ + "id_ascending" + ] + } + ] + }, + "NameOrIdSortMode": { + "description": "Supported set of sort modes for scanning by name or id", + "oneOf": [ + { + "description": "sort in increasing order of \"name\"", + "type": "string", + "enum": [ + "name_ascending" ] }, { - "title": "name", - "allOf": [ - { - "$ref": "#/components/schemas/Name" - } + "description": "sort in decreasing order of \"name\"", + "type": "string", + "enum": [ + "name_descending" + ] + }, + { + "description": "sort in increasing order of \"id\"", + "type": "string", + "enum": [ + "id_ascending" ] } ] From 9c74e88be2d41285e52786ce08cbdc6690cf7021 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 8 Dec 2022 15:50:29 -0500 Subject: [PATCH 41/72] Add attach and detach methods --- nexus/src/app/instance.rs | 40 +---- nexus/src/external_api/http_entrypoints.rs | 142 ++++++++++++++-- nexus/tests/output/nexus_tags.txt | 2 + nexus/types/src/external_api/params.rs | 5 + openapi/nexus.json | 183 ++++++++++++++++++--- 5 files changed, 303 insertions(+), 69 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 877ff6c4abf..9822eceed50 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -755,25 +755,9 @@ impl super::Nexus { pub async fn instance_attach_disk( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, - disk_name: &Name, + authz_instance: &authz::Instance, + authz_disk: &authz::Disk, ) -> UpdateResult { - let (.., authz_project, authz_disk, _) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .disk_name(disk_name) - .fetch() - .await?; - let (.., authz_instance, _) = - LookupPath::new(opctx, &self.db_datastore) - .project_id(authz_project.id()) - .instance_name(instance_name) - .fetch() - .await?; - // TODO(https://github.com/oxidecomputer/omicron/issues/811): // Disk attach is only implemented for instances that are not // currently running. This operation therefore can operate exclusively @@ -803,25 +787,9 @@ impl super::Nexus { pub async fn instance_detach_disk( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - instance_name: &Name, - disk_name: &Name, + authz_instance: &authz::Instance, + authz_disk: &authz::Disk, ) -> UpdateResult { - let (.., authz_project, authz_disk, _) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .disk_name(disk_name) - .fetch() - .await?; - let (.., authz_instance, _) = - LookupPath::new(opctx, &self.db_datastore) - .project_id(authz_project.id()) - .instance_name(instance_name) - .fetch() - .await?; - // TODO(https://github.com/oxidecomputer/omicron/issues/811): // Disk detach is only implemented for instances that are not // currently running. This operation therefore can operate exclusively diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index f2b96f12dd7..c913d90c4e7 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -139,6 +139,8 @@ pub fn external_api() -> NexusApiDescription { api.register(disk_create_v1)?; api.register(disk_view_v1)?; api.register(disk_delete_v1)?; + api.register(disk_attach_v1)?; + api.register(disk_detach_v1)?; api.register(disk_metrics_list_v1)?; api.register(instance_list)?; @@ -2169,6 +2171,84 @@ async fn disk_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Attach a disk to an instance +#[endpoint { + method = POST, + path = "/disks/{disk}/attach", + tags = ["disks"], +}] +async fn disk_attach_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, + body: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let body = body.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let authz_instance = nexus + .instance_lookup( + &opctx, + params::InstanceSelector::new(body.instance, &query.selector), + ) + .await?; + let authz_disk = nexus + .disk_lookup( + &opctx, + params::DiskSelector::new(path.disk, &query.selector), + ) + .await?; + let disk = nexus + .instance_attach_disk(&opctx, &authz_instance, &authz_disk) + .await?; + Ok(HttpResponseAccepted(disk.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Detach a disk from an instance +#[endpoint { + method = POST, + path = "/disks/{disk}/detach", + tags = ["disks"], +}] +async fn disk_detach_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, + body: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let body = body.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let authz_instance = nexus + .instance_lookup( + &opctx, + params::InstanceSelector::new(body.instance, &query.selector), + ) + .await?; + let authz_disk = nexus + .disk_lookup( + &opctx, + params::DiskSelector::new(path.disk, &query.selector), + ) + .await?; + let disk = nexus + .instance_detach_disk(&opctx, &authz_instance, &authz_disk) + .await?; + Ok(HttpResponseAccepted(disk.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + #[derive(Display, Deserialize, JsonSchema)] #[display(style = "snake_case")] #[serde(rename_all = "snake_case")] @@ -3135,17 +3215,36 @@ async fn instance_disk_attach( let organization_name = &path.organization_name; let project_name = &path.project_name; let instance_name = &path.instance_name; + let disk_name = disk_to_attach.into_inner().name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk = nexus - .instance_attach_disk( + let authz_instance = nexus + .instance_lookup( &opctx, - &organization_name, - &project_name, - &instance_name, - &disk_to_attach.into_inner().name.into(), + 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(), + )), + }, ) .await?; + let authz_disk = nexus + .disk_lookup( + &opctx, + params::DiskSelector { + disk: NameOrId::Name(disk_name), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name( + organization_name.clone().into(), + )), + }, + ) + .await?; + let disk = nexus + .instance_attach_disk(&opctx, &authz_instance, &authz_disk) + .await?; Ok(HttpResponseAccepted(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -3168,17 +3267,36 @@ async fn instance_disk_detach( let organization_name = &path.organization_name; let project_name = &path.project_name; let instance_name = &path.instance_name; + let disk_name = disk_to_detach.into_inner().name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk = nexus - .instance_detach_disk( + let authz_instance = nexus + .instance_lookup( &opctx, - &organization_name, - &project_name, - &instance_name, - &disk_to_detach.into_inner().name.into(), + 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(), + )), + }, + ) + .await?; + let authz_disk = nexus + .disk_lookup( + &opctx, + params::DiskSelector { + disk: NameOrId::Name(disk_name), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name( + organization_name.clone().into(), + )), + }, ) .await?; + let disk = nexus + .instance_detach_disk(&opctx, &authz_instance, &authz_disk) + .await?; Ok(HttpResponseAccepted(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 6deaef373ff..27da9db8808 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -1,9 +1,11 @@ API operations found with tag "disks" OPERATION ID URL PATH +disk_attach_v1 /disks/{disk}/attach disk_create /organizations/{organization_name}/projects/{project_name}/disks disk_create_v1 /disks disk_delete /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} disk_delete_v1 /disks/{disk} +disk_detach_v1 /disks/{disk}/detach disk_list /organizations/{organization_name}/projects/{project_name}/disks disk_list_v1 /disks disk_metrics_list /organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name} diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 3a59c9d318e..7407081cadb 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -934,6 +934,11 @@ pub struct DiskIdentifier { pub name: Name, } +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstanceIdentifier { + pub instance: NameOrId, +} + /// Parameters for the /// [`NetworkInterface`](omicron_common::api::external::NetworkInterface) to be /// attached or detached to an instance. diff --git a/openapi/nexus.json b/openapi/nexus.json index 064e72f6154..b3f6b1f5687 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -768,6 +768,136 @@ } } }, + "/disks/{disk}/attach": { + "post": { + "tags": [ + "disks" + ], + "summary": "Attach a disk to an instance", + "operationId": "disk_attach_v1", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "If Name is used to reference the disk 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" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceIdentifier" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/disks/{disk}/detach": { + "post": { + "tags": [ + "disks" + ], + "summary": "Detach a disk from an instance", + "operationId": "disk_detach_v1", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "If Name is used to reference the disk 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" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceIdentifier" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/disks/{disk}/metrics/{metric_name}": { "get": { "tags": [ @@ -10442,6 +10572,17 @@ } ] }, + "InstanceIdentifier": { + "type": "object", + "properties": { + "instance": { + "$ref": "#/components/schemas/NameOrId" + } + }, + "required": [ + "instance" + ] + }, "InstanceMigrate": { "description": "Migration parameters for an [`Instance`](omicron_common::api::external::Instance)", "type": "object", @@ -10942,6 +11083,27 @@ "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$", "maxLength": 63 }, + "NameOrId": { + "oneOf": [ + { + "title": "id", + "allOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + { + "title": "name", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + } + ] + }, "NetworkInterface": { "description": "A `NetworkInterface` represents a virtual network interface device.", "type": "object", @@ -13697,27 +13859,6 @@ } } }, - "NameOrId": { - "oneOf": [ - { - "title": "id", - "allOf": [ - { - "type": "string", - "format": "uuid" - } - ] - }, - { - "title": "name", - "allOf": [ - { - "$ref": "#/components/schemas/Name" - } - ] - } - ] - }, "NameSortMode": { "description": "Supported set of sort modes for scanning by name only\n\nCurrently, we only support scanning in ascending order.", "oneOf": [ From ff14b9b82615a8a340606da873068512d99df935 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 8 Dec 2022 17:46:45 -0500 Subject: [PATCH 42/72] Fix some of the failing tests --- nexus/src/external_api/http_entrypoints.rs | 16 +++--- nexus/tests/integration_tests/endpoints.rs | 55 ++++++++----------- nexus/tests/integration_tests/unauthorized.rs | 4 +- .../unauthorized_coverage.rs | 2 +- 4 files changed, 34 insertions(+), 43 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index c913d90c4e7..bfd83f76a60 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1809,7 +1809,7 @@ pub struct DiskListParams { #[endpoint { method = GET, - path = "/disks", + path = "/v1/disks", tags = ["disks"], }] async fn disk_list_v1( @@ -1927,7 +1927,7 @@ pub struct DiskCreateParams { /// Create a disk #[endpoint { method = POST, - path = "/disks", + path = "/v1/disks", tags = ["disks"] }] async fn disk_create_v1( @@ -2009,7 +2009,7 @@ struct DiskViewParams { #[endpoint { method = GET, - path = "/disks/{disk}", + path = "/v1/disks/{disk}", tags = ["disks"] }] async fn disk_view_v1( @@ -2110,7 +2110,7 @@ async fn disk_view_by_id( /// Delete a disk #[endpoint { method = DELETE, - path = "/disks/{disk}", + path = "/v1/disks/{disk}", tags = ["disks"], }] async fn disk_delete_v1( @@ -2174,7 +2174,7 @@ async fn disk_delete( /// Attach a disk to an instance #[endpoint { method = POST, - path = "/disks/{disk}/attach", + path = "/v1/disks/{disk}/attach", tags = ["disks"], }] async fn disk_attach_v1( @@ -2213,7 +2213,7 @@ async fn disk_attach_v1( /// Detach a disk from an instance #[endpoint { method = POST, - path = "/disks/{disk}/detach", + path = "/v1/disks/{disk}/detach", tags = ["disks"], }] async fn disk_detach_v1( @@ -2273,7 +2273,7 @@ pub struct DiskMetricsPaginationParams { #[endpoint { method = GET, - path = "/disks/{disk}/metrics/{metric_name}", + path = "/v1/disks/{disk}/metrics/{metric_name}", tags = ["disks"], }] async fn disk_metrics_list_v1( @@ -2315,7 +2315,7 @@ async fn disk_metrics_list_v1( /// Fetch disk metrics #[endpoint { method = GET, - path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name}", + path = "/v1/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name}", tags = ["disks"], }] async fn disk_metrics_list( diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 6051eeaf7a0..cd5c45d536a 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -105,11 +105,9 @@ lazy_static! { pub static ref DEMO_PROJECT_URL: String = format!("{}/{}", *DEMO_ORG_PROJECTS_URL, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_SELECTOR: String = - format!("?organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_POLICY_URL: String = format!("{}/policy", *DEMO_PROJECT_URL); - pub static ref DEMO_PROJECT_URL_DISKS: String = - format!("{}/disks", *DEMO_PROJECT_URL); pub static ref DEMO_PROJECT_URL_IMAGES: String = format!("{}/images", *DEMO_PROJECT_URL); pub static ref DEMO_PROJECT_URL_INSTANCES: String = format!("/v1/instances?organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); @@ -194,8 +192,11 @@ lazy_static! { // Disk used for testing pub static ref DEMO_DISK_NAME: Name = "demo-disk".parse().unwrap(); + // TODO: Once we can test a URL multiple times we should also a case to exercise authz for disks filtered by instances + pub static ref DEMO_DISKS_URL: String = + format!("/v1/disks?{}", *DEMO_PROJECT_SELECTOR); pub static ref DEMO_DISK_URL: String = - format!("{}/{}", *DEMO_PROJECT_URL_DISKS, *DEMO_DISK_NAME); + format!("/v1/disks/{}?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_DISK_CREATE: params::DiskCreate = params::DiskCreate { identity: IdentityMetadataCreateParams { @@ -210,10 +211,14 @@ lazy_static! { DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 2 ), }; + pub static ref DEMO_DISKS_ATTACH_URL: String = + format!("/v1/disks/{}/attach?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR); + pub static ref DEMO_DISKS_DETACH_URL: String = + format!("/v1/disks/{}/detach?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_DISK_METRICS_URL: String = format!( - "{}/metrics/activated?start_time={:?}&end_time={:?}", - *DEMO_DISK_URL, + "/v1/disks/{}/metrics/activated?start_time={:?}&end_time={:?}", + *DEMO_DISK_NAME, Utc::now(), Utc::now(), ); @@ -224,28 +229,23 @@ lazy_static! { lazy_static! { // Instance used for testing pub static ref DEMO_INSTANCE_NAME: Name = "demo-instance".parse().unwrap(); + pub static ref DEMO_INSTANCE_SELECTOR: String = format!("{}&instance={}", *DEMO_PROJECT_SELECTOR, *DEMO_INSTANCE_NAME); pub static ref DEMO_INSTANCE_URL: String = - format!("/v1/instances/{}?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("/v1/instances/{}?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_INSTANCE_START_URL: String = - format!("/v1/instances/{}/start?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("/v1/instances/{}/start?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_INSTANCE_STOP_URL: String = - format!("/v1/instances/{}/stop?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("/v1/instances/{}/stop?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_INSTANCE_REBOOT_URL: String = - format!("/v1/instances/{}/reboot?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("/v1/instances/{}/reboot?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_INSTANCE_MIGRATE_URL: String = - format!("/v1/instances/{}/migrate?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("/v1/instances/{}/migrate?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_INSTANCE_SERIAL_URL: String = - format!("/v1/instances/{}/serial-console?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("/v1/instances/{}/serial-console?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_INSTANCE_SERIAL_STREAM_URL: String = - format!("/v1/instances/{}/serial-console/stream?organization={}&project={}", *DEMO_INSTANCE_NAME, *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); + format!("/v1/instances/{}/serial-console/stream?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); // To be migrated... - pub static ref DEMO_INSTANCE_DISKS_URL: String = - format!("/organizations/{}/projects/{}/instances/{}/disks", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); - pub static ref DEMO_INSTANCE_DISKS_ATTACH_URL: String = - format!("{}/attach", *DEMO_INSTANCE_DISKS_URL); - pub static ref DEMO_INSTANCE_DISKS_DETACH_URL: String = - format!("{}/detach", *DEMO_INSTANCE_DISKS_URL); pub static ref DEMO_INSTANCE_NICS_URL: String = format!("/organizations/{}/projects/{}/instances/{}/network-interfaces", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); pub static ref DEMO_INSTANCE_EXTERNAL_IPS_URL: String = @@ -1132,7 +1132,7 @@ lazy_static! { /* Disks */ VerifyEndpoint { - url: &*DEMO_PROJECT_URL_DISKS, + url: &*DEMO_DISKS_URL, visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ @@ -1143,15 +1143,6 @@ lazy_static! { ], }, - VerifyEndpoint { - url: "/by-id/disks/{id}", - visibility: Visibility::Protected, - unprivileged_access: UnprivilegedAccess::None, - allowed_methods: vec![ - AllowedMethod::Get, - ], - }, - VerifyEndpoint { url: &*DEMO_DISK_URL, visibility: Visibility::Protected, @@ -1172,7 +1163,7 @@ lazy_static! { }, VerifyEndpoint { - url: &*DEMO_INSTANCE_DISKS_URL, + url: &*DEMO_DISKS_URL, visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ @@ -1180,7 +1171,7 @@ lazy_static! { ], }, VerifyEndpoint { - url: &*DEMO_INSTANCE_DISKS_ATTACH_URL, + url: &*DEMO_DISKS_ATTACH_URL, visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ @@ -1192,7 +1183,7 @@ lazy_static! { ], }, VerifyEndpoint { - url: &*DEMO_INSTANCE_DISKS_DETACH_URL, + url: &*DEMO_DISKS_DETACH_URL, visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 7f6a6c300f1..58c01a0641d 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -250,9 +250,9 @@ lazy_static! { }, // Create a Disk in the Project SetupReq::Post { - url: &*DEMO_PROJECT_URL_DISKS, + url: &*DEMO_DISKS_URL, body: serde_json::to_value(&*DEMO_DISK_CREATE).unwrap(), - id_routes: vec!["/by-id/disks/{id}"], + id_routes: vec!["/v1/disks/{id}"], }, // Create an Instance in the Project SetupReq::Post { diff --git a/nexus/tests/integration_tests/unauthorized_coverage.rs b/nexus/tests/integration_tests/unauthorized_coverage.rs index 64ce7eabc82..b9bb4767e47 100644 --- a/nexus/tests/integration_tests/unauthorized_coverage.rs +++ b/nexus/tests/integration_tests/unauthorized_coverage.rs @@ -149,7 +149,7 @@ fn test_unauthorized_coverage() { } assert_eq!( has_uncovered_endpoints, false, - "{}\nMake sure you've added a test for this endpoint in unauthorized.rs.", + "{}\nMake sure you've added a test for this endpoint in endpoints.rs.", unexpected_uncovered_endpoints ) } From afbda07b89a4fbd93aa6f08a584212238a5be740 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 9 Dec 2022 15:17:10 -0500 Subject: [PATCH 43/72] Update instance lookup to return lookups instead of authz refs --- nexus/src/app/instance.rs | 115 ++++----- nexus/src/external_api/http_entrypoints.rs | 272 ++++++++------------- 2 files changed, 147 insertions(+), 240 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 23fbdebb3f3..2911839ef89 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -15,6 +15,7 @@ use crate::cidata::InstanceCiData; use crate::context::OpContext; use crate::db; use crate::db::identity::Resource; +use crate::db::lookup; use crate::db::lookup::LookupPath; use crate::db::queries::network_interface; use crate::external_api::params; @@ -36,6 +37,7 @@ use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; use omicron_common::api::external::Vni; use omicron_common::api::internal::nexus; +use ref_cast::RefCast; use sled_agent_client::types::InstanceRuntimeStateMigrateParams; use sled_agent_client::types::InstanceRuntimeStateRequested; use sled_agent_client::types::InstanceStateRequested; @@ -54,20 +56,17 @@ use uuid::Uuid; const MAX_KEYS_PER_INSTANCE: u32 = 8; impl super::Nexus { - pub async fn instance_lookup( - &self, - opctx: &OpContext, - instance_selector: params::InstanceSelector, - ) -> LookupResult { + pub fn instance_lookup<'a>( + &'a self, + opctx: &'a OpContext, + instance_selector: &'a params::InstanceSelector, + ) -> LookupResult> { match instance_selector { params::InstanceSelector { instance: NameOrId::Id(id), .. } => { // TODO: 400 if project or organization are present - let (.., authz_instance) = - LookupPath::new(opctx, &self.db_datastore) - .instance_id(id) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_instance) + let instance = + LookupPath::new(opctx, &self.db_datastore).instance_id(*id); + Ok(instance) } params::InstanceSelector { instance: NameOrId::Name(instance_name), @@ -75,41 +74,32 @@ impl super::Nexus { .. } => { // TODO: 400 if organization is present - let (.., authz_instance) = - LookupPath::new(opctx, &self.db_datastore) - .project_id(project_id) - .instance_name(&Name(instance_name)) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_instance) + 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 (.., authz_instance) = - LookupPath::new(opctx, &self.db_datastore) - .organization_id(organization_id) - .project_name(&Name(project_name)) - .instance_name(&Name(instance_name.clone())) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_instance) + 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 (.., authz_instance) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(&Name(organization_name)) - .project_name(&Name(project_name)) - .instance_name(&Name(instance_name.clone())) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_instance) + 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) } // TODO: Add a better error message _ => Err(Error::InvalidRequest { @@ -269,30 +259,19 @@ impl super::Nexus { .await } - pub async fn instance_fetch( - &self, - opctx: &OpContext, - authz_instance: &authz::Instance, - ) -> LookupResult { - let (.., db_instance) = LookupPath::new(opctx, &self.db_datastore) - .instance_id(authz_instance.id()) - .fetch() - .await?; - Ok(db_instance) - } - // This operation may only occur on stopped instances, which implies that // the attached disks do not have any running "upstairs" process running // within the sled. pub async fn project_destroy_instance( &self, opctx: &OpContext, - authz_instance: &authz::Instance, + instance_lookup: &lookup::Instance<'_>, ) -> DeleteResult { // TODO-robustness We need to figure out what to do with Destroyed // instances? Presumably we need to clean them up at some point, but // not right away so that callers can see that they've been destroyed. - opctx.authorize(authz::Action::Delete, authz_instance).await?; + let (.., authz_instance) = + instance_lookup.lookup_for(authz::Action::Delete).await?; self.db_datastore .project_delete_instance(opctx, &authz_instance) @@ -310,10 +289,11 @@ impl super::Nexus { pub async fn project_instance_migrate( self: &Arc, opctx: &OpContext, - authz_instance: &authz::Instance, + instance_lookup: &lookup::Instance<'_>, params: params::InstanceMigrate, ) -> UpdateResult { - opctx.authorize(authz::Action::Modify, authz_instance).await?; + let (.., authz_instance) = + instance_lookup.lookup_for(authz::Action::Modify).await?; // Kick off the migration saga let saga_params = sagas::instance_migrate::Params { @@ -367,7 +347,7 @@ impl super::Nexus { pub async fn instance_reboot( &self, opctx: &OpContext, - authz_instance: &authz::Instance, + instance_lookup: &lookup::Instance<'_>, ) -> UpdateResult { // To implement reboot, we issue a call to the sled agent to set a // runtime state of "reboot". We cannot simply stop the Instance and @@ -380,11 +360,7 @@ impl super::Nexus { // even if the whole rack powered off while this was going on, we would // never lose track of the fact that this Instance was supposed to be // running. - let (.., authz_instance, db_instance) = - LookupPath::new(opctx, &self.db_datastore) - .instance_id(authz_instance.id()) - .fetch() - .await?; + let (.., authz_instance, db_instance) = instance_lookup.fetch().await?; let requested = InstanceRuntimeStateRequested { run_state: InstanceStateRequested::Reboot, migration_params: None, @@ -403,13 +379,9 @@ impl super::Nexus { pub async fn instance_start( &self, opctx: &OpContext, - authz_instance: &authz::Instance, + instance_lookup: &lookup::Instance<'_>, ) -> UpdateResult { - let (.., authz_instance, db_instance) = - LookupPath::new(opctx, &self.db_datastore) - .instance_id(authz_instance.id()) - .fetch() - .await?; + let (.., authz_instance, db_instance) = instance_lookup.fetch().await?; let requested = InstanceRuntimeStateRequested { run_state: InstanceStateRequested::Running, migration_params: None, @@ -428,13 +400,9 @@ impl super::Nexus { pub async fn instance_stop( &self, opctx: &OpContext, - authz_instance: &authz::Instance, + instance_lookup: &lookup::Instance<'_>, ) -> UpdateResult { - let (.., authz_instance, db_instance) = - LookupPath::new(opctx, &self.db_datastore) - .instance_id(authz_instance.id()) - .fetch() - .await?; + let (.., authz_instance, db_instance) = instance_lookup.fetch().await?; let requested = InstanceRuntimeStateRequested { run_state: InstanceStateRequested::Stopped, migration_params: None, @@ -1126,11 +1094,10 @@ impl super::Nexus { /// provided they are still in the sled-agent's cache. pub(crate) async fn instance_serial_console_data( &self, - opctx: &OpContext, - authz_instance: &authz::Instance, + instance_lookup: &lookup::Instance<'_>, params: ¶ms::InstanceSerialConsoleRequest, ) -> Result { - let db_instance = self.instance_fetch(opctx, authz_instance).await?; + let (.., db_instance) = instance_lookup.fetch().await?; let sa = self.instance_sled(&db_instance).await?; let data = sa @@ -1152,11 +1119,11 @@ impl super::Nexus { pub(crate) async fn instance_serial_console_stream( &self, - opctx: &OpContext, conn: dropshot::WebsocketConnection, - authz_instance: &authz::Instance, + instance_lookup: &lookup::Instance<'_>, ) -> Result<(), Error> { - let instance = self.instance_fetch(opctx, authz_instance).await?; + // TODO: Technically the stream is two way so the user of this method can modify the instance in some way. Should we use different permissions? + let (.., instance) = instance_lookup.fetch().await?; let ip_addr = instance .runtime_state .propolis_ip diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 8da76a36770..62898fcbf50 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2214,13 +2214,11 @@ async fn instance_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(path.instance, &query.selector), - ) - .await?; - let instance = nexus.instance_fetch(&opctx, &authz_instance).await?; + let instance_selector = + params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = + nexus.instance_lookup(&opctx, &instance_selector)?; + let (.., instance) = instance_selector.fetch().await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2250,21 +2248,16 @@ async fn instance_view( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; - let instance = nexus.instance_fetch(&opctx, &authz_instance).await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let (.., instance) = instance_lookup.fetch().await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2286,17 +2279,17 @@ async fn instance_view_by_id( let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus + let (.., instance) = nexus .instance_lookup( &opctx, - params::InstanceSelector { + ¶ms::InstanceSelector { instance: NameOrId::Id(*id), project: None, organization: None, }, - ) + )? + .fetch() .await?; - let instance = nexus.instance_fetch(&opctx, &authz_instance).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2316,15 +2309,13 @@ 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(path.instance, &query.selector), - ) - .await?; - nexus.project_destroy_instance(&opctx, &authz_instance).await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + nexus.project_destroy_instance(&opctx, &instance_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2346,21 +2337,16 @@ async fn instance_delete( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; - nexus.project_destroy_instance(&opctx, &authz_instance).await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + nexus.project_destroy_instance(&opctx, &instance_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2383,18 +2369,16 @@ 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(path.instance, &query.selector), - ) - .await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; let instance = nexus .project_instance_migrate( &opctx, - &authz_instance, + &instance_lookup, migrate_instance_params, ) .await?; @@ -2422,24 +2406,19 @@ async fn instance_migrate( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; let instance = nexus .project_instance_migrate( &opctx, - &authz_instance, + &instance_lookup, migrate_instance_params, ) .await?; @@ -2462,15 +2441,13 @@ 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(path.instance, &query.selector), - ) - .await?; - let instance = nexus.instance_reboot(&opctx, &authz_instance).await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let instance = nexus.instance_reboot(&opctx, &instance_lookup).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2492,21 +2469,16 @@ async fn instance_reboot( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; - let instance = nexus.instance_reboot(&opctx, &authz_instance).await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let instance = nexus.instance_reboot(&opctx, &instance_lookup).await?; Ok(HttpResponseAccepted(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2527,15 +2499,13 @@ 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(path.instance, &query.selector), - ) - .await?; - let instance = nexus.instance_start(&opctx, &authz_instance).await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let instance = nexus.instance_start(&opctx, &instance_lookup).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2557,21 +2527,16 @@ async fn instance_start( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; - let instance = nexus.instance_start(&opctx, &authz_instance).await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let instance = nexus.instance_start(&opctx, &instance_lookup).await?; Ok(HttpResponseAccepted(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2591,15 +2556,13 @@ 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(path.instance, &query.selector), - ) - .await?; - let instance = nexus.instance_stop(&opctx, &authz_instance).await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let instance = nexus.instance_stop(&opctx, &instance_lookup).await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2621,21 +2584,16 @@ async fn instance_stop( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; - let instance = nexus.instance_stop(&opctx, &authz_instance).await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let instance = nexus.instance_stop(&opctx, &instance_lookup).await?; Ok(HttpResponseAccepted(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2664,18 +2622,15 @@ 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(path.instance, &query.selector), - ) - .await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; let data = nexus .instance_serial_console_data( - &opctx, - &authz_instance, + &instance_lookup, &query.console_params, ) .await?; @@ -2701,24 +2656,18 @@ async fn instance_serial_console( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; let data = nexus .instance_serial_console_data( - &opctx, - &authz_instance, + &instance_lookup, &query_params.into_inner(), ) .await?; @@ -2743,13 +2692,10 @@ 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 authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(path.instance, &query.selector), - ) - .await?; - nexus.instance_serial_console_stream(&opctx, conn, &authz_instance).await?; + let instance_selector = + params::InstanceSelector::new(path.instance, &query.selector); + let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; + nexus.instance_serial_console_stream(conn, &instance_lookup).await?; Ok(()) } @@ -2771,19 +2717,13 @@ async fn instance_serial_console_stream( let project_name = &path.project_name; let instance_name = &path.instance_name; let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; - nexus.instance_serial_console_stream(&opctx, conn, &authz_instance).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_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; + nexus.instance_serial_console_stream(conn, &instance_lookup).await?; Ok(()) } From eb9335db6a27b86e70c38930e89caf1925d6e1aa Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 9 Dec 2022 15:42:40 -0500 Subject: [PATCH 44/72] Convert project lookup to return lookup::Project --- nexus/src/app/instance.rs | 10 +++-- nexus/src/app/project.rs | 43 ++++++++----------- nexus/src/external_api/http_entrypoints.rs | 49 ++++++++++------------ 3 files changed, 46 insertions(+), 56 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 2911839ef89..ec4f0f574ac 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -118,10 +118,11 @@ impl super::Nexus { pub async fn project_create_instance( self: &Arc, opctx: &OpContext, - authz_project: &authz::Project, + project_lookup: &lookup::Project<'_>, params: ¶ms::InstanceCreate, ) -> CreateResult { - opctx.authorize(authz::Action::CreateChild, authz_project).await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::CreateChild).await?; // Validate parameters if params.disks.len() > MAX_DISKS_PER_INSTANCE as usize { @@ -250,10 +251,11 @@ impl super::Nexus { pub async fn project_list_instances( &self, opctx: &OpContext, - authz_project: &authz::Project, + project_lookup: &lookup::Project<'_>, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - opctx.authorize(authz::Action::ListChildren, authz_project).await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore .project_list_instances(opctx, &authz_project, pagparams) .await diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index 917fc93dfb6..07c4c73ceec 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.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; @@ -22,47 +23,39 @@ 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 async fn project_lookup( - &self, - opctx: &OpContext, - project_selector: params::ProjectSelector, - ) -> LookupResult { + pub fn project_lookup<'a>( + &'a self, + opctx: &'a OpContext, + project_selector: &'a params::ProjectSelector, + ) -> LookupResult> { match project_selector { params::ProjectSelector { project: NameOrId::Id(id), .. } => { // TODO: 400 if organization is present - let (.., authz_project) = - LookupPath::new(opctx, &self.db_datastore) - .project_id(id) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_project) + 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 (.., authz_project) = - LookupPath::new(opctx, &self.db_datastore) - .organization_id(organization_id) - .project_name(&Name(project_name)) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_project) + 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 (.., authz_project) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(&Name(organization_name)) - .project_name(&Name(project_name)) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_project) + let project = LookupPath::new(opctx, &self.db_datastore) + .organization_name(Name::ref_cast(organization_name)) + .project_name(Name::ref_cast(project_name)); + Ok(project) } _ => Err(Error::InvalidRequest { message: " diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 62898fcbf50..50b3435f5c8 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2032,8 +2032,7 @@ 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).await?; + let authz_project = nexus.project_lookup(&opctx, &query.selector)?; let instances = nexus .project_list_instances( &opctx, @@ -2071,23 +2070,17 @@ async fn instance_list( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_project = nexus - .project_lookup( - &opctx, - params::ProjectSelector { - project: NameOrId::Name(project_name.clone().into()), - organization: Some(NameOrId::Name( - organization_name.clone().into(), - )), - }, - ) - .await?; + 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)? .map_name(|n| Name::ref_cast(n)), ) @@ -2126,9 +2119,13 @@ 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_id = nexus.project_lookup(&opctx, query.selector).await?; + let project_lookup = nexus.project_lookup(&opctx, &query.selector)?; let instance = nexus - .project_create_instance(&opctx, &project_id, &new_instance_params) + .project_create_instance( + &opctx, + &project_lookup, + &new_instance_params, + ) .await?; Ok(HttpResponseCreated(instance.into())) }; @@ -2159,22 +2156,20 @@ async fn instance_create( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_id = nexus - .project_lookup( + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let instance = nexus + .project_create_instance( &opctx, - params::ProjectSelector { - project: NameOrId::Name(project_name.clone().into()), - organization: Some(NameOrId::Name( - organization_name.clone().into(), - )), - }, + &project_lookup, + &new_instance_params, ) .await?; - let instance = nexus - .project_create_instance(&opctx, &project_id, &new_instance_params) - .await?; Ok(HttpResponseCreated(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await From 103368368e9f6d37d31be6a99f71f5e1b7fe07a7 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 12 Dec 2022 14:20:27 -0500 Subject: [PATCH 45/72] Deprecate old endpoints, fix missing v1 prefix, update disk lookups --- nexus/src/app/disk.rs | 86 +- nexus/src/app/instance.rs | 23 +- nexus/src/external_api/http_entrypoints.rs | 317 ++- nexus/tests/output/nexus_tags.txt | 14 +- openapi/nexus.json | 2168 ++++++++++---------- 5 files changed, 1276 insertions(+), 1332 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 52c02532ba1..5902ef95463 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -9,6 +9,7 @@ use crate::authn; 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; @@ -22,66 +23,55 @@ use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::NameOrId; use omicron_common::api::internal::nexus::DiskRuntimeState; +use ref_cast::RefCast; use sled_agent_client::Client as SledAgentClient; use std::sync::Arc; use uuid::Uuid; impl super::Nexus { // Disks - pub async fn disk_lookup( - &self, - opctx: &OpContext, - disk_selector: params::DiskSelector, - ) -> LookupResult { + pub fn disk_lookup<'a>( + &'a self, + opctx: &'a OpContext, + disk_selector: &'a params::DiskSelector, + ) -> LookupResult> { match disk_selector { params::DiskSelector { disk: NameOrId::Id(id), .. } => { - let (.., authz_disk) = - LookupPath::new(opctx, &self.db_datastore) - .disk_id(id) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_disk) + let disk = + LookupPath::new(opctx, &self.db_datastore).disk_id(*id); + Ok(disk) } params::DiskSelector { disk: NameOrId::Name(disk_name), project: Some(NameOrId::Id(project_id)), .. } => { - let (.., authz_disk) = - LookupPath::new(opctx, &self.db_datastore) - .project_id(project_id) - .disk_name(&Name(disk_name.clone())) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_disk) + let disk = LookupPath::new(opctx, &self.db_datastore) + .project_id(*project_id) + .disk_name(Name::ref_cast(disk_name)); + Ok(disk) } params::DiskSelector { disk: NameOrId::Name(disk_name), project: Some(NameOrId::Name(project_name)), organization: Some(NameOrId::Id(organization_id)), } => { - let (.., authz_disk) = - LookupPath::new(opctx, &self.db_datastore) - .organization_id(organization_id) - .project_name(&Name(project_name.clone())) - .disk_name(&Name(disk_name.clone())) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_disk) + let disk = LookupPath::new(opctx, &self.db_datastore) + .organization_id(*organization_id) + .project_name(Name::ref_cast(project_name)) + .disk_name(Name::ref_cast(disk_name)); + Ok(disk) } params::DiskSelector { disk: NameOrId::Name(disk_name), project: Some(NameOrId::Name(project_name)), organization: Some(NameOrId::Name(organization_name)), } => { - let (.., authz_disk) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(&Name(organization_name.clone())) - .project_name(&Name(project_name.clone())) - .disk_name(&Name(disk_name.clone())) - .lookup_for(authz::Action::Read) - .await?; - Ok(authz_disk) + let disk = LookupPath::new(opctx, &self.db_datastore) + .organization_name(Name::ref_cast(organization_name)) + .project_name(Name::ref_cast(project_name)) + .disk_name(Name::ref_cast(disk_name)); + Ok(disk) } _ => Err(Error::InvalidRequest { message: " @@ -99,10 +89,11 @@ impl super::Nexus { pub async fn project_create_disk( self: &Arc, opctx: &OpContext, - authz_project: &authz::Project, + project_lookup: &lookup::Project<'_>, params: ¶ms::DiskCreate, ) -> CreateResult { - opctx.authorize(authz::Action::CreateChild, authz_project).await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::CreateChild).await?; match ¶ms.disk_source { params::DiskSource::Blank { block_size } => { // Reject disks where the block size doesn't evenly divide the @@ -290,27 +281,16 @@ impl super::Nexus { pub async fn project_list_disks( &self, opctx: &OpContext, - authz_project: &authz::Project, + project_lookup: &lookup::Project<'_>, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - opctx.authorize(authz::Action::ListChildren, authz_project).await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore .project_list_disks(opctx, &authz_project, pagparams) .await } - pub async fn disk_fetch( - &self, - opctx: &OpContext, - authz_disk: &authz::Disk, - ) -> LookupResult { - let (.., db_disk) = LookupPath::new(opctx, &self.db_datastore) - .disk_id(authz_disk.id()) - .fetch() - .await?; - Ok(db_disk) - } - /// Modifies the runtime state of the Disk as requested. This generally /// means attaching or detaching the disk. // TODO(https://github.com/oxidecomputer/omicron/issues/811): @@ -413,10 +393,10 @@ impl super::Nexus { pub async fn project_delete_disk( self: &Arc, - opctx: &OpContext, - authz_disk: &authz::Disk, + disk_lookup: &lookup::Disk<'_>, ) -> DeleteResult { - opctx.authorize(authz::Action::Delete, authz_disk).await?; + let (.., authz_disk) = + disk_lookup.lookup_for(authz::Action::Delete).await?; let saga_params = sagas::disk_delete::Params { disk_id: authz_disk.id() }; diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 81a570922cf..7a75387bf20 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -712,12 +712,13 @@ impl super::Nexus { pub async fn instance_list_disks( &self, opctx: &OpContext, - authz_instance: &authz::Instance, + instance_lookup: &lookup::Instance<'_>, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - opctx.authorize(authz::Action::ListChildren, authz_instance).await?; + let (.., authz_instance) = + instance_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore - .instance_list_disks(opctx, authz_instance, pagparams) + .instance_list_disks(opctx, &authz_instance, pagparams) .await } @@ -725,9 +726,13 @@ impl super::Nexus { pub async fn instance_attach_disk( &self, opctx: &OpContext, - authz_instance: &authz::Instance, - authz_disk: &authz::Disk, + instance_lookup: &lookup::Instance<'_>, + disk_lookup: &lookup::Disk<'_>, ) -> UpdateResult { + let (.., authz_instance) = + instance_lookup.lookup_for(authz::Action::Modify).await?; + let (.., authz_disk) = + disk_lookup.lookup_for(authz::Action::Modify).await?; // TODO(https://github.com/oxidecomputer/omicron/issues/811): // Disk attach is only implemented for instances that are not // currently running. This operation therefore can operate exclusively @@ -757,9 +762,13 @@ impl super::Nexus { pub async fn instance_detach_disk( &self, opctx: &OpContext, - authz_instance: &authz::Instance, - authz_disk: &authz::Disk, + instance_lookup: &lookup::Instance<'_>, + disk_lookup: &lookup::Disk<'_>, ) -> UpdateResult { + let (.., authz_instance) = + instance_lookup.lookup_for(authz::Action::Modify).await?; + let (.., authz_disk) = + disk_lookup.lookup_for(authz::Action::Modify).await?; // TODO(https://github.com/oxidecomputer/omicron/issues/811): // Disk detach is only implemented for instances that are not // currently running. This operation therefore can operate exclusively diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 8064ce4be14..7fd597f25fb 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1823,16 +1823,14 @@ async fn disk_list_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let disks = if let Some(instance) = instance { - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(instance, &query.selector), - ) - .await?; + let instance_selector = + params::InstanceSelector::new(instance, &query.selector); + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; nexus .instance_list_disks( &opctx, - &authz_instance, + &instance_lookup, &data_page_params_for(&rqctx, &query.pagination)? .map_name(|n| Name::ref_cast(n)), ) @@ -1841,11 +1839,11 @@ async fn disk_list_v1( .map(|disk| disk.into()) .collect() } else if let Some(selector) = query.selector { - let authz_project = nexus.project_lookup(&opctx, selector).await?; + let project_lookup = nexus.project_lookup(&opctx, &selector)?; nexus .project_list_disks( &opctx, - &authz_project, + &project_lookup, &data_page_params_for(&rqctx, &query.pagination)? .map_name(|n| Name::ref_cast(n)), ) @@ -1869,10 +1867,12 @@ async fn disk_list_v1( } /// List disks +/// Use `GET /v1/disks` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/disks", - tags = ["disks"] + tags = ["disks"], + deprecated = true }] async fn disk_list( rqctx: Arc>>, @@ -1885,19 +1885,13 @@ async fn disk_list( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_project = nexus - .project_lookup( - &opctx, - params::ProjectSelector { - project: NameOrId::Name(project_name.clone().into()), - organization: Some(NameOrId::Name( - organization_name.clone().into(), - )), - }, - ) - .await?; + let authz_project = nexus.project_lookup(&opctx, &project_selector)?; let disks = nexus .project_list_disks( &opctx, @@ -1941,10 +1935,9 @@ async fn disk_create_v1( let new_disk_params = &new_disk.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_project = - nexus.project_lookup(&opctx, query.selector).await?; + let project_lookup = nexus.project_lookup(&opctx, &query.selector)?; let disk = nexus - .project_create_disk(&opctx, &authz_project, new_disk_params) + .project_create_disk(&opctx, &project_lookup, new_disk_params) .await?; Ok(HttpResponseCreated(disk.into())) }; @@ -1952,10 +1945,12 @@ async fn disk_create_v1( } // TODO-correctness See note about instance create. This should be async. +/// Use `POST /v1/disks` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/disks", - tags = ["disks"] + tags = ["disks"], + deprecated = true }] async fn disk_create( rqctx: Arc>>, @@ -1968,21 +1963,15 @@ async fn disk_create( let organization_name = &path.organization_name; let project_name = &path.project_name; let new_disk_params = &new_disk.into_inner(); + let project_selector = params::ProjectSelector { + project: NameOrId::Name(project_name.clone().into()), + organization: Some(NameOrId::Name(organization_name.clone().into())), + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_project = nexus - .project_lookup( - &opctx, - params::ProjectSelector { - project: NameOrId::Name(project_name.clone().into()), - organization: Some(NameOrId::Name( - organization_name.clone().into(), - )), - }, - ) - .await?; + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let disk = nexus - .project_create_disk(&opctx, &authz_project, &new_disk_params) + .project_create_disk(&opctx, &project_lookup, &new_disk_params) .await?; Ok(HttpResponseCreated(disk.into())) }; @@ -2021,15 +2010,11 @@ async fn disk_view_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); + let disk_selector = params::DiskSelector::new(path.disk, &query.selector); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_disk = nexus - .disk_lookup( - &opctx, - params::DiskSelector::new(path.disk, &query.selector), - ) - .await?; - let disk = nexus.disk_fetch(&opctx, &authz_disk).await?; + let (.., disk) = + nexus.disk_lookup(&opctx, &disk_selector)?.fetch().await?; Ok(HttpResponseOk(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2044,10 +2029,12 @@ struct DiskPathParam { } /// Fetch a disk +/// Use `GET /v1/disks/{disk}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}", tags = ["disks"], + deprecated = true }] async fn disk_view( rqctx: Arc>>, @@ -2059,31 +2046,27 @@ async fn disk_view( let organization_name = &path.organization_name; let project_name = &path.project_name; let disk_name = &path.disk_name; + let disk_selector = params::DiskSelector { + disk: NameOrId::Name(disk_name.clone().into()), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name(organization_name.clone().into())), + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk_id = nexus - .disk_lookup( - &opctx, - params::DiskSelector { - disk: NameOrId::Name(disk_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name( - organization_name.clone().into(), - )), - }, - ) - .await?; - let disk = nexus.disk_fetch(&opctx, &disk_id).await?; + let (.., disk) = + nexus.disk_lookup(&opctx, &disk_selector)?.fetch().await?; Ok(HttpResponseOk(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Fetch a disk by id +/// Use `GET /v1/disks/{disk}` instead #[endpoint { method = GET, path = "/by-id/disks/{id}", tags = ["disks"], + deprecated = true }] async fn disk_view_by_id( rqctx: Arc>>, @@ -2093,15 +2076,11 @@ async fn disk_view_by_id( let nexus = &apictx.nexus; let path = path_params.into_inner(); let id = &path.id; + let disk_selector = params::DiskSelector::new(NameOrId::Id(*id), &None); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_disk = nexus - .disk_lookup( - &opctx, - params::DiskSelector::new(NameOrId::Id(*id), &None), - ) - .await?; - let disk = nexus.disk_fetch(&opctx, &authz_disk).await?; + let (.., disk) = + nexus.disk_lookup(&opctx, &disk_selector)?.fetch().await?; Ok(HttpResponseOk(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2122,24 +2101,22 @@ async fn disk_delete_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); + let disk_selector = params::DiskSelector::new(path.disk, &query.selector); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_disk = nexus - .disk_lookup( - &opctx, - params::DiskSelector::new(path.disk, &query.selector), - ) - .await?; - nexus.project_delete_disk(&opctx, &authz_disk).await?; + let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; + nexus.project_delete_disk(&disk_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Use `DELETE /v1/disks/{disk}` instead #[endpoint { method = DELETE, path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}", tags = ["disks"], + deprecated = true }] async fn disk_delete( rqctx: Arc>>, @@ -2151,21 +2128,15 @@ async fn disk_delete( let organization_name = &path.organization_name; let project_name = &path.project_name; let disk_name = &path.disk_name; + let disk_selector = params::DiskSelector { + disk: NameOrId::Name(disk_name.clone().into()), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name(organization_name.clone().into())), + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let disk_id = nexus - .disk_lookup( - &opctx, - params::DiskSelector { - disk: NameOrId::Name(disk_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name( - organization_name.clone().into(), - )), - }, - ) - .await?; - nexus.project_delete_disk(&opctx, &disk_id).await?; + let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; + nexus.project_delete_disk(&disk_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2188,22 +2159,16 @@ async fn disk_attach_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let body = body.into_inner(); + let instance_selector = + params::InstanceSelector::new(body.instance, &query.selector); + let disk_selector = params::DiskSelector::new(path.disk, &query.selector); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(body.instance, &query.selector), - ) - .await?; - let authz_disk = nexus - .disk_lookup( - &opctx, - params::DiskSelector::new(path.disk, &query.selector), - ) - .await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; let disk = nexus - .instance_attach_disk(&opctx, &authz_instance, &authz_disk) + .instance_attach_disk(&opctx, &instance_lookup, &disk_lookup) .await?; Ok(HttpResponseAccepted(disk.into())) }; @@ -2227,22 +2192,16 @@ async fn disk_detach_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let body = body.into_inner(); + let instance_selector = + params::InstanceSelector::new(body.instance, &query.selector); + let disk_selector = params::DiskSelector::new(path.disk, &query.selector); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - params::InstanceSelector::new(body.instance, &query.selector), - ) - .await?; - let authz_disk = nexus - .disk_lookup( - &opctx, - params::DiskSelector::new(path.disk, &query.selector), - ) - .await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; let disk = nexus - .instance_detach_disk(&opctx, &authz_instance, &authz_disk) + .instance_detach_disk(&opctx, &instance_lookup, &disk_lookup) .await?; Ok(HttpResponseAccepted(disk.into())) }; @@ -2288,13 +2247,13 @@ async fn disk_metrics_list_v1( let query = query_params.into_inner(); let limit = rqctx.page_limit(&query.pagination)?; + let disk_selector = + params::DiskSelector::new(path.inner.disk, &query.selector); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_disk = nexus - .disk_lookup( - &opctx, - params::DiskSelector::new(path.inner.disk, &query.selector), - ) + let (.., authz_disk) = nexus + .disk_lookup(&opctx, &disk_selector)? + .lookup_for(authz::Action::Read) .await?; let result = nexus @@ -2313,10 +2272,12 @@ async fn disk_metrics_list_v1( } /// Fetch disk metrics +/// Use `GET /v1/disks/{disk}/metrics/{metric_name}` instead #[endpoint { method = GET, - path = "/v1/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name}", + path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name}", tags = ["disks"], + deprecated = true }] async fn disk_metrics_list( rqctx: Arc>>, @@ -2337,19 +2298,16 @@ async fn disk_metrics_list( let query = query_params.into_inner(); let limit = rqctx.page_limit(&query)?; + let disk_selector = params::DiskSelector { + disk: NameOrId::Name(disk_name.clone().into()), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name(organization_name.clone().into())), + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_disk = nexus - .disk_lookup( - &opctx, - params::DiskSelector { - disk: NameOrId::Name(disk_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name( - organization_name.clone().into(), - )), - }, - ) + let (.., authz_disk) = nexus + .disk_lookup(&opctx, &disk_selector)? + .lookup_for(authz::Action::Read) .await?; let result = nexus @@ -3082,10 +3040,12 @@ async fn instance_serial_console_stream( /// List an instance's disks // TODO-scalability needs to be paginated +/// Use `GET /v1/disks?instance={instance}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks", tags = ["instances"], + deprecated = true }] async fn instance_disk_list( rqctx: Arc>>, @@ -3099,24 +3059,19 @@ async fn instance_disk_list( 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 handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; let disks = nexus .instance_list_disks( &opctx, - &authz_instance, + &instance_lookup, &data_page_params_for(&rqctx, &query)? .map_name(|n| Name::ref_cast(n)), ) @@ -3134,10 +3089,12 @@ async fn instance_disk_list( } /// Attach a disk to an instance +/// Use `POST /v1/disks/{disk}/attach { instance: } ` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach", tags = ["instances"], + deprecated = true }] async fn instance_disk_attach( rqctx: Arc>>, @@ -3151,34 +3108,23 @@ async fn instance_disk_attach( let project_name = &path.project_name; let instance_name = &path.instance_name; let disk_name = disk_to_attach.into_inner().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 disk_selector = params::DiskSelector { + disk: NameOrId::Name(disk_name), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name(organization_name.clone().into())), + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; - let authz_disk = nexus - .disk_lookup( - &opctx, - params::DiskSelector { - disk: NameOrId::Name(disk_name), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name( - organization_name.clone().into(), - )), - }, - ) - .await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; let disk = nexus - .instance_attach_disk(&opctx, &authz_instance, &authz_disk) + .instance_attach_disk(&opctx, &instance_lookup, &disk_lookup) .await?; Ok(HttpResponseAccepted(disk.into())) }; @@ -3186,10 +3132,12 @@ async fn instance_disk_attach( } /// Detach a disk from an instance +/// Use `POST /v1/disks/{disk}/detach { instance: } ` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach", tags = ["instances"], + deprecated = true }] async fn instance_disk_detach( rqctx: Arc>>, @@ -3203,34 +3151,23 @@ async fn instance_disk_detach( let project_name = &path.project_name; let instance_name = &path.instance_name; let disk_name = disk_to_detach.into_inner().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 disk_selector = params::DiskSelector { + disk: NameOrId::Name(disk_name), + project: Some(NameOrId::Name(project_name.clone().into())), + organization: Some(NameOrId::Name(organization_name.clone().into())), + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_instance = nexus - .instance_lookup( - &opctx, - 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(), - )), - }, - ) - .await?; - let authz_disk = nexus - .disk_lookup( - &opctx, - params::DiskSelector { - disk: NameOrId::Name(disk_name), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name( - organization_name.clone().into(), - )), - }, - ) - .await?; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; let disk = nexus - .instance_detach_disk(&opctx, &authz_instance, &authz_disk) + .instance_detach_disk(&opctx, &instance_lookup, &disk_lookup) .await?; Ok(HttpResponseAccepted(disk.into())) }; diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 27da9db8808..e0e0862d27c 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -1,18 +1,18 @@ API operations found with tag "disks" OPERATION ID URL PATH -disk_attach_v1 /disks/{disk}/attach +disk_attach_v1 /v1/disks/{disk}/attach disk_create /organizations/{organization_name}/projects/{project_name}/disks -disk_create_v1 /disks +disk_create_v1 /v1/disks disk_delete /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} -disk_delete_v1 /disks/{disk} -disk_detach_v1 /disks/{disk}/detach +disk_delete_v1 /v1/disks/{disk} +disk_detach_v1 /v1/disks/{disk}/detach disk_list /organizations/{organization_name}/projects/{project_name}/disks -disk_list_v1 /disks +disk_list_v1 /v1/disks disk_metrics_list /organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name} -disk_metrics_list_v1 /disks/{disk}/metrics/{metric_name} +disk_metrics_list_v1 /v1/disks/{disk}/metrics/{metric_name} disk_view /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} disk_view_by_id /by-id/disks/{id} -disk_view_v1 /disks/{disk} +disk_view_v1 /v1/disks/{disk} API operations found with tag "hidden" OPERATION ID URL PATH diff --git a/openapi/nexus.json b/openapi/nexus.json index b3f6b1f5687..84a56d01576 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -16,6 +16,7 @@ "disks" ], "summary": "Fetch a disk by id", + "description": "Use `GET /v1/disks/{disk}` instead", "operationId": "disk_view_by_id", "parameters": [ { @@ -46,7 +47,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/by-id/images/{id}": { @@ -530,22 +532,14 @@ } } }, - "/disks": { + "/groups": { "get": { "tags": [ - "disks" + "silos" ], - "operationId": "disk_list_v1", + "summary": "List groups", + "operationId": "group_list", "parameters": [ - { - "in": "query", - "name": "instance", - "description": "Optional filter to narrow disks returned to those attached to the given instance", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - }, { "in": "query", "name": "limit", @@ -558,14 +552,6 @@ }, "style": "form" }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - }, { "in": "query", "name": "page_token", @@ -576,19 +562,11 @@ }, "style": "form" }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - }, { "in": "query", "name": "sort_by", "schema": { - "$ref": "#/components/schemas/NameSortMode" + "$ref": "#/components/schemas/IdSortMode" }, "style": "form" } @@ -599,7 +577,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DiskResultsPage" + "$ref": "#/components/schemas/GroupResultsPage" } } } @@ -612,52 +590,27 @@ } }, "x-dropshot-pagination": true - }, + } + }, + "/login": { "post": { "tags": [ - "disks" - ], - "summary": "Create a disk", - "operationId": "disk_create_v1", - "parameters": [ - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - }, - { - "in": "query", - "name": "project", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - } + "hidden" ], + "operationId": "login_spoof", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DiskCreate" + "$ref": "#/components/schemas/SpoofLoginBody" } } }, "required": true }, "responses": { - "201": { - "description": "successful creation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Disk" - } - } - } + "204": { + "description": "resource updated" }, "4XX": { "$ref": "#/components/responses/Error" @@ -668,47 +621,44 @@ } } }, - "/disks/{disk}": { - "get": { + "/login/{silo_name}/local": { + "post": { "tags": [ - "disks" + "login" ], - "operationId": "disk_view_v1", + "summary": "Authenticate a user (i.e., log in) via username and password", + "operationId": "login_local", "parameters": [ { "in": "path", - "name": "disk", - "description": "If Name is used to reference the disk 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", + "name": "silo_name", "required": true, "schema": { - "$ref": "#/components/schemas/NameOrId" + "$ref": "#/components/schemas/Name" }, "style": "simple" - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsernamePasswordCredentials" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { + "303": { + "description": "redirect (see other)", + "headers": { + "location": { + "description": "HTTP \"Location\" header", + "style": "simple", + "required": true, "schema": { - "$ref": "#/components/schemas/Disk" + "type": "string" } } } @@ -720,44 +670,49 @@ "$ref": "#/components/responses/Error" } } - }, - "delete": { + } + }, + "/login/{silo_name}/saml/{provider_name}": { + "get": { "tags": [ - "disks" + "login" ], - "summary": "Delete a disk", - "operationId": "disk_delete_v1", + "summary": "Prompt user login", + "description": "Either display a page asking a user for their credentials, or redirect them to their identity provider.", + "operationId": "login_saml_begin", "parameters": [ { "in": "path", - "name": "disk", - "description": "If Name is used to reference the disk 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", + "name": "provider_name", "required": true, "schema": { - "$ref": "#/components/schemas/NameOrId" + "$ref": "#/components/schemas/Name" }, "style": "simple" }, { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - }, - { - "in": "query", - "name": "project", + "in": "path", + "name": "silo_name", + "required": true, "schema": { - "$ref": "#/components/schemas/NameOrId" + "$ref": "#/components/schemas/Name" }, - "style": "form" + "style": "simple" } ], "responses": { - "204": { - "description": "successful deletion" + "302": { + "description": "redirect (found)", + "headers": { + "location": { + "description": "HTTP \"Location\" header", + "style": "simple", + "required": true, + "schema": { + "type": "string" + } + } + } }, "4XX": { "$ref": "#/components/responses/Error" @@ -766,60 +721,54 @@ "$ref": "#/components/responses/Error" } } - } - }, - "/disks/{disk}/attach": { + }, "post": { "tags": [ - "disks" + "login" ], - "summary": "Attach a disk to an instance", - "operationId": "disk_attach_v1", + "summary": "Authenticate a user (i.e., log in) via SAML", + "operationId": "login_saml", "parameters": [ { "in": "path", - "name": "disk", - "description": "If Name is used to reference the disk 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", + "name": "provider_name", "required": true, "schema": { - "$ref": "#/components/schemas/NameOrId" + "$ref": "#/components/schemas/Name" }, "style": "simple" }, { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - }, - { - "in": "query", - "name": "project", + "in": "path", + "name": "silo_name", + "required": true, "schema": { - "$ref": "#/components/schemas/NameOrId" + "$ref": "#/components/schemas/Name" }, - "style": "form" + "style": "simple" } ], "requestBody": { "content": { - "application/json": { + "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/InstanceIdentifier" + "type": "string", + "format": "binary" } } }, "required": true }, "responses": { - "202": { - "description": "successfully enqueued operation", - "content": { - "application/json": { + "303": { + "description": "redirect (see other)", + "headers": { + "location": { + "description": "HTTP \"Location\" header", + "style": "simple", + "required": true, "schema": { - "$ref": "#/components/schemas/Disk" + "type": "string" } } } @@ -833,439 +782,18 @@ } } }, - "/disks/{disk}/detach": { + "/logout": { "post": { "tags": [ - "disks" + "hidden" ], - "summary": "Detach a disk from an instance", - "operationId": "disk_detach_v1", - "parameters": [ - { - "in": "path", - "name": "disk", - "description": "If Name is used to reference the disk 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" - }, - "style": "simple" + "operationId": "logout", + "responses": { + "204": { + "description": "resource updated" }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceIdentifier" - } - } - }, - "required": true - }, - "responses": { - "202": { - "description": "successfully enqueued operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Disk" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/disks/{disk}/metrics/{metric_name}": { - "get": { - "tags": [ - "disks" - ], - "operationId": "disk_metrics_list_v1", - "parameters": [ - { - "in": "path", - "name": "disk", - "description": "If Name is used to reference the disk 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" - }, - "style": "simple" - }, - { - "in": "path", - "name": "metric_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/DiskMetricName" - }, - "style": "simple" - }, - { - "in": "query", - "name": "end_time", - "description": "An exclusive end time of metrics.", - "schema": { - "type": "string", - "format": "date-time" - }, - "style": "form" - }, - { - "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", - "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": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" - }, - { - "in": "query", - "name": "start_time", - "description": "An inclusive start time of metrics.", - "schema": { - "type": "string", - "format": "date-time" - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MeasurementResultsPage" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - }, - "x-dropshot-pagination": true - } - }, - "/groups": { - "get": { - "tags": [ - "silos" - ], - "summary": "List groups", - "operationId": "group_list", - "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/IdSortMode" - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GroupResultsPage" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - }, - "x-dropshot-pagination": true - } - }, - "/login": { - "post": { - "tags": [ - "hidden" - ], - "operationId": "login_spoof", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SpoofLoginBody" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "resource updated" - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/login/{silo_name}/local": { - "post": { - "tags": [ - "login" - ], - "summary": "Authenticate a user (i.e., log in) via username and password", - "operationId": "login_local", - "parameters": [ - { - "in": "path", - "name": "silo_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UsernamePasswordCredentials" - } - } - }, - "required": true - }, - "responses": { - "303": { - "description": "redirect (see other)", - "headers": { - "location": { - "description": "HTTP \"Location\" header", - "style": "simple", - "required": true, - "schema": { - "type": "string" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/login/{silo_name}/saml/{provider_name}": { - "get": { - "tags": [ - "login" - ], - "summary": "Prompt user login", - "description": "Either display a page asking a user for their credentials, or redirect them to their identity provider.", - "operationId": "login_saml_begin", - "parameters": [ - { - "in": "path", - "name": "provider_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - }, - { - "in": "path", - "name": "silo_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - } - ], - "responses": { - "302": { - "description": "redirect (found)", - "headers": { - "location": { - "description": "HTTP \"Location\" header", - "style": "simple", - "required": true, - "schema": { - "type": "string" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - }, - "post": { - "tags": [ - "login" - ], - "summary": "Authenticate a user (i.e., log in) via SAML", - "operationId": "login_saml", - "parameters": [ - { - "in": "path", - "name": "provider_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - }, - { - "in": "path", - "name": "silo_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - } - ], - "requestBody": { - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - }, - "required": true - }, - "responses": { - "303": { - "description": "redirect (see other)", - "headers": { - "location": { - "description": "HTTP \"Location\" header", - "style": "simple", - "required": true, - "schema": { - "type": "string" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/logout": { - "post": { - "tags": [ - "hidden" - ], - "operationId": "logout", - "responses": { - "204": { - "description": "resource updated" - }, - "4XX": { - "$ref": "#/components/responses/Error" + "4XX": { + "$ref": "#/components/responses/Error" }, "5XX": { "$ref": "#/components/responses/Error" @@ -1839,6 +1367,7 @@ "disks" ], "summary": "List disks", + "description": "Use `GET /v1/disks` instead", "operationId": "disk_list", "parameters": [ { @@ -1910,12 +1439,14 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { "tags": [ "disks" ], + "summary": "Use `POST /v1/disks` instead", "operationId": "disk_create", "parameters": [ { @@ -1966,7 +1497,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}": { @@ -1975,6 +1507,7 @@ "disks" ], "summary": "Fetch a disk", + "description": "Use `GET /v1/disks/{disk}` instead", "operationId": "disk_view", "parameters": [ { @@ -2022,12 +1555,14 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "disks" ], + "summary": "Use `DELETE /v1/disks/{disk}` instead", "operationId": "disk_delete", "parameters": [ { @@ -2068,7 +1603,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name}": { @@ -2077,6 +1613,7 @@ "disks" ], "summary": "Fetch disk metrics", + "description": "Use `GET /v1/disks/{disk}/metrics/{metric_name}` instead", "operationId": "disk_metrics_list", "parameters": [ { @@ -2176,6 +1713,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true } }, @@ -2669,6 +2207,7 @@ "instances" ], "summary": "List an instance's disks", + "description": "Use `GET /v1/disks?instance={instance}` instead", "operationId": "instance_disk_list", "parameters": [ { @@ -2747,6 +2286,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true } }, @@ -2756,6 +2296,7 @@ "instances" ], "summary": "Attach a disk to an instance", + "description": "Use `POST /v1/disks/{disk}/attach { instance: } ` instead", "operationId": "instance_disk_attach", "parameters": [ { @@ -2813,7 +2354,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach": { @@ -2822,6 +2364,7 @@ "instances" ], "summary": "Detach a disk from an instance", + "description": "Use `POST /v1/disks/{disk}/detach { instance: } ` instead", "operationId": "instance_disk_detach", "parameters": [ { @@ -2879,7 +2422,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/external-ips": { @@ -6649,22 +6193,202 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IpPoolUpdate" + "$ref": "#/components/schemas/IpPoolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system" + ], + "summary": "Delete an IP Pool", + "operationId": "ip_pool_delete", + "parameters": [ + { + "in": "path", + "name": "pool_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/system/ip-pools/{pool_name}/ranges": { + "get": { + "tags": [ + "system" + ], + "summary": "List ranges for an IP pool", + "description": "Ranges are ordered by their first address.", + "operationId": "ip_pool_range_list", + "parameters": [ + { + "in": "path", + "name": "pool_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, + { + "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" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRangeResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/system/ip-pools/{pool_name}/ranges/add": { + "post": { + "tags": [ + "system" + ], + "summary": "Add a range to an IP pool", + "operationId": "ip_pool_range_add", + "parameters": [ + { + "in": "path", + "name": "pool_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPoolRange" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/system/ip-pools/{pool_name}/ranges/remove": { + "post": { + "tags": [ + "system" + ], + "summary": "Remove a range from an IP pool", + "operationId": "ip_pool_range_remove", + "parameters": [ + { + "in": "path", + "name": "pool_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpRange" } } }, "required": true }, "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IpPool" - } - } - } + "204": { + "description": "resource updated" }, "4XX": { "$ref": "#/components/responses/Error" @@ -6673,27 +6397,37 @@ "$ref": "#/components/responses/Error" } } - }, - "delete": { + } + }, + "/system/ip-pools-service/{rack_id}": { + "get": { "tags": [ "system" ], - "summary": "Delete an IP Pool", - "operationId": "ip_pool_delete", + "summary": "Fetch an IP pool used for Oxide services.", + "operationId": "ip_pool_service_view", "parameters": [ { "in": "path", - "name": "pool_name", + "name": "rack_id", "required": true, "schema": { - "$ref": "#/components/schemas/Name" + "type": "string", + "format": "uuid" }, "style": "simple" } ], "responses": { - "204": { - "description": "successful deletion" + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IpPool" + } + } + } }, "4XX": { "$ref": "#/components/responses/Error" @@ -6704,21 +6438,22 @@ } } }, - "/system/ip-pools/{pool_name}/ranges": { + "/system/ip-pools-service/{rack_id}/ranges": { "get": { "tags": [ "system" ], - "summary": "List ranges for an IP pool", + "summary": "List ranges for an IP pool used for Oxide services.", "description": "Ranges are ordered by their first address.", - "operationId": "ip_pool_range_list", + "operationId": "ip_pool_service_range_list", "parameters": [ { "in": "path", - "name": "pool_name", + "name": "rack_id", "required": true, "schema": { - "$ref": "#/components/schemas/Name" + "type": "string", + "format": "uuid" }, "style": "simple" }, @@ -6766,20 +6501,21 @@ "x-dropshot-pagination": true } }, - "/system/ip-pools/{pool_name}/ranges/add": { + "/system/ip-pools-service/{rack_id}/ranges/add": { "post": { "tags": [ "system" ], - "summary": "Add a range to an IP pool", - "operationId": "ip_pool_range_add", + "summary": "Add a range to an IP pool used for Oxide services.", + "operationId": "ip_pool_service_range_add", "parameters": [ { "in": "path", - "name": "pool_name", + "name": "rack_id", "required": true, "schema": { - "$ref": "#/components/schemas/Name" + "type": "string", + "format": "uuid" }, "style": "simple" } @@ -6814,20 +6550,21 @@ } } }, - "/system/ip-pools/{pool_name}/ranges/remove": { + "/system/ip-pools-service/{rack_id}/ranges/remove": { "post": { "tags": [ "system" ], - "summary": "Remove a range from an IP pool", - "operationId": "ip_pool_range_remove", + "summary": "Remove a range from an IP pool used for Oxide services.", + "operationId": "ip_pool_service_range_remove", "parameters": [ { "in": "path", - "name": "pool_name", + "name": "rack_id", "required": true, "schema": { - "$ref": "#/components/schemas/Name" + "type": "string", + "format": "uuid" }, "style": "simple" } @@ -6855,32 +6592,55 @@ } } }, - "/system/ip-pools-service/{rack_id}": { + "/system/policy": { "get": { "tags": [ - "system" + "policy" ], - "summary": "Fetch an IP pool used for Oxide services.", - "operationId": "ip_pool_service_view", - "parameters": [ - { - "in": "path", - "name": "rack_id", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "simple" + "summary": "Fetch the top-level IAM policy", + "operationId": "system_policy_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" } + } + }, + "put": { + "tags": [ + "policy" ], + "summary": "Update the top-level IAM policy", + "operationId": "system_policy_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + }, + "required": true + }, "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IpPool" + "$ref": "#/components/schemas/FleetRolePolicy" } } } @@ -6894,25 +6654,14 @@ } } }, - "/system/ip-pools-service/{rack_id}/ranges": { + "/system/sagas": { "get": { "tags": [ "system" ], - "summary": "List ranges for an IP pool used for Oxide services.", - "description": "Ranges are ordered by their first address.", - "operationId": "ip_pool_service_range_list", + "summary": "List sagas", + "operationId": "saga_list", "parameters": [ - { - "in": "path", - "name": "rack_id", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "simple" - }, { "in": "query", "name": "limit", @@ -6934,6 +6683,14 @@ "type": "string" }, "style": "form" + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + }, + "style": "form" } ], "responses": { @@ -6942,7 +6699,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IpPoolRangeResultsPage" + "$ref": "#/components/schemas/SagaResultsPage" } } } @@ -6957,17 +6714,17 @@ "x-dropshot-pagination": true } }, - "/system/ip-pools-service/{rack_id}/ranges/add": { - "post": { + "/system/sagas/{saga_id}": { + "get": { "tags": [ "system" ], - "summary": "Add a range to an IP pool used for Oxide services.", - "operationId": "ip_pool_service_range_add", + "summary": "Fetch a saga", + "operationId": "saga_view", "parameters": [ { "in": "path", - "name": "rack_id", + "name": "saga_id", "required": true, "schema": { "type": "string", @@ -6976,23 +6733,13 @@ "style": "simple" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IpRange" - } - } - }, - "required": true - }, "responses": { - "201": { - "description": "successful creation", + "200": { + "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IpPoolRange" + "$ref": "#/components/schemas/Saga" } } } @@ -7006,39 +6753,93 @@ } } }, - "/system/ip-pools-service/{rack_id}/ranges/remove": { - "post": { + "/system/silos": { + "get": { "tags": [ "system" ], - "summary": "Remove a range from an IP pool used for Oxide services.", - "operationId": "ip_pool_service_range_remove", + "summary": "List silos", + "description": "Lists silos that are discoverable based on the current permissions.", + "operationId": "silo_list", "parameters": [ { - "in": "path", - "name": "rack_id", - "required": true, + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", "schema": { - "type": "string", - "format": "uuid" + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 }, - "style": "simple" + "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/SiloResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "system" ], + "summary": "Create a silo", + "operationId": "silo_create", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IpRange" + "$ref": "#/components/schemas/SiloCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Silo" + } } } }, - "required": true - }, - "responses": { - "204": { - "description": "resource updated" - }, "4XX": { "$ref": "#/components/responses/Error" }, @@ -7048,20 +6849,33 @@ } } }, - "/system/policy": { + "/system/silos/{silo_name}": { "get": { "tags": [ - "policy" + "system" + ], + "summary": "Fetch a silo", + "description": "Fetch a silo by name.", + "operationId": "silo_view", + "parameters": [ + { + "in": "path", + "name": "silo_name", + "description": "The silo's unique name.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } ], - "summary": "Fetch the top-level IAM policy", - "operationId": "system_policy_view", "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FleetRolePolicy" + "$ref": "#/components/schemas/Silo" } } } @@ -7074,32 +6888,28 @@ } } }, - "put": { + "delete": { "tags": [ - "policy" + "system" + ], + "summary": "Delete a silo", + "description": "Delete a silo by name.", + "operationId": "silo_delete", + "parameters": [ + { + "in": "path", + "name": "silo_name", + "description": "The silo's unique name.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } ], - "summary": "Update the top-level IAM policy", - "operationId": "system_policy_update", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FleetRolePolicy" - } - } - }, - "required": true - }, "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FleetRolePolicy" - } - } - } + "204": { + "description": "successful deletion" }, "4XX": { "$ref": "#/components/responses/Error" @@ -7110,14 +6920,24 @@ } } }, - "/system/sagas": { + "/system/silos/{silo_name}/identity-providers": { "get": { "tags": [ "system" ], - "summary": "List sagas", - "operationId": "saga_list", + "summary": "List a silo's IDPs", + "operationId": "silo_identity_provider_list", "parameters": [ + { + "in": "path", + "name": "silo_name", + "description": "The silo's unique name.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, { "in": "query", "name": "limit", @@ -7144,7 +6964,7 @@ "in": "query", "name": "sort_by", "schema": { - "$ref": "#/components/schemas/IdSortMode" + "$ref": "#/components/schemas/NameSortMode" }, "style": "form" } @@ -7155,7 +6975,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SagaResultsPage" + "$ref": "#/components/schemas/IdentityProviderResultsPage" } } } @@ -7170,32 +6990,43 @@ "x-dropshot-pagination": true } }, - "/system/sagas/{saga_id}": { - "get": { + "/system/silos/{silo_name}/identity-providers/local/users": { + "post": { "tags": [ "system" ], - "summary": "Fetch a saga", - "operationId": "saga_view", + "summary": "Create a user", + "description": "Users can only be created in Silos with `provision_type` == `Fixed`. Otherwise, Silo users are just-in-time (JIT) provisioned when a user first logs in using an external Identity Provider.", + "operationId": "local_idp_user_create", "parameters": [ { "in": "path", - "name": "saga_id", + "name": "silo_name", + "description": "The silo's unique name.", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/Name" }, "style": "simple" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "successful operation", + "201": { + "description": "successful creation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Saga" + "$ref": "#/components/schemas/User" } } } @@ -7209,53 +7040,138 @@ } } }, - "/system/silos": { - "get": { + "/system/silos/{silo_name}/identity-providers/local/users/{user_id}": { + "delete": { "tags": [ "system" ], - "summary": "List silos", - "description": "Lists silos that are discoverable based on the current permissions.", - "operationId": "silo_list", + "operationId": "local_idp_user_delete", "parameters": [ { - "in": "query", - "name": "limit", - "description": "Maximum number of items returned by a single call", + "in": "path", + "name": "silo_name", + "description": "The silo's unique name.", + "required": true, "schema": { - "nullable": true, - "type": "integer", - "format": "uint32", - "minimum": 1 + "$ref": "#/components/schemas/Name" }, - "style": "form" + "style": "simple" }, { - "in": "query", - "name": "page_token", - "description": "Token returned by previous call to retrieve the subsequent page", + "in": "path", + "name": "user_id", + "description": "The user's internal id", + "required": true, "schema": { - "nullable": true, - "type": "string" + "type": "string", + "format": "uuid" }, - "style": "form" + "style": "simple" + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/system/silos/{silo_name}/identity-providers/local/users/{user_id}/set-password": { + "post": { + "tags": [ + "system" + ], + "summary": "Set or invalidate a user's password", + "description": "Passwords can only be updated for users in Silos with identity mode `LocalOnly`.", + "operationId": "local_idp_user_set_password", + "parameters": [ { - "in": "query", - "name": "sort_by", + "in": "path", + "name": "silo_name", + "description": "The silo's unique name.", + "required": true, "schema": { - "$ref": "#/components/schemas/NameOrIdSortMode" + "$ref": "#/components/schemas/Name" }, - "style": "form" + "style": "simple" + }, + { + "in": "path", + "name": "user_id", + "description": "The user's internal id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPassword" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/system/silos/{silo_name}/identity-providers/saml": { + "post": { + "tags": [ + "system" + ], + "summary": "Create a SAML IDP", + "operationId": "saml_identity_provider_create", + "parameters": [ + { + "in": "path", + "name": "silo_name", + "description": "The silo's unique name.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProviderCreate" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "successful operation", + "201": { + "description": "successful creation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SiloResultsPage" + "$ref": "#/components/schemas/SamlIdentityProvider" } } } @@ -7266,32 +7182,45 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "x-dropshot-pagination": true - }, - "post": { + } + } + }, + "/system/silos/{silo_name}/identity-providers/saml/{provider_name}": { + "get": { "tags": [ "system" ], - "summary": "Create a silo", - "operationId": "silo_create", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SiloCreate" - } - } + "summary": "Fetch a SAML IDP", + "operationId": "saml_identity_provider_view", + "parameters": [ + { + "in": "path", + "name": "provider_name", + "description": "The SAML identity provider's name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" }, - "required": true - }, + { + "in": "path", + "name": "silo_name", + "description": "The silo's unique name.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } + ], "responses": { - "201": { - "description": "successful creation", + "200": { + "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Silo" + "$ref": "#/components/schemas/SamlIdentityProvider" } } } @@ -7305,14 +7234,13 @@ } } }, - "/system/silos/{silo_name}": { + "/system/silos/{silo_name}/policy": { "get": { "tags": [ "system" ], - "summary": "Fetch a silo", - "description": "Fetch a silo by name.", - "operationId": "silo_view", + "summary": "Fetch a silo's IAM policy", + "operationId": "silo_policy_view", "parameters": [ { "in": "path", @@ -7331,7 +7259,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Silo" + "$ref": "#/components/schemas/SiloRolePolicy" } } } @@ -7344,13 +7272,12 @@ } } }, - "delete": { + "put": { "tags": [ "system" ], - "summary": "Delete a silo", - "description": "Delete a silo by name.", - "operationId": "silo_delete", + "summary": "Update a silo's IAM policy", + "operationId": "silo_policy_update", "parameters": [ { "in": "path", @@ -7363,9 +7290,26 @@ "style": "simple" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + }, + "required": true + }, "responses": { - "204": { - "description": "successful deletion" + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } }, "4XX": { "$ref": "#/components/responses/Error" @@ -7376,13 +7320,13 @@ } } }, - "/system/silos/{silo_name}/identity-providers": { + "/system/silos/{silo_name}/users/all": { "get": { "tags": [ "system" ], - "summary": "List a silo's IDPs", - "operationId": "silo_identity_provider_list", + "summary": "List users in a specific Silo", + "operationId": "silo_users_list", "parameters": [ { "in": "path", @@ -7420,7 +7364,7 @@ "in": "query", "name": "sort_by", "schema": { - "$ref": "#/components/schemas/NameSortMode" + "$ref": "#/components/schemas/IdSortMode" }, "style": "form" } @@ -7431,7 +7375,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IdentityProviderResultsPage" + "$ref": "#/components/schemas/UserResultsPage" } } } @@ -7446,14 +7390,12 @@ "x-dropshot-pagination": true } }, - "/system/silos/{silo_name}/identity-providers/local/users": { - "post": { + "/system/silos/{silo_name}/users/id/{user_id}": { + "get": { "tags": [ "system" ], - "summary": "Create a user", - "description": "Users can only be created in Silos with `provision_type` == `Fixed`. Otherwise, Silo users are just-in-time (JIT) provisioned when a user first logs in using an external Identity Provider.", - "operationId": "local_idp_user_create", + "operationId": "silo_user_view", "parameters": [ { "in": "path", @@ -7464,21 +7406,22 @@ "$ref": "#/components/schemas/Name" }, "style": "simple" + }, + { + "in": "path", + "name": "user_id", + "description": "The user's internal id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserCreate" - } - } - }, - "required": true - }, "responses": { - "201": { - "description": "successful creation", + "200": { + "description": "successful operation", "content": { "application/json": { "schema": { @@ -7496,38 +7439,16 @@ } } }, - "/system/silos/{silo_name}/identity-providers/local/users/{user_id}": { - "delete": { + "/system/updates/refresh": { + "post": { "tags": [ "system" ], - "operationId": "local_idp_user_delete", - "parameters": [ - { - "in": "path", - "name": "silo_name", - "description": "The silo's unique name.", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - }, - { - "in": "path", - "name": "user_id", - "description": "The user's internal id", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "style": "simple" - } - ], + "summary": "Refresh update data", + "operationId": "updates_refresh", "responses": { "204": { - "description": "successful deletion" + "description": "resource updated" }, "4XX": { "$ref": "#/components/responses/Error" @@ -7538,96 +7459,52 @@ } } }, - "/system/silos/{silo_name}/identity-providers/local/users/{user_id}/set-password": { - "post": { + "/system/user": { + "get": { "tags": [ "system" ], - "summary": "Set or invalidate a user's password", - "description": "Passwords can only be updated for users in Silos with identity mode `LocalOnly`.", - "operationId": "local_idp_user_set_password", + "summary": "List built-in users", + "operationId": "system_user_list", "parameters": [ { - "in": "path", - "name": "silo_name", - "description": "The silo's unique name.", - "required": true, + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", "schema": { - "$ref": "#/components/schemas/Name" + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 }, - "style": "simple" + "style": "form" }, { - "in": "path", - "name": "user_id", - "description": "The user's internal id", - "required": true, + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", "schema": { - "type": "string", - "format": "uuid" + "nullable": true, + "type": "string" }, - "style": "simple" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserPassword" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "resource updated" - }, - "4XX": { - "$ref": "#/components/responses/Error" + "style": "form" }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/system/silos/{silo_name}/identity-providers/saml": { - "post": { - "tags": [ - "system" - ], - "summary": "Create a SAML IDP", - "operationId": "saml_identity_provider_create", - "parameters": [ { - "in": "path", - "name": "silo_name", - "description": "The silo's unique name.", - "required": true, + "in": "query", + "name": "sort_by", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameSortMode" }, - "style": "simple" + "style": "form" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SamlIdentityProviderCreate" - } - } - }, - "required": true - }, "responses": { - "201": { - "description": "successful creation", + "200": { + "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SamlIdentityProvider" + "$ref": "#/components/schemas/UserBuiltinResultsPage" } } } @@ -7638,31 +7515,22 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "x-dropshot-pagination": true } }, - "/system/silos/{silo_name}/identity-providers/saml/{provider_name}": { + "/system/user/{user_name}": { "get": { "tags": [ "system" ], - "summary": "Fetch a SAML IDP", - "operationId": "saml_identity_provider_view", + "summary": "Fetch a built-in user", + "operationId": "system_user_view", "parameters": [ { "in": "path", - "name": "provider_name", - "description": "The SAML identity provider's name", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - }, - { - "in": "path", - "name": "silo_name", - "description": "The silo's unique name.", + "name": "user_name", + "description": "The built-in user's unique name.", "required": true, "schema": { "$ref": "#/components/schemas/Name" @@ -7676,7 +7544,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SamlIdentityProvider" + "$ref": "#/components/schemas/UserBuiltin" } } } @@ -7690,23 +7558,35 @@ } } }, - "/system/silos/{silo_name}/policy": { + "/timeseries/schema": { "get": { "tags": [ - "system" + "metrics" ], - "summary": "Fetch a silo's IAM policy", - "operationId": "silo_policy_view", + "summary": "List timeseries schema", + "operationId": "timeseries_schema_get", "parameters": [ { - "in": "path", - "name": "silo_name", - "description": "The silo's unique name.", - "required": true, + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", "schema": { - "$ref": "#/components/schemas/Name" + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 }, - "style": "simple" + "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" } ], "responses": { @@ -7715,7 +7595,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SiloRolePolicy" + "$ref": "#/components/schemas/TimeseriesSchemaResultsPage" } } } @@ -7726,43 +7606,56 @@ "5XX": { "$ref": "#/components/responses/Error" } - } - }, - "put": { + }, + "x-dropshot-pagination": true + } + }, + "/users": { + "get": { "tags": [ - "system" + "silos" ], - "summary": "Update a silo's IAM policy", - "operationId": "silo_policy_update", + "summary": "List users", + "operationId": "user_list", "parameters": [ { - "in": "path", - "name": "silo_name", - "description": "The silo's unique name.", - "required": true, + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", "schema": { - "$ref": "#/components/schemas/Name" + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 }, - "style": "simple" + "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/IdSortMode" + }, + "style": "form" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SiloRolePolicy" - } - } - }, - "required": true - }, "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SiloRolePolicy" + "$ref": "#/components/schemas/UserResultsPage" } } } @@ -7773,26 +7666,25 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "x-dropshot-pagination": true } }, - "/system/silos/{silo_name}/users/all": { + "/v1/disks": { "get": { "tags": [ - "system" + "disks" ], - "summary": "List users in a specific Silo", - "operationId": "silo_users_list", + "operationId": "disk_list_v1", "parameters": [ { - "in": "path", - "name": "silo_name", - "description": "The silo's unique name.", - "required": true, + "in": "query", + "name": "instance", + "description": "Optional filter to narrow disks returned to those attached to the given instance", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, - "style": "simple" + "style": "form" }, { "in": "query", @@ -7806,6 +7698,14 @@ }, "style": "form" }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, { "in": "query", "name": "page_token", @@ -7816,11 +7716,19 @@ }, "style": "form" }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, { "in": "query", "name": "sort_by", "schema": { - "$ref": "#/components/schemas/IdSortMode" + "$ref": "#/components/schemas/NameSortMode" }, "style": "form" } @@ -7831,7 +7739,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserResultsPage" + "$ref": "#/components/schemas/DiskResultsPage" } } } @@ -7844,44 +7752,49 @@ } }, "x-dropshot-pagination": true - } - }, - "/system/silos/{silo_name}/users/id/{user_id}": { - "get": { + }, + "post": { "tags": [ - "system" + "disks" ], - "operationId": "silo_user_view", + "summary": "Create a disk", + "operationId": "disk_create_v1", "parameters": [ { - "in": "path", - "name": "silo_name", - "description": "The silo's unique name.", - "required": true, + "in": "query", + "name": "organization", "schema": { - "$ref": "#/components/schemas/Name" + "$ref": "#/components/schemas/NameOrId" }, - "style": "simple" + "style": "form" }, { - "in": "path", - "name": "user_id", - "description": "The user's internal id", + "in": "query", + "name": "project", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/NameOrId" }, - "style": "simple" + "style": "form" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskCreate" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "successful operation", + "201": { + "description": "successful creation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/Disk" } } } @@ -7895,16 +7808,50 @@ } } }, - "/system/updates/refresh": { - "post": { + "/v1/disks/{disk}": { + "get": { "tags": [ - "system" + "disks" + ], + "operationId": "disk_view_v1", + "parameters": [ + { + "in": "path", + "name": "disk", + "description": "If Name is used to reference the disk 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" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } ], - "summary": "Refresh update data", - "operationId": "updates_refresh", "responses": { - "204": { - "description": "resource updated" + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } }, "4XX": { "$ref": "#/components/responses/Error" @@ -7913,57 +7860,44 @@ "$ref": "#/components/responses/Error" } } - } - }, - "/system/user": { - "get": { + }, + "delete": { "tags": [ - "system" + "disks" ], - "summary": "List built-in users", - "operationId": "system_user_list", + "summary": "Delete a disk", + "operationId": "disk_delete_v1", "parameters": [ { - "in": "query", - "name": "limit", - "description": "Maximum number of items returned by a single call", + "in": "path", + "name": "disk", + "description": "If Name is used to reference the disk 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": { - "nullable": true, - "type": "integer", - "format": "uint32", - "minimum": 1 + "$ref": "#/components/schemas/NameOrId" }, - "style": "form" + "style": "simple" }, { "in": "query", - "name": "page_token", - "description": "Token returned by previous call to retrieve the subsequent page", + "name": "organization", "schema": { - "nullable": true, - "type": "string" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "sort_by", + "name": "project", "schema": { - "$ref": "#/components/schemas/NameSortMode" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" } ], "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserBuiltinResultsPage" - } - } - } + "204": { + "description": "successful deletion" }, "4XX": { "$ref": "#/components/responses/Error" @@ -7971,36 +7905,61 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "x-dropshot-pagination": true + } } }, - "/system/user/{user_name}": { - "get": { + "/v1/disks/{disk}/attach": { + "post": { "tags": [ - "system" + "disks" ], - "summary": "Fetch a built-in user", - "operationId": "system_user_view", + "summary": "Attach a disk to an instance", + "operationId": "disk_attach_v1", "parameters": [ { "in": "path", - "name": "user_name", - "description": "The built-in user's unique name.", + "name": "disk", + "description": "If Name is used to reference the disk 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/Name" + "$ref": "#/components/schemas/NameOrId" }, "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceIdentifier" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "successful operation", + "202": { + "description": "successfully enqueued operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserBuiltin" + "$ref": "#/components/schemas/Disk" } } } @@ -8014,44 +7973,58 @@ } } }, - "/timeseries/schema": { - "get": { + "/v1/disks/{disk}/detach": { + "post": { "tags": [ - "metrics" + "disks" ], - "summary": "List timeseries schema", - "operationId": "timeseries_schema_get", + "summary": "Detach a disk from an instance", + "operationId": "disk_detach_v1", "parameters": [ + { + "in": "path", + "name": "disk", + "description": "If Name is used to reference the disk 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" + }, + "style": "simple" + }, { "in": "query", - "name": "limit", - "description": "Maximum number of items returned by a single call", + "name": "organization", "schema": { - "nullable": true, - "type": "integer", - "format": "uint32", - "minimum": 1 + "$ref": "#/components/schemas/NameOrId" }, "style": "form" }, { "in": "query", - "name": "page_token", - "description": "Token returned by previous call to retrieve the subsequent page", + "name": "project", "schema": { - "nullable": true, - "type": "string" + "$ref": "#/components/schemas/NameOrId" }, "style": "form" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceIdentifier" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "successful operation", + "202": { + "description": "successfully enqueued operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TimeseriesSchemaResultsPage" + "$ref": "#/components/schemas/Disk" } } } @@ -8062,18 +8035,45 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "x-dropshot-pagination": true + } } }, - "/users": { + "/v1/disks/{disk}/metrics/{metric_name}": { "get": { "tags": [ - "silos" + "disks" ], - "summary": "List users", - "operationId": "user_list", + "operationId": "disk_metrics_list_v1", "parameters": [ + { + "in": "path", + "name": "disk", + "description": "If Name is used to reference the disk 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" + }, + "style": "simple" + }, + { + "in": "path", + "name": "metric_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/DiskMetricName" + }, + "style": "simple" + }, + { + "in": "query", + "name": "end_time", + "description": "An exclusive end time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + }, + "style": "form" + }, { "in": "query", "name": "limit", @@ -8086,6 +8086,14 @@ }, "style": "form" }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, { "in": "query", "name": "page_token", @@ -8098,9 +8106,19 @@ }, { "in": "query", - "name": "sort_by", + "name": "project", "schema": { - "$ref": "#/components/schemas/IdSortMode" + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "start_time", + "description": "An inclusive start time of metrics.", + "schema": { + "type": "string", + "format": "date-time" }, "style": "form" } @@ -8111,7 +8129,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserResultsPage" + "$ref": "#/components/schemas/MeasurementResultsPage" } } } @@ -13859,29 +13877,6 @@ } } }, - "NameSortMode": { - "description": "Supported set of sort modes for scanning by name only\n\nCurrently, we only support scanning in ascending order.", - "oneOf": [ - { - "description": "sort in increasing order of \"name\"", - "type": "string", - "enum": [ - "name_ascending" - ] - } - ] - }, - "DiskMetricName": { - "type": "string", - "enum": [ - "activated", - "flush", - "read", - "read_bytes", - "write", - "write_bytes" - ] - }, "IdSortMode": { "description": "Supported set of sort modes for scanning by id only.\n\nCurrently, we only support scanning in ascending order.", "oneOf": [ @@ -13919,6 +13914,29 @@ ] } ] + }, + "NameSortMode": { + "description": "Supported set of sort modes for scanning by name only\n\nCurrently, we only support scanning in ascending order.", + "oneOf": [ + { + "description": "sort in increasing order of \"name\"", + "type": "string", + "enum": [ + "name_ascending" + ] + } + ] + }, + "DiskMetricName": { + "type": "string", + "enum": [ + "activated", + "flush", + "read", + "read_bytes", + "write", + "write_bytes" + ] } } }, From 8d51dbb8a05ea6e45d86bed5409b2423efc9100a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 13 Dec 2022 10:08:34 -0500 Subject: [PATCH 46/72] Revamp selectors (again), convert organizations --- nexus/db-model/src/name.rs | 2 + nexus/src/app/instance.rs | 60 +-- nexus/src/app/organization.rs | 76 ++- nexus/src/app/project.rs | 40 +- nexus/src/external_api/http_entrypoints.rs | 512 ++++++++++++++++----- nexus/types/src/external_api/params.rs | 39 +- 6 files changed, 472 insertions(+), 257 deletions(-) diff --git a/nexus/db-model/src/name.rs b/nexus/db-model/src/name.rs index 96603530333..11d3d6574c6 100644 --- a/nexus/db-model/src/name.rs +++ b/nexus/db-model/src/name.rs @@ -37,6 +37,8 @@ use serde::{Deserialize, Serialize}; #[display("{0}")] pub struct Name(pub external::Name); +// impl Into for Name {} + NewtypeFrom! { () pub struct Name(external::Name); } NewtypeDeref! { () pub struct Name(external::Name); } diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index ec4f0f574ac..bab5e77cbd2 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -62,56 +62,26 @@ impl super::Nexus { instance_selector: &'a params::InstanceSelector, ) -> LookupResult> { match instance_selector { - params::InstanceSelector { instance: NameOrId::Id(id), .. } => { - // TODO: 400 if project or organization are present + params::InstanceSelector(NameOrId::Id(id), ..) => { let instance = LookupPath::new(opctx, &self.db_datastore).instance_id(*id); Ok(instance) } - params::InstanceSelector { - instance: NameOrId::Name(instance_name), - project: Some(NameOrId::Id(project_id)), - .. - } => { - // TODO: 400 if organization is present - let instance = LookupPath::new(opctx, &self.db_datastore) - .project_id(*project_id) - .instance_name(Name::ref_cast(instance_name)); - Ok(instance) - } - params::InstanceSelector { - instance: NameOrId::Name(instance_name), - project: Some(NameOrId::Name(project_name)), - organization: Some(NameOrId::Id(organization_id)), - } => { - let instance = LookupPath::new(opctx, &self.db_datastore) - .organization_id(*organization_id) - .project_name(Name::ref_cast(project_name)) - .instance_name(Name::ref_cast(instance_name)); - Ok(instance) - } - params::InstanceSelector { - instance: NameOrId::Name(instance_name), - project: Some(NameOrId::Name(project_name)), - organization: Some(NameOrId::Name(organization_name)), - } => { - let instance = LookupPath::new(opctx, &self.db_datastore) - .organization_name(Name::ref_cast(organization_name)) - .project_name(Name::ref_cast(project_name)) - .instance_name(Name::ref_cast(instance_name)); - Ok(instance) + params::InstanceSelector( + NameOrId::Name(name), + project_selector, + ) => { + if let Some(project) = project_selector { + let instance = self + .project_lookup(opctx, project)? + .instance_name(Name::ref_cast(name)); + Ok(instance) + } else { + Err(Error::InvalidRequest { + message: "Unable to resolve instance by name without instance".to_string(), + }) + } } - // TODO: Add a better error message - _ => Err(Error::InvalidRequest { - message: " - Unable to resolve instance. Expected one of - - instance: Uuid - - instance: Name, project: Uuid - - instance: Name, project: Name, organization: Uuid - - instance: Name, project: Name, organization: Name - " - .to_string(), - }), } } diff --git a/nexus/src/app/organization.rs b/nexus/src/app/organization.rs index d7783d2a7a6..a1fae4997db 100644 --- a/nexus/src/app/organization.rs +++ b/nexus/src/app/organization.rs @@ -7,6 +7,7 @@ use crate::authz; use crate::context::OpContext; use crate::db; +use crate::db::lookup; use crate::db::lookup::LookupPath; use crate::db::model::Name; use crate::external_api::params; @@ -18,10 +19,30 @@ use omicron_common::api::external::DeleteResult; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; +use ref_cast::RefCast; use uuid::Uuid; impl super::Nexus { + pub fn organization_lookup<'a>( + &'a self, + opctx: &'a OpContext, + organization_selector: &'a params::OrganizationSelector, + ) -> LookupResult> { + match organization_selector { + params::OrganizationSelector(NameOrId::Id(id)) => { + let organization = LookupPath::new(opctx, &self.db_datastore) + .organization_id(*id); + Ok(organization) + } + params::OrganizationSelector(NameOrId::Name(name)) => { + let organization = LookupPath::new(opctx, &self.db_datastore) + .organization_name(Name::ref_cast(name)); + Ok(organization) + } + } + } pub async fn organization_create( &self, opctx: &OpContext, @@ -30,30 +51,6 @@ impl super::Nexus { self.db_datastore.organization_create(opctx, new_organization).await } - pub async fn organization_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - ) -> LookupResult { - let (.., db_organization) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .fetch() - .await?; - Ok(db_organization) - } - - pub async fn organization_fetch_by_id( - &self, - opctx: &OpContext, - organization_id: &Uuid, - ) -> LookupResult { - let (.., db_organization) = LookupPath::new(opctx, &self.db_datastore) - .organization_id(*organization_id) - .fetch() - .await?; - Ok(db_organization) - } - pub async fn organizations_list_by_name( &self, opctx: &OpContext, @@ -73,27 +70,20 @@ impl super::Nexus { pub async fn organization_delete( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, ) -> DeleteResult { - let (.., authz_org, db_org) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .fetch() - .await?; + let (.., authz_org, db_org) = organization_lookup.fetch().await?; self.db_datastore.organization_delete(opctx, &authz_org, &db_org).await } pub async fn organization_update( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, new_params: ¶ms::OrganizationUpdate, ) -> UpdateResult { let (.., authz_organization) = - LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::Modify) - .await?; + organization_lookup.lookup_for(authz::Action::Modify).await?; self.db_datastore .organization_update( opctx, @@ -108,12 +98,10 @@ impl super::Nexus { pub async fn organization_fetch_policy( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, ) -> LookupResult> { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::ReadPolicy) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::ReadPolicy).await?; let role_assignments = self .db_datastore .role_assignment_fetch_visible(opctx, &authz_org) @@ -128,13 +116,11 @@ impl super::Nexus { pub async fn organization_update_policy( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, policy: &shared::Policy, ) -> UpdateResult> { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::ModifyPolicy) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::ModifyPolicy).await?; let role_assignments = self .db_datastore diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index 07c4c73ceec..0166bde2a6d 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -33,39 +33,23 @@ impl super::Nexus { project_selector: &'a params::ProjectSelector, ) -> LookupResult> { match project_selector { - params::ProjectSelector { project: NameOrId::Id(id), .. } => { - // TODO: 400 if organization is present + params::ProjectSelector(NameOrId::Id(id), ..) => { let project = LookupPath::new(opctx, &self.db_datastore).project_id(*id); Ok(project) } - params::ProjectSelector { - project: NameOrId::Name(project_name), - organization: Some(NameOrId::Id(organization_id)), - } => { - let project = LookupPath::new(opctx, &self.db_datastore) - .organization_id(*organization_id) - .project_name(Name::ref_cast(project_name)); - Ok(project) - } - params::ProjectSelector { - project: NameOrId::Name(project_name), - organization: Some(NameOrId::Name(organization_name)), - } => { - let project = LookupPath::new(opctx, &self.db_datastore) - .organization_name(Name::ref_cast(organization_name)) - .project_name(Name::ref_cast(project_name)); - Ok(project) + params::ProjectSelector(NameOrId::Name(name), org_selector) => { + if let Some(org) = org_selector { + let project = self + .organization_lookup(opctx, org)? + .project_name(Name::ref_cast(name)); + Ok(project) + } else { + Err(Error::InvalidRequest { + message: "Unable to resolve project by name without organization".to_string(), + }) + } } - _ => Err(Error::InvalidRequest { - message: " - Unable to resolve project. Expected one of - - project: Uuid - - project: Name, organization: Uuid - - project: Name, organization: Name - " - .to_string(), - }), } } pub async fn project_create( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 50b3435f5c8..6d342177715 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -98,6 +98,14 @@ pub fn external_api() -> NexusApiDescription { api.register(organization_policy_view)?; api.register(organization_policy_update)?; + api.register(organization_list_v1)?; + api.register(organization_create_v1)?; + api.register(organization_view_v1)?; + api.register(organization_delete_v1)?; + api.register(organization_update_v1)?; + api.register(organization_policy_view_v1)?; + api.register(organization_policy_update_v1)?; + api.register(project_list)?; api.register(project_create)?; api.register(project_view)?; @@ -918,10 +926,55 @@ async fn local_idp_user_set_password( } /// List organizations +#[endpoint { + method = GET, + path = "/v1/organizations", + tags = ["organizations"] +}] +async fn organization_list_v1( + rqctx: Arc>>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let query = query_params.into_inner(); + let params = ScanByNameOrId::from_query(&query)?; + let field = pagination_field_for_scan_params(params); + + let organizations = match field { + PagField::Id => { + let page_selector = data_page_params_nameid_id(&rqctx, &query)?; + nexus.organizations_list_by_id(&opctx, &page_selector).await? + } + + PagField::Name => { + let page_selector = + data_page_params_nameid_name(&rqctx, &query)? + .map_name(|n| Name::ref_cast(n)); + nexus.organizations_list_by_name(&opctx, &page_selector).await? + } + } + .into_iter() + .map(|p| p.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + organizations, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// List organizations +/// Use `/v1/organizations` instead #[endpoint { method = GET, path = "/organizations", tags = ["organizations"], + deprecated = true }] async fn organization_list( rqctx: Arc>>, @@ -961,10 +1014,34 @@ async fn organization_list( } /// Create an organization +#[endpoint { + method = POST, + path = "/v1/organizations", + tags = ["organizations"], +}] +async fn organization_create_v1( + rqctx: Arc>>, + new_organization: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization = nexus + .organization_create(&opctx, &new_organization.into_inner()) + .await?; + Ok(HttpResponseCreated(organization.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Create an organization +/// Use `POST /v1/organizations` instead #[endpoint { method = POST, path = "/organizations", tags = ["organizations"], + deprecated = true }] async fn organization_create( rqctx: Arc>>, @@ -982,6 +1059,37 @@ async fn organization_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +struct OrganizationLookupPathParam { + organization: NameOrId, +} + +#[endpoint { + method = GET, + path = "/v1/organizations/{organization}", + tags = ["organizations"], +}] +async fn organization_view_v1( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let (.., organization) = nexus + .organization_lookup( + &opctx, + ¶ms::OrganizationSelector(path.organization), + )? + .fetch() + .await?; + Ok(HttpResponseOk(organization.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Organization requests #[derive(Deserialize, JsonSchema)] struct OrganizationPathParam { @@ -990,10 +1098,12 @@ struct OrganizationPathParam { } /// Fetch an organization +/// Use `GET /v1/organizations/{organization}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}", tags = ["organizations"], + deprecated = true }] async fn organization_view( rqctx: Arc>>, @@ -1005,18 +1115,27 @@ async fn organization_view( let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization = - nexus.organization_fetch(&opctx, &organization_name).await?; + let (.., organization) = nexus + .organization_lookup( + &opctx, + ¶ms::OrganizationSelector(NameOrId::Name( + path.organization_name.into(), + )), + )? + .fetch() + .await?; Ok(HttpResponseOk(organization.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Fetch an organization by id +/// Use `GET /v1/organizations/{organization}` instead #[endpoint { method = GET, path = "/by-id/organizations/{id}", tags = ["organizations"], + deprecated = true }] async fn organization_view_by_id( rqctx: Arc>>, @@ -1025,20 +1144,51 @@ async fn organization_view_by_id( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization = nexus.organization_fetch_by_id(&opctx, id).await?; + let (.., organization) = nexus + .organization_lookup( + &opctx, + ¶ms::OrganizationSelector(NameOrId::Id(path.id.into())), + )? + .fetch() + .await?; Ok(HttpResponseOk(organization.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = DELETE, + path = "/v1/organizations/{organization}", + tags = ["organizations"], +}] +async fn organization_delete_v1( + rqctx: Arc>>, + path_params: Path, +) -> Result { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(params.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + nexus.organization_delete(&opctx, &organization_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete an organization +/// Use `DELETE /v1/organizations/{organization}` instead #[endpoint { method = DELETE, path = "/organizations/{organization_name}", tags = ["organizations"], + deprecated = true }] async fn organization_delete( rqctx: Arc>>, @@ -1047,25 +1197,62 @@ async fn organization_delete( let apictx = rqctx.context(); let nexus = &apictx.nexus; let params = path_params.into_inner(); - let organization_name = ¶ms.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus.organization_delete(&opctx, &organization_name).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(params.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + nexus.organization_delete(&opctx, &organization_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = PUT, + path = "/v1/organizations/{organization}", + tags = ["organizations"], +}] +async fn organization_update_v1( + rqctx: Arc>>, + path_params: Path, + updated_organization: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(params.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let new_organization = nexus + .organization_update( + &opctx, + &organization_lookup, + &updated_organization.into_inner(), + ) + .await?; + Ok(HttpResponseOk(new_organization.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Update an organization // TODO-correctness: Is it valid for PUT to accept application/json that's a // subset of what the resource actually represents? If not, is that a problem? // (HTTP may require that this be idempotent.) If so, can we get around that // having this be a slightly different content-type (e.g., // "application/json-patch")? We should see what other APIs do. +/// Use `PUT /v1/organizations/{organization}` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}", tags = ["organizations"], + deprecated = true }] async fn organization_update( rqctx: Arc>>, @@ -1078,10 +1265,15 @@ async fn organization_update( let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; let new_organization = nexus .organization_update( &opctx, - &organization_name, + &organization_lookup, &updated_organization.into_inner(), ) .await?; @@ -1090,11 +1282,40 @@ async fn organization_update( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = GET, + path = "/v1/organizations/{organization}/policy", + tags = ["organizations"], +}] +async fn organization_policy_view_v1( + rqctx: Arc>>, + path_params: Path, +) -> Result>, HttpError> +{ + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(params.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let policy = nexus + .organization_fetch_policy(&opctx, &organization_lookup) + .await?; + Ok(HttpResponseOk(policy.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Fetch an organization's IAM policy +/// Use `GET /v1/organizations/{organization}/policy` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/policy", tags = ["organizations"], + deprecated = true }] async fn organization_policy_view( rqctx: Arc>>, @@ -1104,22 +1325,65 @@ async fn organization_policy_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let policy = - nexus.organization_fetch_policy(&opctx, organization_name).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let policy = nexus + .organization_fetch_policy(&opctx, &organization_lookup) + .await?; + Ok(HttpResponseOk(policy)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +#[endpoint { + method = PUT, + path = "/v1/organizations/{organization}/policy", + tags = ["organizations"], +}] +async fn organization_policy_update_v1( + rqctx: Arc>>, + path_params: Path, + new_policy: TypedBody>, +) -> Result>, HttpError> +{ + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let new_policy = new_policy.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(params.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let nasgns = new_policy.role_assignments.len(); + // This should have been validated during parsing. + bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); + let policy = nexus + .organization_update_policy( + &opctx, + &organization_lookup, + &new_policy, + ) + .await?; Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Update an organization's IAM policy +/// Use `PUT /v1/organizations/{organization}/policy` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}/policy", tags = ["organizations"], + deprecated = true }] async fn organization_policy_update( rqctx: Arc>>, @@ -1131,15 +1395,22 @@ async fn organization_policy_update( let nexus = &apictx.nexus; let path = path_params.into_inner(); let new_policy = new_policy.into_inner(); - let organization_name = &path.organization_name; - let handler = async { let nasgns = new_policy.role_assignments.len(); // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus - .organization_update_policy(&opctx, organization_name, &new_policy) + .organization_update_policy( + &opctx, + &organization_lookup, + &new_policy, + ) .await?; Ok(HttpResponseOk(policy)) }; @@ -2014,8 +2285,8 @@ async fn disk_metrics_list( struct InstanceListQueryParams { #[serde(flatten)] pagination: PaginatedByName, - #[serde(flatten)] - selector: params::ProjectSelector, + project: NameOrId, + organization: Option, } #[endpoint { @@ -2032,7 +2303,9 @@ async fn instance_list_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_project = nexus.project_lookup(&opctx, &query.selector)?; + let project_selector = + ¶ms::ProjectSelector::new(query.organization, query.project); + let authz_project = nexus.project_lookup(&opctx, project_selector)?; let instances = nexus .project_list_instances( &opctx, @@ -2068,12 +2341,10 @@ async fn instance_list( let nexus = &apictx.nexus; let query = query_params.into_inner(); let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let project_selector = params::ProjectSelector { - project: NameOrId::Name(project_name.clone().into()), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; @@ -2099,8 +2370,8 @@ async fn instance_list( #[derive(Deserialize, JsonSchema)] struct InstanceCreateParams { - #[serde(flatten)] - selector: params::ProjectSelector, + organization: Option, + project: NameOrId, } #[endpoint { @@ -2119,7 +2390,9 @@ async fn instance_create_v1( let new_instance_params = &new_instance.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_lookup = nexus.project_lookup(&opctx, &query.selector)?; + let project_selector = + ¶ms::ProjectSelector::new(query.organization, query.project); + let project_lookup = nexus.project_lookup(&opctx, project_selector)?; let instance = nexus .project_create_instance( &opctx, @@ -2153,13 +2426,11 @@ async fn instance_create( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; let new_instance_params = &new_instance.into_inner(); - let project_selector = params::ProjectSelector { - project: NameOrId::Name(project_name.clone().into()), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; @@ -2189,8 +2460,8 @@ struct InstanceLookupPathParam { #[derive(Deserialize, JsonSchema)] struct InstanceQueryParams { - #[serde(flatten)] - selector: Option, + organization: Option, + project: Option, } #[endpoint { @@ -2209,11 +2480,15 @@ async fn instance_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); - let instance_selector = + // let instance_selector = params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); + let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; - let (.., instance) = instance_selector.fetch().await?; + let (.., instance) = instance_lookup.fetch().await?; Ok(HttpResponseOk(instance.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2240,14 +2515,11 @@ async fn instance_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2277,11 +2549,7 @@ async fn instance_view_by_id( let (.., instance) = nexus .instance_lookup( &opctx, - ¶ms::InstanceSelector { - instance: NameOrId::Id(*id), - project: None, - organization: None, - }, + ¶ms::InstanceSelector::new(None, None, NameOrId::Id(*id)), )? .fetch() .await?; @@ -2304,8 +2572,11 @@ async fn instance_delete_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2329,14 +2600,11 @@ async fn instance_delete( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2364,8 +2632,11 @@ async fn instance_migrate_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let migrate_instance_params = migrate_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2397,15 +2668,12 @@ async fn instance_migrate( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; let migrate_instance_params = migrate_params.into_inner(); - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2436,8 +2704,11 @@ async fn instance_reboot_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2461,14 +2732,11 @@ async fn instance_reboot( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2494,8 +2762,11 @@ async fn instance_start_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2519,14 +2790,11 @@ async fn instance_start( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2551,8 +2819,11 @@ async fn instance_stop_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2576,14 +2847,11 @@ async fn instance_stop( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2596,8 +2864,8 @@ async fn instance_stop( #[derive(Deserialize, JsonSchema)] pub struct InstanceSerialConsoleParams { - #[serde(flatten)] - selector: Option, + organization: Option, + project: Option, #[serde(flatten)] pub console_params: params::InstanceSerialConsoleRequest, @@ -2617,8 +2885,11 @@ async fn instance_serial_console_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2648,14 +2919,11 @@ async fn instance_serial_console( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2687,8 +2955,11 @@ async fn instance_serial_console_stream_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_selector = - params::InstanceSelector::new(path.instance, &query.selector); + let instance_selector = params::InstanceSelector::new( + query.organization, + query.project, + path.instance, + ); let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; nexus.instance_serial_console_stream(conn, &instance_lookup).await?; Ok(()) @@ -2708,15 +2979,12 @@ async fn instance_serial_console_stream( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let instance_name = &path.instance_name; let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_selector = params::InstanceSelector { - instance: NameOrId::Name(instance_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + Some(NameOrId::Name(path.project_name.into())), + NameOrId::Name(path.instance_name.into()), + ); let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; nexus.instance_serial_console_stream(conn, &instance_lookup).await?; Ok(()) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index dd57a3935dc..bf47b856ed0 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -18,31 +18,36 @@ use serde::{ use std::{net::IpAddr, str::FromStr}; use uuid::Uuid; -#[derive(Deserialize, JsonSchema)] -pub struct ProjectSelector { - pub project: NameOrId, - pub organization: Option, -} +pub struct OrganizationSelector(pub NameOrId); + +pub struct ProjectSelector(pub NameOrId, pub Option); -#[derive(Deserialize, JsonSchema)] -pub struct InstanceSelector { - pub instance: NameOrId, - pub project: Option, - pub organization: Option, +impl ProjectSelector { + pub fn new( + organization: Option, + project: NameOrId, + ) -> ProjectSelector { + ProjectSelector(project, organization.map(|o| OrganizationSelector(o))) + } } +pub struct InstanceSelector(pub NameOrId, pub Option); + impl InstanceSelector { pub fn new( + organization: Option, + project: Option, instance: NameOrId, - project_selector: &Option, ) -> InstanceSelector { - InstanceSelector { + InstanceSelector( instance, - organization: project_selector - .as_ref() - .and_then(|s| s.organization.clone()), - project: project_selector.as_ref().map(|s| s.project.clone()), - } + project.map(|p| { + ProjectSelector( + p, + organization.map(|o| OrganizationSelector(o)), + ) + }), + ) } } From 129011eec752278a49ca0ffd4265dea3ddfdab03 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 13 Dec 2022 16:58:51 -0500 Subject: [PATCH 47/72] Add project APIs, update routes --- nexus/src/app/project.rs | 99 +-- nexus/src/app/vpc.rs | 11 +- nexus/src/external_api/http_entrypoints.rs | 377 ++++++++++-- nexus/tests/output/nexus_tags.txt | 14 + openapi/nexus.json | 685 ++++++++++++++++++++- 5 files changed, 1043 insertions(+), 143 deletions(-) diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index 0166bde2a6d..5051bfb62c6 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -14,6 +14,7 @@ use crate::external_api::params; use crate::external_api::shared; use anyhow::Context; use nexus_defaults as defaults; +use nexus_types::identity::Resource; use omicron_common::api::external::CreateResult; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::DeleteResult; @@ -55,13 +56,11 @@ impl super::Nexus { pub async fn project_create( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, new_project: ¶ms::ProjectCreate, ) -> CreateResult { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::CreateChild) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::CreateChild).await?; // Create a project. let db_project = @@ -70,6 +69,8 @@ impl super::Nexus { .db_datastore .project_create(opctx, &authz_org, db_project) .await?; + let project_lookup = LookupPath::new(opctx, &self.db_datastore) + .project_id(db_project.id()); // TODO: We probably want to have "project creation" and "default VPC // creation" co-located within a saga for atomicity. @@ -77,14 +78,10 @@ impl super::Nexus { // Until then, we just perform the operations sequentially. // Create a default VPC associated with the project. - // TODO-correctness We need to be using the project_id we just created. - // project_create() should return authz::Project and we should use that - // here. let _ = self .project_create_vpc( opctx, - &organization_name, - &new_project.identity.name.clone().into(), + &project_lookup, ¶ms::VpcCreate { identity: IdentityMetadataCreateParams { name: "default".parse().unwrap(), @@ -102,42 +99,14 @@ impl super::Nexus { Ok(db_project) } - pub async fn project_fetch( - &self, - opctx: &OpContext, - organization_name: &Name, - project_name: &Name, - ) -> LookupResult { - let (.., db_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .fetch() - .await?; - Ok(db_project) - } - - pub async fn project_fetch_by_id( - &self, - opctx: &OpContext, - project_id: &Uuid, - ) -> LookupResult { - let (.., db_project) = LookupPath::new(opctx, &self.db_datastore) - .project_id(*project_id) - .fetch() - .await?; - Ok(db_project) - } - pub async fn projects_list_by_name( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::ListChildren) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore .projects_list_by_name(opctx, &authz_org, pagparams) .await @@ -146,13 +115,11 @@ impl super::Nexus { pub async fn projects_list_by_id( &self, opctx: &OpContext, - organization_name: &Name, + organization_lookup: &lookup::Organization<'_>, pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { - let (.., authz_org) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .lookup_for(authz::Action::ListChildren) - .await?; + let (.., authz_org) = + organization_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore .projects_list_by_id(opctx, &authz_org, pagparams) .await @@ -161,15 +128,11 @@ impl super::Nexus { pub async fn project_update( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, new_params: ¶ms::ProjectUpdate, ) -> UpdateResult { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::Modify) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::Modify).await?; self.db_datastore .project_update(opctx, &authz_project, new_params.clone().into()) .await @@ -178,14 +141,10 @@ impl super::Nexus { pub async fn project_delete( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, ) -> DeleteResult { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::Delete) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::Delete).await?; self.db_datastore.project_delete(opctx, &authz_project).await } @@ -194,14 +153,10 @@ impl super::Nexus { pub async fn project_fetch_policy( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, ) -> LookupResult> { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::ReadPolicy) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ReadPolicy).await?; let role_assignments = self .db_datastore .role_assignment_fetch_visible(opctx, &authz_project) @@ -216,15 +171,11 @@ impl super::Nexus { pub async fn project_update_policy( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, policy: &shared::Policy, ) -> UpdateResult> { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::ModifyPolicy) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ModifyPolicy).await?; let role_assignments = self .db_datastore diff --git a/nexus/src/app/vpc.rs b/nexus/src/app/vpc.rs index 121e73d05e3..c4544d323cb 100644 --- a/nexus/src/app/vpc.rs +++ b/nexus/src/app/vpc.rs @@ -9,6 +9,7 @@ use crate::context::OpContext; use crate::db; use crate::db::identity::Asset; use crate::db::identity::Resource; +use crate::db::lookup; use crate::db::lookup::LookupPath; use crate::db::model::Name; use crate::db::model::VpcRouterKind; @@ -45,15 +46,11 @@ impl super::Nexus { pub async fn project_create_vpc( &self, opctx: &OpContext, - organization_name: &Name, - project_name: &Name, + project_lookup: &lookup::Project<'_>, params: ¶ms::VpcCreate, ) -> CreateResult { - let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore) - .organization_name(organization_name) - .project_name(project_name) - .lookup_for(authz::Action::CreateChild) - .await?; + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::CreateChild).await?; let vpc_id = Uuid::new_v4(); let system_router_id = Uuid::new_v4(); let default_route_id = Uuid::new_v4(); diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 6d342177715..db945cfae84 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -115,6 +115,14 @@ pub fn external_api() -> NexusApiDescription { api.register(project_policy_view)?; api.register(project_policy_update)?; + api.register(project_list_v1)?; + api.register(project_create_v1)?; + api.register(project_view_v1)?; + api.register(project_delete_v1)?; + api.register(project_update_v1)?; + api.register(project_policy_view_v1)?; + api.register(project_policy_update_v1)?; + // Customer-Accessible IP Pools API api.register(ip_pool_list)?; api.register(ip_pool_create)?; @@ -1112,7 +1120,6 @@ async fn organization_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let (.., organization) = nexus @@ -1262,7 +1269,6 @@ async fn organization_update( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = ¶ms::OrganizationSelector( @@ -1417,11 +1423,79 @@ async fn organization_policy_update( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +pub struct ProjectListQueryParams { + pub organization: NameOrId, + #[serde(flatten)] + pagination: PaginatedByNameOrId, +} + +/// List projects +#[endpoint { + method = GET, + path = "/v1/projects", + tags = ["projects"], +}] +async fn project_list_v1( + rqctx: Arc>>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + ¶ms::OrganizationSelector(query.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let params = ScanByNameOrId::from_query(&query.pagination)?; + let field = pagination_field_for_scan_params(params); + let projects = match field { + PagField::Id => { + let page_selector = + data_page_params_nameid_id(&rqctx, &query.pagination)?; + nexus + .projects_list_by_id( + &opctx, + &organization_lookup, + &page_selector, + ) + .await? + } + + PagField::Name => { + let page_selector = + data_page_params_nameid_name(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)); + nexus + .projects_list_by_name( + &opctx, + &organization_lookup, + &page_selector, + ) + .await? + } + } + .into_iter() + .map(|p| p.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query.pagination, + projects, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// List projects +/// Use `GET /v1/projects` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects", tags = ["projects"], + deprecated = true, }] async fn project_list( rqctx: Arc>>, @@ -1432,10 +1506,14 @@ async fn project_list( let nexus = &apictx.nexus; let query = query_params.into_inner(); let path = path_params.into_inner(); - let organization_name = &path.organization_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; let params = ScanByNameOrId::from_query(&query)?; let field = pagination_field_for_scan_params(params); let projects = match field { @@ -1444,7 +1522,7 @@ async fn project_list( nexus .projects_list_by_id( &opctx, - &organization_name, + &organization_lookup, &page_selector, ) .await? @@ -1457,7 +1535,7 @@ async fn project_list( nexus .projects_list_by_name( &opctx, - &organization_name, + &organization_lookup, &page_selector, ) .await? @@ -1475,11 +1553,54 @@ async fn project_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +pub struct ProjectLookupPathParams { + pub project: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ProjectCreateParams { + pub organization: NameOrId, +} + +#[endpoint { + method = POST, + path = "/v1/projects", + tags = ["projects"], +}] +async fn project_create_v1( + rqctx: Arc>>, + query_params: Query, + new_project: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = + params::OrganizationSelector(query.organization); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; + let project = nexus + .project_create( + &opctx, + &organization_lookup, + &new_project.into_inner(), + ) + .await?; + Ok(HttpResponseCreated(project.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a project +/// Use `POST /v1/projects` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects", tags = ["projects"], + deprecated = true }] async fn project_create( rqctx: Arc>>, @@ -1488,14 +1609,18 @@ async fn project_create( ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; - let params = path_params.into_inner(); - let organization_name = ¶ms.organization_name; + let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let organization_selector = ¶ms::OrganizationSelector( + NameOrId::Name(path.organization_name.into()), + ); + let organization_lookup = + nexus.organization_lookup(&opctx, &organization_selector)?; let project = nexus .project_create( &opctx, - &organization_name, + &organization_lookup, &new_project.into_inner(), ) .await?; @@ -1504,6 +1629,36 @@ async fn project_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[derive(Deserialize, JsonSchema)] +pub struct ProjectQueryParams { + pub organization: Option, +} + +#[endpoint { + method = GET, + path = "/v1/projects/{project}", + tags = ["projects"], +}] +async fn project_view_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = + params::ProjectSelector::new(query.organization, path.project); + let (.., project) = + nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; + Ok(HttpResponseOk(project.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Project requests #[derive(Deserialize, JsonSchema)] struct ProjectPathParam { @@ -1514,10 +1669,12 @@ struct ProjectPathParam { } /// Fetch a project +/// Use `GET /v1/projects/{project}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}", tags = ["projects"], + deprecated = true }] async fn project_view( rqctx: Arc>>, @@ -1526,23 +1683,26 @@ async fn project_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project = nexus - .project_fetch(&opctx, &organization_name, &project_name) - .await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let (.., project) = + nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; Ok(HttpResponseOk(project.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Fetch a project by id +/// Use `GET /v1/projects/{project}` instead #[endpoint { method = GET, path = "/by-id/projects/{id}", tags = ["projects"], + deprecated = true }] async fn project_view_by_id( rqctx: Arc>>, @@ -1551,20 +1711,50 @@ async fn project_view_by_id( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project = nexus.project_fetch_by_id(&opctx, id).await?; + let project_selector = + params::ProjectSelector::new(None, NameOrId::Id(path.id)); + let (.., project) = + nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; Ok(HttpResponseOk(project.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Delete a project +#[endpoint { + method = DELETE, + path = "/v1/projects/{project}", + tags = ["projects"], +}] +async fn project_delete_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = + params::ProjectSelector::new(query.organization, path.project); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + nexus.project_delete(&opctx, &project_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Delete a project +/// Use `DELETE /v1/projects/{project}` instead #[endpoint { method = DELETE, path = "/organizations/{organization_name}/projects/{project_name}", tags = ["projects"], + deprecated = true }] async fn project_delete( rqctx: Arc>>, @@ -1572,27 +1762,62 @@ async fn project_delete( ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; - let params = path_params.into_inner(); - let organization_name = ¶ms.organization_name; - let project_name = ¶ms.project_name; + let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus.project_delete(&opctx, &organization_name, &project_name).await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + nexus.project_delete(&opctx, &project_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Update a project +#[endpoint { + method = PUT, + path = "/v1/projects/{project}", + tags = ["projects"], +}] +async fn project_update_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, + updated_project: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let updated_project = updated_project.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = + params::ProjectSelector::new(query.organization, path.project); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let project = nexus + .project_update(&opctx, &project_lookup, &updated_project) + .await?; + Ok(HttpResponseOk(project.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Update a project // TODO-correctness: Is it valid for PUT to accept application/json that's a // subset of what the resource actually represents? If not, is that a problem? // (HTTP may require that this be idempotent.) If so, can we get around that // having this be a slightly different content-type (e.g., // "application/json-patch")? We should see what other APIs do. +/// Use `PUT /v1/projects/{project}` instead #[endpoint { method = PUT, path = "/organizations/{organization_name}/projects/{project_name}", tags = ["projects"], + deprecated = true }] async fn project_update( rqctx: Arc>>, @@ -1602,28 +1827,59 @@ async fn project_update( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let newproject = nexus + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let new_project = nexus .project_update( &opctx, - &organization_name, - &project_name, + &project_lookup, &updated_project.into_inner(), ) .await?; - Ok(HttpResponseOk(newproject.into())) + Ok(HttpResponseOk(new_project.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Fetch a project's IAM policy +#[endpoint { + method = GET, + path = "/v1/projects/{project}/policy", + tags = ["projects"], +}] +async fn project_policy_view_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = + params::ProjectSelector::new(query.organization, path.project); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let policy = + nexus.project_fetch_policy(&opctx, &project_lookup).await?; + Ok(HttpResponseOk(policy.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Fetch a project's IAM policy +/// Use `GET /v1/projects/{project}/policy` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/policy", tags = ["projects"], + deprecated = true }] async fn project_policy_view( rqctx: Arc>>, @@ -1632,15 +1888,43 @@ async fn project_policy_view( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + let policy = + nexus.project_fetch_policy(&opctx, &project_lookup).await?; + Ok(HttpResponseOk(policy)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} +/// Update a project's IAM policy +#[endpoint { + method = PUT, + path = "/v1/projects/{project}/policy", + tags = ["projects"], +}] +async fn project_policy_update_v1( + rqctx: Arc>>, + path_params: Path, + new_policy: TypedBody>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let new_policy = new_policy.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let policy = nexus - .project_fetch_policy(&opctx, organization_name, project_name) + let project_selector = params::ProjectSelector::new(None, path.project); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; + nexus + .project_update_policy(&opctx, &project_lookup, &new_policy) .await?; - Ok(HttpResponseOk(policy)) + Ok(HttpResponseOk(new_policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -1660,21 +1944,18 @@ async fn project_policy_update( let nexus = &apictx.nexus; let path = path_params.into_inner(); let new_policy = new_policy.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let handler = async { let nasgns = new_policy.role_assignments.len(); // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = nexus - .project_update_policy( - &opctx, - organization_name, - project_name, - &new_policy, - ) + .project_update_policy(&opctx, &project_lookup, &new_policy) .await?; Ok(HttpResponseOk(policy)) }; @@ -2305,11 +2586,11 @@ async fn instance_list_v1( let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = ¶ms::ProjectSelector::new(query.organization, query.project); - let authz_project = nexus.project_lookup(&opctx, project_selector)?; + let project_lookup = nexus.project_lookup(&opctx, project_selector)?; let instances = nexus .project_list_instances( &opctx, - &authz_project, + &project_lookup, &data_page_params_for(&rqctx, &query.pagination)? .map_name(|n| Name::ref_cast(n)), ) @@ -3946,18 +4227,16 @@ async fn vpc_create( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; let new_vpc_params = &new_vpc.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let project_selector = params::ProjectSelector::new( + Some(NameOrId::Name(path.organization_name.into())), + NameOrId::Name(path.project_name.into()), + ); + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let vpc = nexus - .project_create_vpc( - &opctx, - &organization_name, - &project_name, - &new_vpc_params, - ) + .project_create_vpc(&opctx, &project_lookup, &new_vpc_params) .await?; Ok(HttpResponseCreated(vpc.into())) }; diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 3828fb5bfb7..5d6714c6b12 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -72,13 +72,20 @@ timeseries_schema_get /timeseries/schema API operations found with tag "organizations" OPERATION ID URL PATH organization_create /organizations +organization_create_v1 /v1/organizations organization_delete /organizations/{organization_name} +organization_delete_v1 /v1/organizations/{organization} organization_list /organizations +organization_list_v1 /v1/organizations organization_policy_update /organizations/{organization_name}/policy +organization_policy_update_v1 /v1/organizations/{organization}/policy organization_policy_view /organizations/{organization_name}/policy +organization_policy_view_v1 /v1/organizations/{organization}/policy organization_update /organizations/{organization_name} +organization_update_v1 /v1/organizations/{organization} organization_view /organizations/{organization_name} organization_view_by_id /by-id/organizations/{id} +organization_view_v1 /v1/organizations/{organization} API operations found with tag "policy" OPERATION ID URL PATH @@ -88,13 +95,20 @@ system_policy_view /system/policy API operations found with tag "projects" OPERATION ID URL PATH project_create /organizations/{organization_name}/projects +project_create_v1 /v1/projects project_delete /organizations/{organization_name}/projects/{project_name} +project_delete_v1 /v1/projects/{project} project_list /organizations/{organization_name}/projects +project_list_v1 /v1/projects project_policy_update /organizations/{organization_name}/projects/{project_name}/policy +project_policy_update_v1 /v1/projects/{project}/policy project_policy_view /organizations/{organization_name}/projects/{project_name}/policy +project_policy_view_v1 /v1/projects/{project}/policy project_update /organizations/{organization_name}/projects/{project_name} +project_update_v1 /v1/projects/{project} project_view /organizations/{organization_name}/projects/{project_name} project_view_by_id /by-id/projects/{id} +project_view_v1 /v1/projects/{project} API operations found with tag "roles" OPERATION ID URL PATH diff --git a/openapi/nexus.json b/openapi/nexus.json index e6a4b225d1d..0981635a1ae 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -172,6 +172,7 @@ "organizations" ], "summary": "Fetch an organization by id", + "description": "Use `GET /v1/organizations/{organization}` instead", "operationId": "organization_view_by_id", "parameters": [ { @@ -202,7 +203,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/by-id/projects/{id}": { @@ -211,6 +213,7 @@ "projects" ], "summary": "Fetch a project by id", + "description": "Use `GET /v1/projects/{project}` instead", "operationId": "project_view_by_id", "parameters": [ { @@ -241,7 +244,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/by-id/snapshots/{id}": { @@ -805,6 +809,7 @@ "organizations" ], "summary": "List organizations", + "description": "Use `/v1/organizations` instead", "operationId": "organization_list", "parameters": [ { @@ -856,6 +861,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -863,6 +869,7 @@ "organizations" ], "summary": "Create an organization", + "description": "Use `POST /v1/organizations` instead", "operationId": "organization_create", "requestBody": { "content": { @@ -891,7 +898,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}": { @@ -900,6 +908,7 @@ "organizations" ], "summary": "Fetch an organization", + "description": "Use `GET /v1/organizations/{organization}` instead", "operationId": "organization_view", "parameters": [ { @@ -930,13 +939,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "organizations" ], "summary": "Update an organization", + "description": "Use `PUT /v1/organizations/{organization}` instead", "operationId": "organization_update", "parameters": [ { @@ -977,13 +988,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "organizations" ], "summary": "Delete an organization", + "description": "Use `DELETE /v1/organizations/{organization}` instead", "operationId": "organization_delete", "parameters": [ { @@ -1007,7 +1020,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/policy": { @@ -1016,6 +1030,7 @@ "organizations" ], "summary": "Fetch an organization's IAM policy", + "description": "Use `GET /v1/organizations/{organization}/policy` instead", "operationId": "organization_policy_view", "parameters": [ { @@ -1046,13 +1061,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "organizations" ], "summary": "Update an organization's IAM policy", + "description": "Use `PUT /v1/organizations/{organization}/policy` instead", "operationId": "organization_policy_update", "parameters": [ { @@ -1093,7 +1110,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects": { @@ -1102,6 +1120,7 @@ "projects" ], "summary": "List projects", + "description": "Use `GET /v1/projects` instead", "operationId": "project_list", "parameters": [ { @@ -1163,6 +1182,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -1170,6 +1190,7 @@ "projects" ], "summary": "Create a project", + "description": "Use `POST /v1/projects` instead", "operationId": "project_create", "parameters": [ { @@ -1210,7 +1231,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}": { @@ -1219,6 +1241,7 @@ "projects" ], "summary": "Fetch a project", + "description": "Use `GET /v1/projects/{project}` instead", "operationId": "project_view", "parameters": [ { @@ -1259,13 +1282,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "projects" ], "summary": "Update a project", + "description": "Use `PUT /v1/projects/{project}` instead", "operationId": "project_update", "parameters": [ { @@ -1316,13 +1341,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "projects" ], "summary": "Delete a project", + "description": "Use `DELETE /v1/projects/{project}` instead", "operationId": "project_delete", "parameters": [ { @@ -1356,7 +1383,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/organizations/{organization_name}/projects/{project_name}/disks": { @@ -3193,6 +3221,7 @@ "projects" ], "summary": "Fetch a project's IAM policy", + "description": "Use `GET /v1/projects/{project}/policy` instead", "operationId": "project_policy_view", "parameters": [ { @@ -3233,7 +3262,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ @@ -8245,6 +8275,635 @@ } } } + }, + "/v1/organizations": { + "get": { + "tags": [ + "organizations" + ], + "summary": "List organizations", + "operationId": "organization_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + }, + "style": "form" + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "organizations" + ], + "summary": "Create an organization", + "operationId": "organization_create_v1", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/organizations/{organization}": { + "get": { + "tags": [ + "organizations" + ], + "operationId": "organization_view_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "organizations" + ], + "operationId": "organization_update_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "organizations" + ], + "operationId": "organization_delete_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/organizations/{organization}/policy": { + "get": { + "tags": [ + "organizations" + ], + "operationId": "organization_policy_view_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "organizations" + ], + "operationId": "organization_policy_update_v1", + "parameters": [ + { + "in": "path", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects": { + "get": { + "tags": [ + "projects" + ], + "summary": "List projects", + "operationId": "project_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + }, + "style": "form" + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "projects" + ], + "operationId": "project_create_v1", + "parameters": [ + { + "in": "query", + "name": "organization", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects/{project}": { + "get": { + "tags": [ + "projects" + ], + "operationId": "project_view_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "projects" + ], + "summary": "Update a project", + "operationId": "project_update_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "projects" + ], + "summary": "Delete a project", + "operationId": "project_delete_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/projects/{project}/policy": { + "get": { + "tags": [ + "projects" + ], + "summary": "Fetch a project's IAM policy", + "operationId": "project_policy_view_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "projects" + ], + "summary": "Update a project's IAM policy", + "operationId": "project_policy_update_v1", + "parameters": [ + { + "in": "path", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { From c29c67d556243fb29387a7275bd112b5eda09532 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 13 Dec 2022 23:19:33 -0500 Subject: [PATCH 48/72] Fix clippy failures --- nexus/src/external_api/http_entrypoints.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index db945cfae84..e14a557e921 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1156,7 +1156,7 @@ async fn organization_view_by_id( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(NameOrId::Id(path.id.into())), + ¶ms::OrganizationSelector(NameOrId::Id(path.id)), )? .fetch() .await?; @@ -1310,7 +1310,7 @@ async fn organization_policy_view_v1( let policy = nexus .organization_fetch_policy(&opctx, &organization_lookup) .await?; - Ok(HttpResponseOk(policy.into())) + Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -1868,7 +1868,7 @@ async fn project_policy_view_v1( let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = nexus.project_fetch_policy(&opctx, &project_lookup).await?; - Ok(HttpResponseOk(policy.into())) + Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } From 599e6eb9989cd1e6e43f511162cf5e7726e2a499 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 13 Dec 2022 23:34:02 -0500 Subject: [PATCH 49/72] Require less conversions when going from NameOrId to name --- common/src/api/external/mod.rs | 12 ++ nexus/db-model/src/name.rs | 6 +- nexus/src/external_api/http_entrypoints.rs | 121 ++++++++++----------- 3 files changed, 73 insertions(+), 66 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 7ae77794494..6d988c61397 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -288,6 +288,18 @@ impl TryFrom for NameOrId { } } +impl From for NameOrId { + fn from(name: Name) -> Self { + NameOrId::Name(name) + } +} + +impl From for NameOrId { + fn from(id: Uuid) -> Self { + NameOrId::Id(id) + } +} + impl JsonSchema for NameOrId { fn schema_name() -> String { "NameOrId".to_string() diff --git a/nexus/db-model/src/name.rs b/nexus/db-model/src/name.rs index 11d3d6574c6..74ff715c8dd 100644 --- a/nexus/db-model/src/name.rs +++ b/nexus/db-model/src/name.rs @@ -37,7 +37,11 @@ use serde::{Deserialize, Serialize}; #[display("{0}")] pub struct Name(pub external::Name); -// impl Into for Name {} +impl From for external::NameOrId { + fn from(name: Name) -> Self { + Self::Name(name.0) + } +} NewtypeFrom! { () pub struct Name(external::Name); } NewtypeDeref! { () pub struct Name(external::Name); } diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index e14a557e921..9191651e14f 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1125,9 +1125,7 @@ async fn organization_view( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(NameOrId::Name( - path.organization_name.into(), - )), + ¶ms::OrganizationSelector(path.organization_name.into()), )? .fetch() .await?; @@ -1156,7 +1154,7 @@ async fn organization_view_by_id( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(NameOrId::Id(path.id)), + ¶ms::OrganizationSelector(path.id.into()), )? .fetch() .await?; @@ -1206,9 +1204,8 @@ async fn organization_delete( let params = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(params.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(params.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; nexus.organization_delete(&opctx, &organization_lookup).await?; @@ -1271,9 +1268,8 @@ async fn organization_update( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let new_organization = nexus @@ -1334,9 +1330,8 @@ async fn organization_policy_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1406,9 +1401,8 @@ async fn organization_policy_update( // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1509,9 +1503,8 @@ async fn project_list( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let params = ScanByNameOrId::from_query(&query)?; @@ -1612,9 +1605,8 @@ async fn project_create( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = ¶ms::OrganizationSelector( - NameOrId::Name(path.organization_name.into()), - ); + let organization_selector = + ¶ms::OrganizationSelector(path.organization_name.into()); let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let project = nexus @@ -1686,8 +1678,8 @@ async fn project_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let (.., project) = nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; @@ -1714,7 +1706,7 @@ async fn project_view_by_id( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = - params::ProjectSelector::new(None, NameOrId::Id(path.id)); + params::ProjectSelector::new(None, path.id.into()); let (.., project) = nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; Ok(HttpResponseOk(project.into())) @@ -1766,8 +1758,8 @@ async fn project_delete( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; nexus.project_delete(&opctx, &project_lookup).await?; @@ -1830,8 +1822,8 @@ async fn project_update( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let new_project = nexus @@ -1891,8 +1883,8 @@ async fn project_policy_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = @@ -1950,8 +1942,8 @@ async fn project_policy_update( bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = nexus @@ -2623,8 +2615,8 @@ async fn instance_list( let query = query_params.into_inner(); let path = path_params.into_inner(); let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2709,8 +2701,8 @@ async fn instance_create( let path = path_params.into_inner(); let new_instance_params = &new_instance.into_inner(); let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2797,9 +2789,9 @@ async fn instance_view( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2824,13 +2816,12 @@ async fn instance_view_by_id( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let (.., instance) = nexus .instance_lookup( &opctx, - ¶ms::InstanceSelector::new(None, None, NameOrId::Id(*id)), + ¶ms::InstanceSelector::new(None, None, path.id.into()), )? .fetch() .await?; @@ -2882,9 +2873,9 @@ async fn instance_delete( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2951,9 +2942,9 @@ async fn instance_migrate( let path = path_params.into_inner(); let migrate_instance_params = migrate_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3014,9 +3005,9 @@ async fn instance_reboot( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3072,9 +3063,9 @@ async fn instance_start( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3129,9 +3120,9 @@ async fn instance_stop( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3201,9 +3192,9 @@ async fn instance_serial_console( let nexus = &apictx.nexus; let path = path_params.into_inner(); let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -3262,9 +3253,9 @@ async fn instance_serial_console_stream( let path = path_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; let instance_selector = params::InstanceSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - Some(NameOrId::Name(path.project_name.into())), - NameOrId::Name(path.instance_name.into()), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), ); let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; nexus.instance_serial_console_stream(conn, &instance_lookup).await?; @@ -4231,8 +4222,8 @@ async fn vpc_create( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_selector = params::ProjectSelector::new( - Some(NameOrId::Name(path.organization_name.into())), - NameOrId::Name(path.project_name.into()), + Some(path.organization_name.into()), + path.project_name.into(), ); let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let vpc = nexus From 114d39a18eef6678b02612df4a343a45e160a01d Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 15:22:53 -0500 Subject: [PATCH 50/72] Revamp selectors (again x2), convert projects --- nexus/src/app/instance.rs | 32 +-- nexus/src/app/organization.rs | 6 +- nexus/src/app/project.rs | 27 +- nexus/src/external_api/http_entrypoints.rs | 299 ++++++++------------- nexus/test-utils/src/resource_helpers.rs | 2 +- nexus/tests/integration_tests/instances.rs | 2 +- nexus/types/src/external_api/params.rs | 112 ++++++-- 7 files changed, 248 insertions(+), 232 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index bab5e77cbd2..5f30d3dc54e 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -62,25 +62,27 @@ impl super::Nexus { instance_selector: &'a params::InstanceSelector, ) -> LookupResult> { match instance_selector { - params::InstanceSelector(NameOrId::Id(id), ..) => { + params::InstanceSelector { + project_selector: None, + instance: NameOrId::Id(id), + } => { let instance = LookupPath::new(opctx, &self.db_datastore).instance_id(*id); Ok(instance) } - params::InstanceSelector( - NameOrId::Name(name), - project_selector, - ) => { - if let Some(project) = project_selector { - let instance = self - .project_lookup(opctx, project)? - .instance_name(Name::ref_cast(name)); - Ok(instance) - } else { - Err(Error::InvalidRequest { - message: "Unable to resolve instance by name without instance".to_string(), - }) - } + params::InstanceSelector { + project_selector: Some(project_selector), + instance: NameOrId::Name(name), + } => { + let instance = self + .project_lookup(opctx, project_selector)? + .instance_name(Name::ref_cast(name)); + Ok(instance) + } + _ => { + return Err(Error::invalid_request( + "instance should either be UUID or project should be specified", + )); } } } diff --git a/nexus/src/app/organization.rs b/nexus/src/app/organization.rs index a1fae4997db..5c0d7cfdb06 100644 --- a/nexus/src/app/organization.rs +++ b/nexus/src/app/organization.rs @@ -31,12 +31,14 @@ impl super::Nexus { organization_selector: &'a params::OrganizationSelector, ) -> LookupResult> { match organization_selector { - params::OrganizationSelector(NameOrId::Id(id)) => { + params::OrganizationSelector { organization: NameOrId::Id(id) } => { let organization = LookupPath::new(opctx, &self.db_datastore) .organization_id(*id); Ok(organization) } - params::OrganizationSelector(NameOrId::Name(name)) => { + params::OrganizationSelector { + organization: NameOrId::Name(name), + } => { let organization = LookupPath::new(opctx, &self.db_datastore) .organization_name(Name::ref_cast(name)); Ok(organization) diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index 5051bfb62c6..0a20c5cc1cb 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -34,23 +34,26 @@ impl super::Nexus { project_selector: &'a params::ProjectSelector, ) -> LookupResult> { match project_selector { - params::ProjectSelector(NameOrId::Id(id), ..) => { + params::ProjectSelector { + project: NameOrId::Id(id), + organization_selector: None, + } => { let project = LookupPath::new(opctx, &self.db_datastore).project_id(*id); Ok(project) } - params::ProjectSelector(NameOrId::Name(name), org_selector) => { - if let Some(org) = org_selector { - let project = self - .organization_lookup(opctx, org)? - .project_name(Name::ref_cast(name)); - Ok(project) - } else { - Err(Error::InvalidRequest { - message: "Unable to resolve project by name without organization".to_string(), - }) - } + params::ProjectSelector { + project: NameOrId::Name(name), + organization_selector: Some(organization_selector), + } => { + let project = self + .organization_lookup(opctx, organization_selector)? + .project_name(Name::ref_cast(name)); + Ok(project) } + _ => Err(Error::invalid_request( + "project should either be UUID or organization should be specified" + )), } } pub async fn project_create( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 9191651e14f..aaaeed90945 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -60,7 +60,6 @@ use omicron_common::api::external::Disk; use omicron_common::api::external::Error; use omicron_common::api::external::Instance; use omicron_common::api::external::InternalContext; -use omicron_common::api::external::NameOrId; use omicron_common::api::external::NetworkInterface; use omicron_common::api::external::RouterRoute; use omicron_common::api::external::RouterRouteCreateParams; @@ -1067,11 +1066,6 @@ async fn organization_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -struct OrganizationLookupPathParam { - organization: NameOrId, -} - #[endpoint { method = GET, path = "/v1/organizations/{organization}", @@ -1079,7 +1073,7 @@ struct OrganizationLookupPathParam { }] async fn organization_view_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1089,7 +1083,9 @@ async fn organization_view_v1( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(path.organization), + ¶ms::OrganizationSelector { + organization: path.organization, + }, )? .fetch() .await?; @@ -1125,7 +1121,9 @@ async fn organization_view( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(path.organization_name.into()), + ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }, )? .fetch() .await?; @@ -1154,7 +1152,7 @@ async fn organization_view_by_id( let (.., organization) = nexus .organization_lookup( &opctx, - ¶ms::OrganizationSelector(path.id.into()), + ¶ms::OrganizationSelector { organization: path.id.into() }, )? .fetch() .await?; @@ -1170,7 +1168,7 @@ async fn organization_view_by_id( }] async fn organization_delete_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1178,7 +1176,7 @@ async fn organization_delete_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - ¶ms::OrganizationSelector(params.organization); + ¶ms::OrganizationSelector { organization: params.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; nexus.organization_delete(&opctx, &organization_lookup).await?; @@ -1204,8 +1202,9 @@ async fn organization_delete( let params = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(params.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: params.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; nexus.organization_delete(&opctx, &organization_lookup).await?; @@ -1221,7 +1220,7 @@ async fn organization_delete( }] async fn organization_update_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, updated_organization: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -1230,7 +1229,7 @@ async fn organization_update_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - ¶ms::OrganizationSelector(params.organization); + ¶ms::OrganizationSelector { organization: params.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let new_organization = nexus @@ -1268,8 +1267,9 @@ async fn organization_update( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let new_organization = nexus @@ -1291,7 +1291,7 @@ async fn organization_update( }] async fn organization_policy_view_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, ) -> Result>, HttpError> { let apictx = rqctx.context(); @@ -1300,7 +1300,7 @@ async fn organization_policy_view_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - ¶ms::OrganizationSelector(params.organization); + ¶ms::OrganizationSelector { organization: params.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1330,8 +1330,9 @@ async fn organization_policy_view( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1349,7 +1350,7 @@ async fn organization_policy_view( }] async fn organization_policy_update_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, new_policy: TypedBody>, ) -> Result>, HttpError> { @@ -1360,7 +1361,7 @@ async fn organization_policy_update_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - ¶ms::OrganizationSelector(params.organization); + ¶ms::OrganizationSelector { organization: params.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let nasgns = new_policy.role_assignments.len(); @@ -1401,8 +1402,9 @@ async fn organization_policy_update( // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let policy = nexus @@ -1417,13 +1419,6 @@ async fn organization_policy_update( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -pub struct ProjectListQueryParams { - pub organization: NameOrId, - #[serde(flatten)] - pagination: PaginatedByNameOrId, -} - /// List projects #[endpoint { method = GET, @@ -1432,17 +1427,15 @@ pub struct ProjectListQueryParams { }] async fn project_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(query.organization); let organization_lookup = - nexus.organization_lookup(&opctx, &organization_selector)?; + nexus.organization_lookup(&opctx, &query.organization)?; let params = ScanByNameOrId::from_query(&query.pagination)?; let field = pagination_field_for_scan_params(params); let projects = match field { @@ -1503,8 +1496,9 @@ async fn project_list( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let params = ScanByNameOrId::from_query(&query)?; @@ -1546,16 +1540,6 @@ async fn project_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -pub struct ProjectLookupPathParams { - pub project: NameOrId, -} - -#[derive(Deserialize, JsonSchema)] -pub struct ProjectCreateParams { - pub organization: NameOrId, -} - #[endpoint { method = POST, path = "/v1/projects", @@ -1563,7 +1547,7 @@ pub struct ProjectCreateParams { }] async fn project_create_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, new_project: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -1572,7 +1556,7 @@ async fn project_create_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let organization_selector = - params::OrganizationSelector(query.organization); + params::OrganizationSelector { organization: query.organization }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let project = nexus @@ -1605,8 +1589,9 @@ async fn project_create( let path = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let organization_selector = - ¶ms::OrganizationSelector(path.organization_name.into()); + let organization_selector = ¶ms::OrganizationSelector { + organization: path.organization_name.into(), + }; let organization_lookup = nexus.organization_lookup(&opctx, &organization_selector)?; let project = nexus @@ -1621,11 +1606,6 @@ async fn project_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -pub struct ProjectQueryParams { - pub organization: Option, -} - #[endpoint { method = GET, path = "/v1/projects/{project}", @@ -1633,8 +1613,8 @@ pub struct ProjectQueryParams { }] async fn project_view_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1642,8 +1622,10 @@ async fn project_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - params::ProjectSelector::new(query.organization, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let (.., project) = nexus.project_lookup(&opctx, &project_selector)?.fetch().await?; Ok(HttpResponseOk(project.into())) @@ -1722,8 +1704,8 @@ async fn project_view_by_id( }] async fn project_delete_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1731,8 +1713,10 @@ async fn project_delete_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - params::ProjectSelector::new(query.organization, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; nexus.project_delete(&opctx, &project_lookup).await?; Ok(HttpResponseDeleted()) @@ -1776,8 +1760,8 @@ async fn project_delete( }] async fn project_update_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, updated_project: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -1787,8 +1771,10 @@ async fn project_update_v1( let updated_project = updated_project.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - params::ProjectSelector::new(query.organization, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let project = nexus .project_update(&opctx, &project_lookup, &updated_project) @@ -1846,8 +1832,8 @@ async fn project_update( }] async fn project_policy_view_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -1855,8 +1841,10 @@ async fn project_policy_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - params::ProjectSelector::new(query.organization, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let policy = nexus.project_fetch_policy(&opctx, &project_lookup).await?; @@ -1902,7 +1890,7 @@ async fn project_policy_view( }] async fn project_policy_update_v1( rqctx: Arc>>, - path_params: Path, + path_params: Path, new_policy: TypedBody>, ) -> Result>, HttpError> { let apictx = rqctx.context(); @@ -2554,14 +2542,6 @@ async fn disk_metrics_list( // Instances -#[derive(Deserialize, JsonSchema)] -struct InstanceListQueryParams { - #[serde(flatten)] - pagination: PaginatedByName, - project: NameOrId, - organization: Option, -} - #[endpoint { method = GET, path = "/v1/instances", @@ -2569,16 +2549,15 @@ struct InstanceListQueryParams { }] async fn instance_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - ¶ms::ProjectSelector::new(query.organization, query.project); - let project_lookup = nexus.project_lookup(&opctx, project_selector)?; + let project_lookup = + nexus.project_lookup(&opctx, &query.project_selector)?; let instances = nexus .project_list_instances( &opctx, @@ -2641,12 +2620,6 @@ async fn instance_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -struct InstanceCreateParams { - organization: Option, - project: NameOrId, -} - #[endpoint { method = POST, path = "/v1/instances", @@ -2654,18 +2627,16 @@ struct InstanceCreateParams { }] async fn instance_create_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, new_instance: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; - let query = query_params.into_inner(); + let project_selector = query_params.into_inner(); let new_instance_params = &new_instance.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = - ¶ms::ProjectSelector::new(query.organization, query.project); - let project_lookup = nexus.project_lookup(&opctx, project_selector)?; + let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; let instance = nexus .project_create_instance( &opctx, @@ -2719,24 +2690,6 @@ async fn instance_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Path parameters for Instance requests -#[derive(Deserialize, JsonSchema)] -struct InstanceLookupPathParam { - /// If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - /// - `project_id` - /// - `project_name`, `organization_id` - /// - `project_name`, `organization_name` - /// - /// If Id is used the above qualifiers are will be ignored - instance: NameOrId, -} - -#[derive(Deserialize, JsonSchema)] -struct InstanceQueryParams { - organization: Option, - project: Option, -} - #[endpoint { method = GET, path = "/v1/instances/{instance}", @@ -2744,8 +2697,8 @@ struct InstanceQueryParams { }] async fn instance_view_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -2753,12 +2706,10 @@ async fn instance_view_v1( let query = query_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - // let instance_selector = params::InstanceSelector::new(path.instance, &query.selector); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; let (.., instance) = instance_lookup.fetch().await?; @@ -2837,18 +2788,17 @@ async fn instance_view_by_id( }] async fn instance_delete_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2895,8 +2845,8 @@ async fn instance_delete( }] async fn instance_migrate_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, migrate_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -2904,11 +2854,10 @@ async fn instance_migrate_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let migrate_instance_params = migrate_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2969,18 +2918,17 @@ async fn instance_migrate( }] async fn instance_reboot_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3027,18 +2975,17 @@ async fn instance_reboot( }] async fn instance_start_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3084,18 +3031,17 @@ async fn instance_start( }] async fn instance_stop_v1( rqctx: Arc>>, - query_params: Query, - path_params: Path, + query_params: Query, + path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3134,15 +3080,6 @@ async fn instance_stop( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -pub struct InstanceSerialConsoleParams { - organization: Option, - project: Option, - - #[serde(flatten)] - pub console_params: params::InstanceSerialConsoleRequest, -} - #[endpoint { method = GET, path = "/v1/instances/{instance}/serial-console", @@ -3150,18 +3087,17 @@ pub struct InstanceSerialConsoleParams { }] async fn instance_serial_console_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3219,19 +3155,18 @@ async fn instance_serial_console( async fn instance_serial_console_stream_v1( rqctx: Arc>>, conn: WebsocketConnection, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, ) -> WebsocketChannelResult { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_selector = params::InstanceSelector::new( - query.organization, - query.project, - path.instance, - ); + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; nexus.instance_serial_console_stream(conn, &instance_lookup).await?; Ok(()) diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index da344d40657..ddc8e7374a8 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -77,7 +77,7 @@ pub async fn create_ip_pool( client: &ClientTestContext, pool_name: &str, ip_range: Option, - project_path: Option, + project_path: Option, ) -> (IpPool, IpPoolRange) { let ip_range = ip_range.unwrap_or_else(|| { use std::net::Ipv4Addr; diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 594c9573640..25389545710 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -2880,7 +2880,7 @@ async fn test_instance_ephemeral_ip_from_correct_project( // Create two IP pools. // // The first is restricted to the "restricted" project, the second unrestricted. - let project_path = params::ProjectPath { + let project_path = params::OldProjectPath { organization: Name::try_from(ORGANIZATION_NAME.to_string()).unwrap(), project: Name::try_from("restricted".to_string()).unwrap(), }; diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index bf47b856ed0..121fc291de6 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -7,6 +7,7 @@ use crate::external_api::shared; use chrono::{DateTime, Utc}; use omicron_common::api::external::{ + http_pagination::{PaginatedByName, PaginatedByNameOrId}, ByteCount, IdentityMetadataCreateParams, IdentityMetadataUpdateParams, InstanceCpuCount, Ipv4Net, Ipv6Net, Name, NameOrId, }; @@ -18,39 +19,112 @@ use serde::{ use std::{net::IpAddr, str::FromStr}; use uuid::Uuid; -pub struct OrganizationSelector(pub NameOrId); +#[derive(Deserialize, JsonSchema)] +pub struct OrganizationPath { + pub organization: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ProjectPath { + pub project: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +pub struct InstancePath { + pub instance: NameOrId, +} + +#[derive(Deserialize, JsonSchema)] +pub struct OrganizationSelector { + pub organization: NameOrId, +} + +impl From for OrganizationSelector { + fn from(name: Name) -> Self { + OrganizationSelector { organization: name.into() } + } +} -pub struct ProjectSelector(pub NameOrId, pub Option); +#[derive(Deserialize, JsonSchema)] +pub struct OptionalOrganizationSelector { + #[serde(flatten)] + pub organization_selector: Option, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ProjectSelector { + #[serde(flatten)] + pub organization_selector: Option, + pub project: NameOrId, +} +// TODO-v1: delete this post migration impl ProjectSelector { - pub fn new( - organization: Option, - project: NameOrId, - ) -> ProjectSelector { - ProjectSelector(project, organization.map(|o| OrganizationSelector(o))) + pub fn new(organization: Option, project: NameOrId) -> Self { + ProjectSelector { + organization_selector: organization + .map(|o| OrganizationSelector { organization: o }), + project, + } } } -pub struct InstanceSelector(pub NameOrId, pub Option); +#[derive(Deserialize, JsonSchema)] +pub struct ProjectList { + #[serde(flatten)] + pub pagination: PaginatedByNameOrId, + #[serde(flatten)] + pub organization: OrganizationSelector, +} + +#[derive(Deserialize, JsonSchema)] +pub struct OptionalProjectSelector { + #[serde(flatten)] + pub project_selector: Option, +} + +#[derive(Deserialize, JsonSchema)] +pub struct InstanceSelector { + #[serde(flatten)] + pub project_selector: Option, + pub instance: NameOrId, +} +// TODO-v1: delete this post migration impl InstanceSelector { pub fn new( organization: Option, project: Option, instance: NameOrId, - ) -> InstanceSelector { - InstanceSelector( + ) -> Self { + InstanceSelector { + project_selector: if let Some(p) = project { + Some(ProjectSelector::new(organization, p)) + } else { + None + }, instance, - project.map(|p| { - ProjectSelector( - p, - organization.map(|o| OrganizationSelector(o)), - ) - }), - ) + } } } +#[derive(Deserialize, JsonSchema)] +pub struct InstanceList { + #[serde(flatten)] + pub pagination: PaginatedByName, + #[serde(flatten)] + pub project_selector: ProjectSelector, +} + +#[derive(Deserialize, JsonSchema)] +pub struct InstanceSerialConsole { + #[serde(flatten)] + pub project_selector: Option, + + #[serde(flatten)] + pub console_params: InstanceSerialConsoleRequest, +} + // Silos /// Create-time parameters for a [`Silo`](crate::external_api::views::Silo) @@ -516,7 +590,7 @@ pub struct NetworkInterfaceUpdate { // Type used to identify a Project in request bodies, where one may not have // the path in the request URL. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -pub struct ProjectPath { +pub struct OldProjectPath { pub organization: Name, pub project: Name, } @@ -529,7 +603,7 @@ pub struct IpPoolCreate { #[serde(flatten)] pub identity: IdentityMetadataCreateParams, #[serde(flatten)] - pub project: Option, + pub project: Option, } /// Parameters for updating an IP Pool From 6ae7da34ec220af06e4f7fa9b4c58183cb373a44 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 15:31:46 -0500 Subject: [PATCH 51/72] Update nexus.json --- openapi/nexus.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openapi/nexus.json b/openapi/nexus.json index 0981635a1ae..df5124b7963 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7839,7 +7839,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -7891,7 +7890,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -7938,7 +7936,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8002,7 +7999,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8040,7 +8036,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8130,7 +8125,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8194,7 +8188,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" @@ -8248,7 +8241,6 @@ { "in": "path", "name": "instance", - "description": "If Name is used to reference the instance you must also include one of the following qualifiers as query parameters: - `project_id` - `project_name`, `organization_id` - `project_name`, `organization_name`\n\nIf Id is used the above qualifiers are will be ignored", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" From 63eddd75767e28cc53ac780f3ec7b3dc1615c557 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 16:21:00 -0500 Subject: [PATCH 52/72] Skip deprecated endpoints in auth check; update test to assert contents --- nexus/tests/integration_tests/endpoints.rs | 47 ++++++------------- nexus/tests/integration_tests/unauthorized.rs | 6 +-- .../unauthorized_coverage.rs | 27 ++++------- .../output/uncovered-authz-endpoints.txt | 30 ++++++++---- 4 files changed, 49 insertions(+), 61 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 6051eeaf7a0..d2dfe67a283 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -87,11 +87,10 @@ lazy_static! { // Organization used for testing pub static ref DEMO_ORG_NAME: Name = "demo-org".parse().unwrap(); pub static ref DEMO_ORG_URL: String = - format!("/organizations/{}", *DEMO_ORG_NAME); + format!("/v1/organizations/{}", *DEMO_ORG_NAME); pub static ref DEMO_ORG_POLICY_URL: String = - format!("{}/policy", *DEMO_ORG_URL); - pub static ref DEMO_ORG_PROJECTS_URL: String = - format!("{}/projects", *DEMO_ORG_URL); + format!("/v1/organizations/{}/policy", *DEMO_ORG_NAME); + pub static ref DEMO_ORG_PROJECTS_URL: String = format!("/v1/projects?organization={}", *DEMO_ORG_NAME); pub static ref DEMO_ORG_CREATE: params::OrganizationCreate = params::OrganizationCreate { identity: IdentityMetadataCreateParams { @@ -103,20 +102,20 @@ lazy_static! { // Project used for testing pub static ref DEMO_PROJECT_NAME: Name = "demo-project".parse().unwrap(); pub static ref DEMO_PROJECT_URL: String = - format!("{}/{}", *DEMO_ORG_PROJECTS_URL, *DEMO_PROJECT_NAME); + format!("/v1/projects/{}?organization={}", *DEMO_PROJECT_NAME, *DEMO_ORG_NAME); pub static ref DEMO_PROJECT_SELECTOR: String = format!("?organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_POLICY_URL: String = - format!("{}/policy", *DEMO_PROJECT_URL); + format!("/v1/projects/{}/policy?organization={}", *DEMO_PROJECT_NAME, *DEMO_ORG_NAME); pub static ref DEMO_PROJECT_URL_DISKS: String = - format!("{}/disks", *DEMO_PROJECT_URL); + format!("/organizations/{}/projects/{}/disks", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_IMAGES: String = - format!("{}/images", *DEMO_PROJECT_URL); + format!("/organizations/{}/projects/{}/images", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_INSTANCES: String = format!("/v1/instances?organization={}&project={}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_SNAPSHOTS: String = - format!("{}/snapshots", *DEMO_PROJECT_URL); + format!("/organizations/{}/projects/{}/snapshots", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_VPCS: String = - format!("{}/vpcs", *DEMO_PROJECT_URL); + format!("/organizations/{}/projects/{}/vpcs", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_CREATE: params::ProjectCreate = params::ProjectCreate { identity: IdentityMetadataCreateParams { @@ -128,13 +127,13 @@ lazy_static! { // VPC used for testing pub static ref DEMO_VPC_NAME: Name = "demo-vpc".parse().unwrap(); pub static ref DEMO_VPC_URL: String = - format!("{}/{}", *DEMO_PROJECT_URL_VPCS, *DEMO_VPC_NAME); + format!("/organizations/{}/projects/{}/vpcs/{}", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_FIREWALL_RULES: String = - format!("{}/firewall/rules", *DEMO_VPC_URL); + format!("/organizations/{}/projects/{}/vpcs/{}/firewall/rules", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_ROUTERS: String = - format!("{}/routers", *DEMO_VPC_URL); + format!("/organizations/{}/projects/{}/vpcs/{}/routers", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_URL_SUBNETS: String = - format!("{}/subnets", *DEMO_VPC_URL); + format!("/organizations/{}/projects/{}/vpcs/{}/subnets", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); pub static ref DEMO_VPC_CREATE: params::VpcCreate = params::VpcCreate { identity: IdentityMetadataCreateParams { @@ -819,7 +818,7 @@ lazy_static! { /* Organizations */ VerifyEndpoint { - url: "/organizations", + url: "/v1/organizations", visibility: Visibility::Public, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ @@ -830,15 +829,6 @@ lazy_static! { ], }, - VerifyEndpoint { - url: "/by-id/organizations/{id}", - visibility: Visibility::Protected, - unprivileged_access: UnprivilegedAccess::None, - allowed_methods: vec![ - AllowedMethod::Get, - ], - }, - VerifyEndpoint { url: &*DEMO_ORG_URL, visibility: Visibility::Protected, @@ -895,15 +885,6 @@ lazy_static! { ], }, - VerifyEndpoint { - url: "/by-id/projects/{id}", - visibility: Visibility::Protected, - unprivileged_access: UnprivilegedAccess::None, - allowed_methods: vec![ - AllowedMethod::Get, - ], - }, - VerifyEndpoint { url: &*DEMO_PROJECT_URL, visibility: Visibility::Protected, diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 7f6a6c300f1..a51326747d3 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -214,15 +214,15 @@ lazy_static! { }, // Create an Organization SetupReq::Post { - url: "/organizations", + url: "/v1/organizations", body: serde_json::to_value(&*DEMO_ORG_CREATE).unwrap(), - id_routes: vec!["/by-id/organizations/{id}"], + id_routes: vec![], }, // Create a Project in the Organization SetupReq::Post { url: &*DEMO_ORG_PROJECTS_URL, body: serde_json::to_value(&*DEMO_PROJECT_CREATE).unwrap(), - id_routes: vec!["/by-id/projects/{id}"], + id_routes: vec![], }, // Create a VPC in the Project SetupReq::Post { diff --git a/nexus/tests/integration_tests/unauthorized_coverage.rs b/nexus/tests/integration_tests/unauthorized_coverage.rs index 64ce7eabc82..856dafe394d 100644 --- a/nexus/tests/integration_tests/unauthorized_coverage.rs +++ b/nexus/tests/integration_tests/unauthorized_coverage.rs @@ -135,23 +135,16 @@ fn test_unauthorized_coverage() { // not `expectorage::assert_contents`? Because we only expect this file to // ever shrink, which is easy enough to fix by hand, and we don't want to // make it easy to accidentally add things to the allowlist.) - let expected_uncovered_endpoints = - std::fs::read_to_string("tests/output/uncovered-authz-endpoints.txt") - .expect("failed to load file of allowed uncovered endpoints"); - let mut unexpected_uncovered_endpoints = "These endpoints were expected to be covered by the unauthorized_coverage test but were not:\n".to_string(); - let mut has_uncovered_endpoints = false; - for endpoint in uncovered_endpoints.lines() { - if !expected_uncovered_endpoints.contains(endpoint) { - unexpected_uncovered_endpoints - .push_str(&format!("\t{}\n", endpoint)); - has_uncovered_endpoints = true; - } - } - assert_eq!( - has_uncovered_endpoints, false, - "{}\nMake sure you've added a test for this endpoint in unauthorized.rs.", - unexpected_uncovered_endpoints - ) + // let expected_uncovered_endpoints = + // std::fs::read_to_string("tests/output/uncovered-authz-endpoints.txt") + // .expect("failed to load file of allowed uncovered endpoints"); + + // TODO: Update this to remove overwrite capabilities + // See https://github.com/oxidecomputer/expectorate/pull/12 + assert_contents( + "tests/output/uncovered-authz-endpoints.txt", + uncovered_endpoints.as_str(), + ); } #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index 70576c0b21c..1f6005f6886 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -1,5 +1,21 @@ API endpoints with no coverage in authz tests: +organization_delete (delete "/organizations/{organization_name}") +project_delete (delete "/organizations/{organization_name}/projects/{project_name}") +instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") +instance_view_by_id (get "/by-id/instances/{id}") +organization_view_by_id (get "/by-id/organizations/{id}") +project_view_by_id (get "/by-id/projects/{id}") login_saml_begin (get "/login/{silo_name}/saml/{provider_name}") +organization_list (get "/organizations") +organization_view (get "/organizations/{organization_name}") +organization_policy_view (get "/organizations/{organization_name}/policy") +project_list (get "/organizations/{organization_name}/projects") +project_view (get "/organizations/{organization_name}/projects/{project_name}") +instance_list (get "/organizations/{organization_name}/projects/{project_name}/instances") +instance_view (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") +instance_serial_console (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console") +instance_serial_console_stream (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console/stream") +project_policy_view (get "/organizations/{organization_name}/projects/{project_name}/policy") device_auth_request (post "/device/auth") device_auth_confirm (post "/device/confirm") device_access_token (post "/device/token") @@ -7,16 +23,14 @@ login_spoof (post "/login") login_local (post "/login/{silo_name}/local") login_saml (post "/login/{silo_name}/saml/{provider_name}") logout (post "/logout") - -Deprecated API endpoints to be removed at the end of the V1 migration -instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") -instance_list (get "/organizations/{organization_name}/projects/{project_name}/instances") -instance_view (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") -instance_serial_console (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console") -instance_serial_console_stream (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console/stream") +organization_create (post "/organizations") +project_create (post "/organizations/{organization_name}/projects") instance_create (post "/organizations/{organization_name}/projects/{project_name}/instances") instance_migrate (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/migrate") instance_reboot (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/reboot") instance_start (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start") instance_stop (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop") -instance_view_by_id (get "/by-id/instances/{id}") \ No newline at end of file +organization_update (put "/organizations/{organization_name}") +organization_policy_update (put "/organizations/{organization_name}/policy") +project_update (put "/organizations/{organization_name}/projects/{project_name}") +project_policy_update (put "/organizations/{organization_name}/projects/{project_name}/policy") From 8db8fa2c639d05f9bd45c0b2a810200a020deb4b Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 17:25:05 -0500 Subject: [PATCH 53/72] Fix authz test --- nexus/src/external_api/http_entrypoints.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index aaaeed90945..1d00073fabd 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1891,15 +1891,20 @@ async fn project_policy_view( async fn project_policy_update_v1( rqctx: Arc>>, path_params: Path, + query_params: Query, new_policy: TypedBody>, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); + let query = query_params.into_inner(); let new_policy = new_policy.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_selector = params::ProjectSelector::new(None, path.project); + let project_selector = params::ProjectSelector { + organization_selector: query.organization_selector, + project: path.project, + }; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; nexus .project_update_policy(&opctx, &project_lookup, &new_policy) From 3a68689afef89afb53e0d0f22ab135230069b014 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 17:51:42 -0500 Subject: [PATCH 54/72] Fixup disk params --- nexus/src/app/disk.rs | 51 +++++++------------------- nexus/types/src/external_api/params.rs | 28 +++++++++++--- 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 5902ef95463..5230e588cd0 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -36,53 +36,28 @@ impl super::Nexus { disk_selector: &'a params::DiskSelector, ) -> LookupResult> { match disk_selector { - params::DiskSelector { disk: NameOrId::Id(id), .. } => { - let disk = - LookupPath::new(opctx, &self.db_datastore).disk_id(*id); - Ok(disk) - } - params::DiskSelector { - disk: NameOrId::Name(disk_name), - project: Some(NameOrId::Id(project_id)), - .. - } => { - let disk = LookupPath::new(opctx, &self.db_datastore) - .project_id(*project_id) - .disk_name(Name::ref_cast(disk_name)); - Ok(disk) - } params::DiskSelector { - disk: NameOrId::Name(disk_name), - project: Some(NameOrId::Name(project_name)), - organization: Some(NameOrId::Id(organization_id)), + disk: NameOrId::Id(id), + project_selector: None, } => { let disk = LookupPath::new(opctx, &self.db_datastore) - .organization_id(*organization_id) - .project_name(Name::ref_cast(project_name)) - .disk_name(Name::ref_cast(disk_name)); + .disk_id(*id) + .fetch(); Ok(disk) } params::DiskSelector { - disk: NameOrId::Name(disk_name), - project: Some(NameOrId::Name(project_name)), - organization: Some(NameOrId::Name(organization_name)), + disk: NameOrId::Name(name), + project_selector: Some(project_selector), } => { - let disk = LookupPath::new(opctx, &self.db_datastore) - .organization_name(Name::ref_cast(organization_name)) - .project_name(Name::ref_cast(project_name)) - .disk_name(Name::ref_cast(disk_name)); + let disk = self + .project_lookup(opctx, project_selector)? + .disk_name(name) + .fetch(); Ok(disk) } - _ => Err(Error::InvalidRequest { - message: " - Unable to resolve disk. Expected one of - - disk: Uuid - - disk: Name, project: Uuid - - disk: Name, project: Name, organization: Uuid - - disk: Name, project: Name, organization: Name - " - .to_string(), - }), + _ => Err(Error::invalid_request( + "disk should either be UUID or project should be specified", + )), } } diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 36124c071b4..b98d13ad357 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -83,6 +83,27 @@ pub struct OptionalProjectSelector { pub project_selector: Option, } +#[derive(Deserialize, JsonSchema)] +pub struct DiskSelector { + #[serde(flatten)] + pub project_selector: Option, + pub disk: NameOrId, +} + +impl DiskSelector { + pub fn new( + organization: Option, + project: Option, + disk: NameOrId, + ) -> Self { + DiskSelector { + project_selector: project + .map(|p| ProjectSelector::new(organization, p)), + disk, + } + } +} + #[derive(Deserialize, JsonSchema)] pub struct InstanceSelector { #[serde(flatten)] @@ -98,11 +119,8 @@ 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, } } From 33f09b56d1448956086006bb2519501450fac2ed Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 18:28:45 -0500 Subject: [PATCH 55/72] WIP --- nexus/src/app/disk.rs | 8 +- nexus/src/external_api/http_entrypoints.rs | 116 ++++++++------------- nexus/types/src/external_api/params.rs | 15 +++ 3 files changed, 64 insertions(+), 75 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 5230e588cd0..7c8caff42b2 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -40,9 +40,8 @@ impl super::Nexus { disk: NameOrId::Id(id), project_selector: None, } => { - let disk = LookupPath::new(opctx, &self.db_datastore) - .disk_id(*id) - .fetch(); + let disk = + LookupPath::new(opctx, &self.db_datastore).disk_id(*id); Ok(disk) } params::DiskSelector { @@ -51,8 +50,7 @@ impl super::Nexus { } => { let disk = self .project_lookup(opctx, project_selector)? - .disk_name(name) - .fetch(); + .disk_name(Name::ref_cast(name)); Ok(disk) } _ => Err(Error::invalid_request( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 305ad8953fb..24b222551d7 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2334,16 +2334,6 @@ async fn ip_pool_service_range_remove( // Disks -#[derive(Deserialize, JsonSchema)] -pub struct DiskListParams { - /// Optional filter to narrow disks returned to those attached to the given instance - instance: Option, - #[serde(flatten)] - selector: Option, - #[serde(flatten)] - pagination: PaginatedByName, -} - #[endpoint { method = GET, path = "/v1/disks", @@ -2351,7 +2341,7 @@ pub struct DiskListParams { }] async fn disk_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -2360,8 +2350,10 @@ async fn disk_list_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let disks = if let Some(instance) = instance { - let instance_selector = - params::InstanceSelector::new(instance, &query.selector); + let instance_selector = params::InstanceSelector { + instance, + project_selector: &query.selector, + }; let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; nexus @@ -2420,12 +2412,10 @@ async fn disk_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(path.organization_name.into()), + path.project_name.into(), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let authz_project = nexus.project_lookup(&opctx, &project_selector)?; @@ -2500,10 +2490,10 @@ async fn disk_create( let organization_name = &path.organization_name; let project_name = &path.project_name; let new_disk_params = &new_disk.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(organization_name.into()), + project_name.into(), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_lookup = nexus.project_lookup(&opctx, &project_selector)?; @@ -2515,24 +2505,6 @@ async fn disk_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Path parameters for Disk requests -#[derive(Deserialize, JsonSchema)] -struct DiskLookupPathParam { - /// If Name is used to reference the disk 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 - disk: NameOrId, -} - -#[derive(Deserialize, JsonSchema)] -struct DiskViewParams { - #[serde(flatten)] - selector: Option, -} - #[endpoint { method = GET, path = "/v1/disks/{disk}", @@ -2540,14 +2512,17 @@ struct DiskViewParams { }] async fn disk_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; let path = path_params.into_inner(); let query = query_params.into_inner(); - let disk_selector = params::DiskSelector::new(path.disk, &query.selector); + let disk_selector = params::DiskSelector { + disk: path.disk, + project_selector: query.project_selector, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let (.., disk) = @@ -2580,14 +2555,11 @@ async fn disk_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 disk_name = &path.disk_name; - let disk_selector = params::DiskSelector { - disk: NameOrId::Name(disk_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let disk_selector = params::DiskSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.disk_name.into(), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let (.., disk) = @@ -2612,8 +2584,7 @@ async fn disk_view_by_id( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let id = &path.id; - let disk_selector = params::DiskSelector::new(NameOrId::Id(*id), &None); + let disk_selector = params::DiskSelector::new(None, None, path.id.into()); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let (.., disk) = @@ -2631,14 +2602,17 @@ async fn disk_view_by_id( }] async fn disk_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; let path = path_params.into_inner(); let query = query_params.into_inner(); - let disk_selector = params::DiskSelector::new(path.disk, &query.selector); + let disk_selector = params::DiskSelector { + disk: path.disk, + project_selector: query.project_selector, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; @@ -2662,14 +2636,11 @@ async fn disk_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 disk_name = &path.disk_name; - let disk_selector = params::DiskSelector { - disk: NameOrId::Name(disk_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let disk_selector = params::DiskSelector::new( + Some(path.organization.into()), + Some(path.project.into()), + path.disk.into(), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; @@ -2687,8 +2658,8 @@ async fn disk_delete( }] async fn disk_attach_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, body: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -2696,9 +2667,14 @@ async fn disk_attach_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let body = body.into_inner(); - let instance_selector = - params::InstanceSelector::new(body.instance, &query.selector); - let disk_selector = params::DiskSelector::new(path.disk, &query.selector); + let instance_selector = params::InstanceSelector { + instance: body.instance, + project_selector: query.project_selector, + }; + let disk_selector = params::DiskSelector { + disk: path.disk, + project_selector: query.project_selector, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index b98d13ad357..64e015d26aa 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -29,6 +29,11 @@ pub struct ProjectPath { pub project: NameOrId, } +#[derive(Deserialize, JsonSchema)] +pub struct DiskPath { + pub disk: NameOrId, +} + #[derive(Deserialize, JsonSchema)] pub struct InstancePath { pub instance: NameOrId, @@ -104,6 +109,16 @@ impl DiskSelector { } } +#[derive(Deserialize, JsonSchema)] +pub struct DiskList { + /// Optional filter to only return disks attached to the given instance + pub instance: Option, + #[serde(flatten)] + pub project_selector: Option, + #[serde(flatten)] + pub pagination: PaginatedByName, +} + #[derive(Deserialize, JsonSchema)] pub struct InstanceSelector { #[serde(flatten)] From f949c6cea86bc989420ea1eb7afc763280be4ec2 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 18:42:21 -0500 Subject: [PATCH 56/72] Update nexus.json --- nexus/types/src/external_api/params.rs | 6 +----- openapi/nexus.json | 8 ++++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 121fc291de6..470371d9ee2 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -98,11 +98,7 @@ impl InstanceSelector { instance: NameOrId, ) -> Self { InstanceSelector { - project_selector: if let Some(p) = project { - Some(ProjectSelector::new(organization, p)) - } else { - None - }, + project_selector: project.map(|p| ProjectSelector::new(organization, p) ), instance, } } diff --git a/openapi/nexus.json b/openapi/nexus.json index df5124b7963..2c66ba85594 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -8865,6 +8865,14 @@ "$ref": "#/components/schemas/NameOrId" }, "style": "simple" + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + }, + "style": "form" } ], "requestBody": { From bd033a0a13d456900085562886fdb6b00f0dfef7 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 14 Dec 2022 19:03:30 -0500 Subject: [PATCH 57/72] format -_- --- nexus/types/src/external_api/params.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 470371d9ee2..6ab7a074242 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -98,7 +98,8 @@ impl InstanceSelector { instance: NameOrId, ) -> Self { InstanceSelector { - project_selector: project.map(|p| ProjectSelector::new(organization, p) ), + project_selector: project + .map(|p| ProjectSelector::new(organization, p)), instance, } } From 8eb8fca17f331fb24c4bff4dc0c30d6b6b69f1f2 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 15 Dec 2022 15:37:57 -0500 Subject: [PATCH 58/72] Update implementation to match project/org setup --- common/src/api/external/http_pagination.rs | 2 +- nexus/src/external_api/http_entrypoints.rs | 138 +++++++++------------ nexus/types/src/external_api/params.rs | 16 ++- openapi/nexus.json | 7 +- 4 files changed, 74 insertions(+), 89 deletions(-) diff --git a/common/src/api/external/http_pagination.rs b/common/src/api/external/http_pagination.rs index 1ae6706e101..905a24817b5 100644 --- a/common/src/api/external/http_pagination.rs +++ b/common/src/api/external/http_pagination.rs @@ -45,7 +45,7 @@ use crate::api::external::Name; use crate::api::external::ObjectIdentity; use crate::api::external::PaginationOrder; use dropshot::HttpError; -use dropshot::PaginationParams; +pub use dropshot::PaginationParams; use dropshot::RequestContext; use dropshot::ResultsPage; use dropshot::WhichPage; diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 24b222551d7..67d9115096c 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2341,7 +2341,7 @@ async fn ip_pool_service_range_remove( }] async fn disk_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; @@ -2352,7 +2352,7 @@ async fn disk_list_v1( let disks = if let Some(instance) = instance { let instance_selector = params::InstanceSelector { instance, - project_selector: &query.selector, + project_selector: query.project_selector, }; let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; @@ -2367,7 +2367,7 @@ async fn disk_list_v1( .into_iter() .map(|disk| disk.into()) .collect() - } else if let Some(selector) = query.selector { + } else if let Some(selector) = query.project_selector { let project_lookup = nexus.project_lookup(&opctx, &selector)?; nexus .project_list_disks( @@ -2383,7 +2383,8 @@ async fn disk_list_v1( } else { Err(Error::InvalidRequest { // TODO: Improve this error message - message: "instance or project selector required".to_string(), + message: "either instance or project must be specified" + .to_string(), })? }; Ok(HttpResponseOk(ScanByName::results_page( @@ -2487,12 +2488,10 @@ async fn disk_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_disk_params = &new_disk.into_inner(); let project_selector = params::ProjectSelector::new( - Some(organization_name.into()), - project_name.into(), + Some(path.organization_name.into()), + path.project_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2637,9 +2636,9 @@ async fn disk_delete( let nexus = &apictx.nexus; let path = path_params.into_inner(); let disk_selector = params::DiskSelector::new( - Some(path.organization.into()), - Some(path.project.into()), - path.disk.into(), + Some(path.organization_name.into()), + Some(path.project_name.into()), + path.disk_name.into(), ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; @@ -2669,7 +2668,7 @@ async fn disk_attach_v1( let body = body.into_inner(); let instance_selector = params::InstanceSelector { instance: body.instance, - project_selector: query.project_selector, + project_selector: query.project_selector.clone(), }; let disk_selector = params::DiskSelector { disk: path.disk, @@ -2696,8 +2695,8 @@ async fn disk_attach_v1( }] async fn disk_detach_v1( rqctx: Arc>>, - path_params: Path, - query_params: Query, + path_params: Path, + query_params: Query, body: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -2705,9 +2704,14 @@ async fn disk_detach_v1( let path = path_params.into_inner(); let query = query_params.into_inner(); let body = body.into_inner(); - let instance_selector = - params::InstanceSelector::new(body.instance, &query.selector); - let disk_selector = params::DiskSelector::new(path.disk, &query.selector); + let instance_selector = params::InstanceSelector { + instance: body.instance, + project_selector: query.project_selector.clone(), + }; + let disk_selector = params::DiskSelector { + disk: path.disk, + project_selector: query.project_selector, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -2733,16 +2737,6 @@ pub enum DiskMetricName { WriteBytes, } -#[derive(Deserialize, JsonSchema)] -pub struct DiskMetricsPaginationParams { - #[serde(flatten)] - pagination: - PaginationParams, - - #[serde(flatten)] - selector: Option, -} - #[endpoint { method = GET, path = "/v1/disks/{disk}/metrics/{metric_name}", @@ -2750,8 +2744,8 @@ pub struct DiskMetricsPaginationParams { }] async fn disk_metrics_list_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; @@ -2760,8 +2754,10 @@ async fn disk_metrics_list_v1( let query = query_params.into_inner(); let limit = rqctx.page_limit(&query.pagination)?; - let disk_selector = - params::DiskSelector::new(path.inner.disk, &query.selector); + let disk_selector = params::DiskSelector { + disk: path.inner.disk, + project_selector: query.project_selector, + }; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let (.., authz_disk) = nexus @@ -2801,21 +2797,14 @@ async fn disk_metrics_list( ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let organization_name = &path.inner.organization_name; - let project_name = &path.inner.project_name; - let disk_name = &path.inner.disk_name; - let metric_name = path.metric_name; - let query = query_params.into_inner(); let limit = rqctx.page_limit(&query)?; - - let disk_selector = params::DiskSelector { - disk: NameOrId::Name(disk_name.clone().into()), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let disk_selector = params::DiskSelector::new( + Some(path.inner.organization_name.into()), + Some(path.inner.project_name.into()), + path.inner.disk_name.into(), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let (.., authz_disk) = nexus @@ -2825,7 +2814,7 @@ async fn disk_metrics_list( let result = nexus .select_timeseries( - &format!("crucible_upstairs:{}", metric_name), + &format!("crucible_upstairs:{}", path.metric_name), &[&format!("upstairs_uuid=={}", authz_disk.id())], query, limit, @@ -3512,14 +3501,11 @@ async fn instance_disk_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 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(path.organization_name.into()), + Some(path.project_name.into()), + path.instance_name.into(), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3560,20 +3546,17 @@ async fn instance_disk_attach( 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 disk_name = disk_to_attach.into_inner().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 disk_selector = params::DiskSelector { - disk: NameOrId::Name(disk_name), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let disk = disk_to_attach.into_inner(); + let instance_selector = params::InstanceSelector::new( + Some(path.organization_name.clone().into()), + Some(path.project_name.clone().into()), + path.instance_name.into(), + ); + let disk_selector = params::DiskSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + disk.name.into(), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = @@ -3603,20 +3586,17 @@ async fn instance_disk_detach( 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 disk_name = disk_to_detach.into_inner().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 disk_selector = params::DiskSelector { - disk: NameOrId::Name(disk_name), - project: Some(NameOrId::Name(project_name.clone().into())), - organization: Some(NameOrId::Name(organization_name.clone().into())), - }; + let instance_selector = params::InstanceSelector::new( + Some(path.organization_name.clone().into()), + Some(path.project_name.clone().into()), + path.instance_name.into(), + ); + let disk_selector = params::DiskSelector::new( + Some(path.organization_name.into()), + Some(path.project_name.into()), + disk_name.into(), + ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 64e015d26aa..f318b225740 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -7,7 +7,7 @@ use crate::external_api::shared; use chrono::{DateTime, Utc}; use omicron_common::api::external::{ - http_pagination::{PaginatedByName, PaginatedByNameOrId}, + http_pagination::{PaginatedByName, PaginatedByNameOrId, PaginationParams}, ByteCount, IdentityMetadataCreateParams, IdentityMetadataUpdateParams, InstanceCpuCount, Ipv4Net, Ipv6Net, Name, NameOrId, }; @@ -39,7 +39,7 @@ pub struct InstancePath { pub instance: NameOrId, } -#[derive(Deserialize, JsonSchema)] +#[derive(Clone, Deserialize, JsonSchema)] pub struct OrganizationSelector { pub organization: NameOrId, } @@ -56,7 +56,7 @@ pub struct OptionalOrganizationSelector { pub organization_selector: Option, } -#[derive(Deserialize, JsonSchema)] +#[derive(Clone, Deserialize, JsonSchema)] pub struct ProjectSelector { #[serde(flatten)] pub organization_selector: Option, @@ -113,12 +113,22 @@ impl DiskSelector { pub struct DiskList { /// Optional filter to only return disks attached to the given instance pub instance: Option, + /// If `instance` is supplied as an ID this field should be left empty #[serde(flatten)] pub project_selector: Option, #[serde(flatten)] pub pagination: PaginatedByName, } +#[derive(Deserialize, JsonSchema)] +pub struct DiskMetricsList { + #[serde(flatten)] + pub pagination: PaginationParams, + + #[serde(flatten)] + pub project_selector: Option, +} + #[derive(Deserialize, JsonSchema)] pub struct InstanceSelector { #[serde(flatten)] diff --git a/openapi/nexus.json b/openapi/nexus.json index 725eb479e1e..b9217ce5fd8 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7710,7 +7710,7 @@ { "in": "query", "name": "instance", - "description": "Optional filter to narrow disks returned to those attached to the given instance", + "description": "Optional filter to only return disks attached to the given instance", "schema": { "$ref": "#/components/schemas/NameOrId" }, @@ -7848,7 +7848,6 @@ { "in": "path", "name": "disk", - "description": "If Name is used to reference the disk 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" @@ -7901,7 +7900,6 @@ { "in": "path", "name": "disk", - "description": "If Name is used to reference the disk 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" @@ -7949,7 +7947,6 @@ { "in": "path", "name": "disk", - "description": "If Name is used to reference the disk 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" @@ -8014,7 +8011,6 @@ { "in": "path", "name": "disk", - "description": "If Name is used to reference the disk 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" @@ -8078,7 +8074,6 @@ { "in": "path", "name": "disk", - "description": "If Name is used to reference the disk 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 69ac00809a03f69a323a74986f9e8deb3af12abe Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 15 Dec 2022 16:04:20 -0500 Subject: [PATCH 59/72] Add expanded error checking --- nexus/src/app/instance.rs | 12 ++++++++++-- nexus/src/app/project.rs | 10 +++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 5f30d3dc54e..a08c0e11627 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -79,10 +79,18 @@ impl super::Nexus { .instance_name(Name::ref_cast(name)); Ok(instance) } + params::InstanceSelector { + project_selector: Some(_), + instance: NameOrId::Id(_), + } => { + Err(Error::invalid_request( + "when providing instance as an ID, project should not be specified", + )) + } _ => { - return Err(Error::invalid_request( + Err(Error::invalid_request( "instance should either be UUID or project should be specified", - )); + )) } } } diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index 0a20c5cc1cb..7ba52f34c3a 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -51,8 +51,16 @@ impl super::Nexus { .project_name(Name::ref_cast(name)); Ok(project) } + params::ProjectSelector { + project: NameOrId::Id(_), + organization_selector: Some(_) + } => { + Err(Error::invalid_request( + "when providing project as an ID, organization should not be specified", + )) + } _ => Err(Error::invalid_request( - "project should either be UUID or organization should be specified" + "project should either be specified by id or organization should be specified" )), } } From 11762116f12640bf172b9f652667f6f0cbb3f386 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 16 Dec 2022 13:16:54 -0500 Subject: [PATCH 60/72] Update nexus.json --- openapi/nexus.json | 69 ++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/openapi/nexus.json b/openapi/nexus.json index dfa8a2e40c0..d1f50144527 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7913,8 +7913,7 @@ "type": "integer", "format": "uint32", "minimum": 1 - }, - "style": "form" + } }, { "in": "query", @@ -7923,16 +7922,14 @@ "schema": { "nullable": true, "type": "string" - }, - "style": "form" + } }, { "in": "query", "name": "sort_by", "schema": { "$ref": "#/components/schemas/NameOrIdSortMode" - }, - "style": "form" + } } ], "responses": { @@ -8004,8 +8001,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -8039,8 +8035,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "requestBody": { @@ -8084,8 +8079,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -8114,8 +8108,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -8149,8 +8142,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "requestBody": { @@ -8200,8 +8192,7 @@ "type": "integer", "format": "uint32", "minimum": 1 - }, - "style": "form" + } }, { "in": "query", @@ -8209,8 +8200,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", @@ -8219,16 +8209,14 @@ "schema": { "nullable": true, "type": "string" - }, - "style": "form" + } }, { "in": "query", "name": "sort_by", "schema": { "$ref": "#/components/schemas/NameOrIdSortMode" - }, - "style": "form" + } } ], "responses": { @@ -8263,8 +8251,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { @@ -8310,16 +8297,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -8354,16 +8339,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { @@ -8408,16 +8391,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -8447,16 +8428,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -8491,16 +8470,14 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { From 4065a3a83466ff294ac24a09224b3471e5b3ba27 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 19 Dec 2022 14:40:23 -0500 Subject: [PATCH 61/72] Fix type failures, fix some failing tests --- nexus/src/app/disk.rs | 6 + nexus/src/external_api/http_entrypoints.rs | 10 +- nexus/tests/integration_tests/endpoints.rs | 3 +- openapi/nexus.json | 2005 +++++++++++++++----- 4 files changed, 1520 insertions(+), 504 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 7c8caff42b2..61cd8b1b35b 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -53,6 +53,12 @@ impl super::Nexus { .disk_name(Name::ref_cast(name)); Ok(disk) } + params::DiskSelector { + disk: NameOrId::Id(_), + project_selector: Some(_), + } => Err(Error::invalid_request( + "when providing disk as an ID, project should not be specified", + )), _ => Err(Error::invalid_request( "disk should either be UUID or project should be specified", )), diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 67d9115096c..15dab76de11 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2440,12 +2440,6 @@ async fn disk_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -#[derive(Deserialize, JsonSchema)] -pub struct DiskCreateParams { - #[serde(flatten)] - selector: params::ProjectSelector, -} - /// Create a disk #[endpoint { method = POST, @@ -2454,7 +2448,7 @@ pub struct DiskCreateParams { }] async fn disk_create_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query, new_disk: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -2463,7 +2457,7 @@ async fn disk_create_v1( let new_disk_params = &new_disk.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_lookup = nexus.project_lookup(&opctx, &query.selector)?; + let project_lookup = nexus.project_lookup(&opctx, &query)?; let disk = nexus .project_create_disk(&opctx, &project_lookup, new_disk_params) .await?; diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 7b376590f5c..81233d2e17e 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -218,8 +218,9 @@ lazy_static! { format!("/v1/disks/{}/detach?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_DISK_METRICS_URL: String = format!( - "/v1/disks/{}/metrics/activated?start_time={:?}&end_time={:?}", + "/v1/disks/{}/metrics/activated?{}&start_time={:?}&end_time={:?}", *DEMO_DISK_NAME, + *DEMO_PROJECT_SELECTOR, Utc::now(), Utc::now(), ); diff --git a/openapi/nexus.json b/openapi/nexus.json index 493045f0fe0..6efd430a377 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -12,7 +12,9 @@ "paths": { "/by-id/disks/{id}": { "get": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "Fetch a disk by id", "description": "Use `GET /v1/disks/{disk}` instead", "operationId": "disk_view_by_id", @@ -50,7 +52,9 @@ }, "/by-id/images/{id}": { "get": { - "tags": ["images"], + "tags": [ + "images" + ], "summary": "Fetch an image by id", "operationId": "image_view_by_id", "parameters": [ @@ -86,7 +90,9 @@ }, "/by-id/instances/{id}": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Fetch an instance by id", "operationId": "instance_view_by_id", "parameters": [ @@ -122,7 +128,9 @@ }, "/by-id/network-interfaces/{id}": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Fetch a network interface by id", "operationId": "instance_network_interface_view_by_id", "parameters": [ @@ -158,7 +166,9 @@ }, "/by-id/organizations/{id}": { "get": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "Fetch an organization by id", "description": "Use `GET /v1/organizations/{organization}` instead", "operationId": "organization_view_by_id", @@ -196,7 +206,9 @@ }, "/by-id/projects/{id}": { "get": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Fetch a project by id", "description": "Use `GET /v1/projects/{project}` instead", "operationId": "project_view_by_id", @@ -234,7 +246,9 @@ }, "/by-id/snapshots/{id}": { "get": { - "tags": ["snapshots"], + "tags": [ + "snapshots" + ], "summary": "Fetch a snapshot by id", "operationId": "snapshot_view_by_id", "parameters": [ @@ -270,7 +284,9 @@ }, "/by-id/vpc-router-routes/{id}": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Fetch a route by id", "operationId": "vpc_router_route_view_by_id", "parameters": [ @@ -306,7 +322,9 @@ }, "/by-id/vpc-routers/{id}": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Get a router by id", "operationId": "vpc_router_view_by_id", "parameters": [ @@ -342,7 +360,9 @@ }, "/by-id/vpc-subnets/{id}": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Fetch a subnet by id", "operationId": "vpc_subnet_view_by_id", "parameters": [ @@ -378,7 +398,9 @@ }, "/by-id/vpcs/{id}": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Fetch a VPC", "operationId": "vpc_view_by_id", "parameters": [ @@ -414,7 +436,9 @@ }, "/device/auth": { "post": { - "tags": ["hidden"], + "tags": [ + "hidden" + ], "summary": "Start an OAuth 2.0 Device Authorization Grant", "description": "This endpoint is designed to be accessed from an *unauthenticated* API client. It generates and records a `device_code` and `user_code` which must be verified and confirmed prior to a token being granted.", "operationId": "device_auth_request", @@ -442,7 +466,9 @@ }, "/device/confirm": { "post": { - "tags": ["hidden"], + "tags": [ + "hidden" + ], "summary": "Confirm an OAuth 2.0 Device Authorization Grant", "description": "This endpoint is designed to be accessed by the user agent (browser), not the client requesting the token. So we do not actually return the token here; it will be returned in response to the poll on `/device/token`.", "operationId": "device_auth_confirm", @@ -471,7 +497,9 @@ }, "/device/token": { "post": { - "tags": ["hidden"], + "tags": [ + "hidden" + ], "summary": "Request a device access token", "description": "This endpoint should be polled by the client until the user code is verified and the grant is confirmed.", "operationId": "device_access_token", @@ -499,7 +527,9 @@ }, "/groups": { "get": { - "tags": ["silos"], + "tags": [ + "silos" + ], "summary": "List groups", "operationId": "group_list", "parameters": [ @@ -554,7 +584,9 @@ }, "/login": { "post": { - "tags": ["hidden"], + "tags": [ + "hidden" + ], "operationId": "login_spoof", "requestBody": { "content": { @@ -581,7 +613,9 @@ }, "/login/{silo_name}/local": { "post": { - "tags": ["login"], + "tags": [ + "login" + ], "summary": "Authenticate a user (i.e., log in) via username and password", "operationId": "login_local", "parameters": [ @@ -629,7 +663,9 @@ }, "/login/{silo_name}/saml/{provider_name}": { "get": { - "tags": ["login"], + "tags": [ + "login" + ], "summary": "Prompt user login", "description": "Either display a page asking a user for their credentials, or redirect them to their identity provider.", "operationId": "login_saml_begin", @@ -674,7 +710,9 @@ } }, "post": { - "tags": ["login"], + "tags": [ + "login" + ], "summary": "Authenticate a user (i.e., log in) via SAML", "operationId": "login_saml", "parameters": [ @@ -731,7 +769,9 @@ }, "/logout": { "post": { - "tags": ["hidden"], + "tags": [ + "hidden" + ], "operationId": "logout", "responses": { "204": { @@ -748,7 +788,9 @@ }, "/organizations": { "get": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "List organizations", "description": "Use `/v1/organizations` instead", "operationId": "organization_list", @@ -803,7 +845,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "Create an organization", "description": "Use `POST /v1/organizations` instead", "operationId": "organization_create", @@ -840,7 +884,9 @@ }, "/organizations/{organization_name}": { "get": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "Fetch an organization", "description": "Use `GET /v1/organizations/{organization}` instead", "operationId": "organization_view", @@ -876,7 +922,9 @@ "deprecated": true }, "put": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "Update an organization", "description": "Use `PUT /v1/organizations/{organization}` instead", "operationId": "organization_update", @@ -922,7 +970,9 @@ "deprecated": true }, "delete": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "Delete an organization", "description": "Use `DELETE /v1/organizations/{organization}` instead", "operationId": "organization_delete", @@ -953,7 +1003,9 @@ }, "/organizations/{organization_name}/policy": { "get": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "Fetch an organization's IAM policy", "description": "Use `GET /v1/organizations/{organization}/policy` instead", "operationId": "organization_policy_view", @@ -989,7 +1041,9 @@ "deprecated": true }, "put": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "Update an organization's IAM policy", "description": "Use `PUT /v1/organizations/{organization}/policy` instead", "operationId": "organization_policy_update", @@ -1037,7 +1091,9 @@ }, "/organizations/{organization_name}/projects": { "get": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "List projects", "description": "Use `GET /v1/projects` instead", "operationId": "project_list", @@ -1101,7 +1157,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Create a project", "description": "Use `POST /v1/projects` instead", "operationId": "project_create", @@ -1149,7 +1207,9 @@ }, "/organizations/{organization_name}/projects/{project_name}": { "get": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Fetch a project", "description": "Use `GET /v1/projects/{project}` instead", "operationId": "project_view", @@ -1194,7 +1254,9 @@ "deprecated": true }, "put": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Update a project", "description": "Use `PUT /v1/projects/{project}` instead", "operationId": "project_update", @@ -1249,7 +1311,9 @@ "deprecated": true }, "delete": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Delete a project", "description": "Use `DELETE /v1/projects/{project}` instead", "operationId": "project_delete", @@ -1289,7 +1353,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/disks": { "get": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "List disks", "description": "Use `GET /v1/disks` instead", "operationId": "disk_list", @@ -1362,7 +1428,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "Use `POST /v1/disks` instead", "operationId": "disk_create", "parameters": [ @@ -1418,7 +1486,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}": { "get": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "Fetch a disk", "description": "Use `GET /v1/disks/{disk}` instead", "operationId": "disk_view", @@ -1469,7 +1539,9 @@ "deprecated": true }, "delete": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "Use `DELETE /v1/disks/{disk}` instead", "operationId": "disk_delete", "parameters": [ @@ -1514,7 +1586,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name}": { "get": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "Fetch disk metrics", "description": "Use `GET /v1/disks/{disk}/metrics/{metric_name}` instead", "operationId": "disk_metrics_list", @@ -1614,7 +1688,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/images": { "get": { - "tags": ["images"], + "tags": [ + "images" + ], "summary": "List images", "description": "List images in a project. The images are returned sorted by creation date, with the most recent images appearing first.", "operationId": "image_list", @@ -1686,7 +1762,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["images"], + "tags": [ + "images" + ], "summary": "Create an image", "description": "Create a new image in a project.", "operationId": "image_create", @@ -1742,7 +1820,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/images/{image_name}": { "get": { - "tags": ["images"], + "tags": [ + "images" + ], "summary": "Fetch an image", "description": "Fetch the details for a specific image in a project.", "operationId": "image_view", @@ -1792,7 +1872,9 @@ } }, "delete": { - "tags": ["images"], + "tags": [ + "images" + ], "summary": "Delete an image", "description": "Permanently delete an image from a project. This operation cannot be undone. Any instances in the project using the image will continue to run, however new instances can not be created with this image.", "operationId": "image_delete", @@ -1837,7 +1919,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "List instances", "operationId": "instance_list", "parameters": [ @@ -1908,7 +1992,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Create an instance", "operationId": "instance_create", "parameters": [ @@ -1963,7 +2049,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Fetch an instance", "operationId": "instance_view", "parameters": [ @@ -2012,7 +2100,9 @@ } }, "delete": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Delete an instance", "operationId": "instance_delete", "parameters": [ @@ -2056,7 +2146,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "List an instance's disks", "description": "Use `GET /v1/disks?instance={instance}` instead", "operationId": "instance_disk_list", @@ -2137,7 +2229,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Attach a disk to an instance", "description": "Use `POST /v1/disks/{disk}/attach { instance: } ` instead", "operationId": "instance_disk_attach", @@ -2200,7 +2294,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Detach a disk from an instance", "description": "Use `POST /v1/disks/{disk}/detach { instance: } ` instead", "operationId": "instance_disk_detach", @@ -2263,7 +2359,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/external-ips": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "List external IP addresses", "operationId": "instance_external_ip_list", "parameters": [ @@ -2314,7 +2412,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/migrate": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Migrate an instance", "operationId": "instance_migrate", "parameters": [ @@ -2375,7 +2475,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "List network interfaces", "operationId": "instance_network_interface_list", "parameters": [ @@ -2452,7 +2554,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Create a network interface", "operationId": "instance_network_interface_create", "parameters": [ @@ -2513,7 +2617,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Fetch a network interface", "operationId": "instance_network_interface_view", "parameters": [ @@ -2570,7 +2676,9 @@ } }, "put": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Update a network interface", "operationId": "instance_network_interface_update", "parameters": [ @@ -2637,7 +2745,9 @@ } }, "delete": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Delete a network interface", "description": "Note that the primary interface for an instance cannot be deleted if there are any secondary interfaces. A new primary interface must be designated first. The primary interface can be deleted if there are no secondary interfaces.", "operationId": "instance_network_interface_delete", @@ -2690,7 +2800,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/reboot": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Reboot an instance", "operationId": "instance_reboot", "parameters": [ @@ -2741,7 +2853,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Fetch an instance's serial console", "operationId": "instance_serial_console", "parameters": [ @@ -2825,7 +2939,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console/stream": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Connect to an instance's serial console", "operationId": "instance_serial_console_stream", "parameters": [ @@ -2869,7 +2985,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Boot an instance", "operationId": "instance_start", "parameters": [ @@ -2920,7 +3038,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/stop": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Halt an instance", "operationId": "instance_stop", "parameters": [ @@ -2971,7 +3091,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/policy": { "get": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Fetch a project's IAM policy", "description": "Use `GET /v1/projects/{project}/policy` instead", "operationId": "project_policy_view", @@ -3016,7 +3138,9 @@ "deprecated": true }, "put": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Update a project's IAM policy", "operationId": "project_policy_update", "parameters": [ @@ -3071,7 +3195,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/snapshots": { "get": { - "tags": ["snapshots"], + "tags": [ + "snapshots" + ], "summary": "List snapshots", "operationId": "snapshot_list", "parameters": [ @@ -3142,7 +3268,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["snapshots"], + "tags": [ + "snapshots" + ], "summary": "Create a snapshot", "description": "Creates a point-in-time snapshot from a disk.", "operationId": "snapshot_create", @@ -3198,7 +3326,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}": { "get": { - "tags": ["snapshots"], + "tags": [ + "snapshots" + ], "summary": "Fetch a snapshot", "operationId": "snapshot_view", "parameters": [ @@ -3247,7 +3377,9 @@ } }, "delete": { - "tags": ["snapshots"], + "tags": [ + "snapshots" + ], "summary": "Delete a snapshot", "operationId": "snapshot_delete", "parameters": [ @@ -3291,7 +3423,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "List VPCs", "operationId": "vpc_list", "parameters": [ @@ -3362,7 +3496,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Create a VPC", "operationId": "vpc_create", "parameters": [ @@ -3417,7 +3553,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Fetch a VPC", "operationId": "vpc_view", "parameters": [ @@ -3466,7 +3604,9 @@ } }, "put": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Update a VPC", "operationId": "vpc_update", "parameters": [ @@ -3525,7 +3665,9 @@ } }, "delete": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Delete a VPC", "operationId": "vpc_delete", "parameters": [ @@ -3569,7 +3711,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/firewall/rules": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "List firewall rules", "operationId": "vpc_firewall_rules_view", "parameters": [ @@ -3618,7 +3762,9 @@ } }, "put": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Replace firewall rules", "operationId": "vpc_firewall_rules_update", "parameters": [ @@ -3679,7 +3825,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "List routers", "operationId": "vpc_router_list", "parameters": [ @@ -3756,7 +3904,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Create a router", "operationId": "vpc_router_create", "parameters": [ @@ -3817,7 +3967,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Get a router", "operationId": "vpc_router_view", "parameters": [ @@ -3874,7 +4026,9 @@ } }, "put": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Update a router", "operationId": "vpc_router_update", "parameters": [ @@ -3941,7 +4095,9 @@ } }, "delete": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Delete a router", "operationId": "vpc_router_delete", "parameters": [ @@ -3993,7 +4149,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "List routes", "description": "List the routes associated with a router in a particular VPC.", "operationId": "vpc_router_route_list", @@ -4079,7 +4237,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Create a router", "operationId": "vpc_router_route_create", "parameters": [ @@ -4148,7 +4308,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Fetch a route", "operationId": "vpc_router_route_view", "parameters": [ @@ -4213,7 +4375,9 @@ } }, "put": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Update a route", "operationId": "vpc_router_route_update", "parameters": [ @@ -4288,7 +4452,9 @@ } }, "delete": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Delete a route", "operationId": "vpc_router_route_delete", "parameters": [ @@ -4348,7 +4514,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "List subnets", "operationId": "vpc_subnet_list", "parameters": [ @@ -4425,7 +4593,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Create a subnet", "operationId": "vpc_subnet_create", "parameters": [ @@ -4486,7 +4656,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Fetch a subnet", "operationId": "vpc_subnet_view", "parameters": [ @@ -4543,7 +4715,9 @@ } }, "put": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Update a subnet", "operationId": "vpc_subnet_update", "parameters": [ @@ -4610,7 +4784,9 @@ } }, "delete": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "Delete a subnet", "operationId": "vpc_subnet_delete", "parameters": [ @@ -4662,7 +4838,9 @@ }, "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/network-interfaces": { "get": { - "tags": ["vpcs"], + "tags": [ + "vpcs" + ], "summary": "List network interfaces", "operationId": "vpc_subnet_list_network_interfaces", "parameters": [ @@ -4749,7 +4927,9 @@ }, "/policy": { "get": { - "tags": ["silos"], + "tags": [ + "silos" + ], "summary": "Fetch the current silo's IAM policy", "operationId": "policy_view", "responses": { @@ -4772,7 +4952,9 @@ } }, "put": { - "tags": ["silos"], + "tags": [ + "silos" + ], "summary": "Update the current silo's IAM policy", "operationId": "policy_update", "requestBody": { @@ -4807,7 +4989,9 @@ }, "/roles": { "get": { - "tags": ["roles"], + "tags": [ + "roles" + ], "summary": "List built-in roles", "operationId": "role_list", "parameters": [ @@ -4855,7 +5039,9 @@ }, "/roles/{role_name}": { "get": { - "tags": ["roles"], + "tags": [ + "roles" + ], "summary": "Fetch a built-in role", "operationId": "role_view", "parameters": [ @@ -4891,7 +5077,9 @@ }, "/session/me": { "get": { - "tags": ["hidden"], + "tags": [ + "hidden" + ], "summary": "Fetch the user associated with the current session", "operationId": "session_me", "responses": { @@ -4916,7 +5104,9 @@ }, "/session/me/groups": { "get": { - "tags": ["hidden"], + "tags": [ + "hidden" + ], "summary": "Fetch the silo groups the current user belongs to", "operationId": "session_me_groups", "parameters": [ @@ -4971,7 +5161,9 @@ }, "/session/me/sshkeys": { "get": { - "tags": ["session"], + "tags": [ + "session" + ], "summary": "List SSH public keys", "description": "Lists SSH public keys for the currently authenticated user.", "operationId": "session_sshkey_list", @@ -5025,7 +5217,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["session"], + "tags": [ + "session" + ], "summary": "Create an SSH public key", "description": "Create an SSH public key for the currently authenticated user.", "operationId": "session_sshkey_create", @@ -5061,7 +5255,9 @@ }, "/session/me/sshkeys/{ssh_key_name}": { "get": { - "tags": ["session"], + "tags": [ + "session" + ], "summary": "Fetch an SSH public key", "description": "Fetch an SSH public key associated with the currently authenticated user.", "operationId": "session_sshkey_view", @@ -5095,7 +5291,9 @@ } }, "delete": { - "tags": ["session"], + "tags": [ + "session" + ], "summary": "Delete an SSH public key", "description": "Delete an SSH public key associated with the currently authenticated user.", "operationId": "session_sshkey_delete", @@ -5124,7 +5322,9 @@ }, "/system/by-id/images/{id}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a system-wide image by id", "operationId": "system_image_view_by_id", "parameters": [ @@ -5160,7 +5360,9 @@ }, "/system/by-id/ip-pools/{id}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch an IP pool by id", "operationId": "ip_pool_view_by_id", "parameters": [ @@ -5196,7 +5398,9 @@ }, "/system/by-id/silos/{id}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a silo by id", "operationId": "silo_view_by_id", "parameters": [ @@ -5232,7 +5436,9 @@ }, "/system/hardware/racks": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List racks", "operationId": "rack_list", "parameters": [ @@ -5287,7 +5493,9 @@ }, "/system/hardware/racks/{rack_id}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a rack", "operationId": "rack_view", "parameters": [ @@ -5324,7 +5532,9 @@ }, "/system/hardware/sleds": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List sleds", "operationId": "sled_list", "parameters": [ @@ -5379,7 +5589,9 @@ }, "/system/hardware/sleds/{sled_id}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a sled", "operationId": "sled_view", "parameters": [ @@ -5416,7 +5628,9 @@ }, "/system/images": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List system-wide images", "description": "Returns a list of all the system-wide images. System-wide images are returned sorted by creation date, with the most recent images appearing first.", "operationId": "system_image_list", @@ -5470,7 +5684,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Create a system-wide image", "description": "Create a new system-wide image. This image can then be used by any user in any silo as a base for instances.", "operationId": "system_image_create", @@ -5506,7 +5722,9 @@ }, "/system/images/{image_name}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a system-wide image", "description": "Returns the details of a specific system-wide image.", "operationId": "system_image_view", @@ -5540,7 +5758,9 @@ } }, "delete": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Delete a system-wide image", "description": "Permanently delete a system-wide image. This operation cannot be undone. Any instances using the system-wide image will continue to run, however new instances can not be created with this image.", "operationId": "system_image_delete", @@ -5569,7 +5789,9 @@ }, "/system/ip-pools": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List IP pools", "operationId": "ip_pool_list", "parameters": [ @@ -5622,7 +5844,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Create an IP pool", "operationId": "ip_pool_create", "requestBody": { @@ -5657,7 +5881,9 @@ }, "/system/ip-pools/{pool_name}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch an IP pool", "operationId": "ip_pool_view", "parameters": [ @@ -5690,7 +5916,9 @@ } }, "put": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Update an IP Pool", "operationId": "ip_pool_update", "parameters": [ @@ -5733,7 +5961,9 @@ } }, "delete": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Delete an IP Pool", "operationId": "ip_pool_delete", "parameters": [ @@ -5761,7 +5991,9 @@ }, "/system/ip-pools/{pool_name}/ranges": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List ranges for an IP pool", "description": "Ranges are ordered by their first address.", "operationId": "ip_pool_range_list", @@ -5818,7 +6050,9 @@ }, "/system/ip-pools/{pool_name}/ranges/add": { "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Add a range to an IP pool", "operationId": "ip_pool_range_add", "parameters": [ @@ -5863,7 +6097,9 @@ }, "/system/ip-pools/{pool_name}/ranges/remove": { "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Remove a range from an IP pool", "operationId": "ip_pool_range_remove", "parameters": [ @@ -5901,7 +6137,9 @@ }, "/system/ip-pools-service/{rack_id}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch an IP pool used for Oxide services.", "operationId": "ip_pool_service_view", "parameters": [ @@ -5937,7 +6175,9 @@ }, "/system/ip-pools-service/{rack_id}/ranges": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List ranges for an IP pool used for Oxide services.", "description": "Ranges are ordered by their first address.", "operationId": "ip_pool_service_range_list", @@ -5995,7 +6235,9 @@ }, "/system/ip-pools-service/{rack_id}/ranges/add": { "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Add a range to an IP pool used for Oxide services.", "operationId": "ip_pool_service_range_add", "parameters": [ @@ -6041,7 +6283,9 @@ }, "/system/ip-pools-service/{rack_id}/ranges/remove": { "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Remove a range from an IP pool used for Oxide services.", "operationId": "ip_pool_service_range_remove", "parameters": [ @@ -6080,7 +6324,9 @@ }, "/system/policy": { "get": { - "tags": ["policy"], + "tags": [ + "policy" + ], "summary": "Fetch the top-level IAM policy", "operationId": "system_policy_view", "responses": { @@ -6103,7 +6349,9 @@ } }, "put": { - "tags": ["policy"], + "tags": [ + "policy" + ], "summary": "Update the top-level IAM policy", "operationId": "system_policy_update", "requestBody": { @@ -6138,7 +6386,9 @@ }, "/system/sagas": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List sagas", "operationId": "saga_list", "parameters": [ @@ -6193,7 +6443,9 @@ }, "/system/sagas/{saga_id}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a saga", "operationId": "saga_view", "parameters": [ @@ -6229,7 +6481,9 @@ }, "/system/silos": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List silos", "description": "Lists silos that are discoverable based on the current permissions.", "operationId": "silo_list", @@ -6283,7 +6537,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Create a silo", "operationId": "silo_create", "requestBody": { @@ -6318,7 +6574,9 @@ }, "/system/silos/{silo_name}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a silo", "description": "Fetch a silo by name.", "operationId": "silo_view", @@ -6353,7 +6611,9 @@ } }, "delete": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Delete a silo", "description": "Delete a silo by name.", "operationId": "silo_delete", @@ -6383,7 +6643,9 @@ }, "/system/silos/{silo_name}/identity-providers": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List a silo's IDPs", "operationId": "silo_identity_provider_list", "parameters": [ @@ -6447,7 +6709,9 @@ }, "/system/silos/{silo_name}/identity-providers/local/users": { "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Create a user", "description": "Users can only be created in Silos with `provision_type` == `Fixed`. Otherwise, Silo users are just-in-time (JIT) provisioned when a user first logs in using an external Identity Provider.", "operationId": "local_idp_user_create", @@ -6494,7 +6758,9 @@ }, "/system/silos/{silo_name}/identity-providers/local/users/{user_id}": { "delete": { - "tags": ["system"], + "tags": [ + "system" + ], "operationId": "local_idp_user_delete", "parameters": [ { @@ -6532,7 +6798,9 @@ }, "/system/silos/{silo_name}/identity-providers/local/users/{user_id}/set-password": { "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Set or invalidate a user's password", "description": "Passwords can only be updated for users in Silos with identity mode `LocalOnly`.", "operationId": "local_idp_user_set_password", @@ -6582,7 +6850,9 @@ }, "/system/silos/{silo_name}/identity-providers/saml": { "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Create a SAML IDP", "operationId": "saml_identity_provider_create", "parameters": [ @@ -6628,7 +6898,9 @@ }, "/system/silos/{silo_name}/identity-providers/saml/{provider_name}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a SAML IDP", "operationId": "saml_identity_provider_view", "parameters": [ @@ -6673,7 +6945,9 @@ }, "/system/silos/{silo_name}/policy": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a silo's IAM policy", "operationId": "silo_policy_view", "parameters": [ @@ -6707,7 +6981,9 @@ } }, "put": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Update a silo's IAM policy", "operationId": "silo_policy_update", "parameters": [ @@ -6753,7 +7029,9 @@ }, "/system/silos/{silo_name}/users/all": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List users in a specific Silo", "operationId": "silo_users_list", "parameters": [ @@ -6817,7 +7095,9 @@ }, "/system/silos/{silo_name}/users/id/{user_id}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "operationId": "silo_user_view", "parameters": [ { @@ -6862,7 +7142,9 @@ }, "/system/updates/refresh": { "post": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Refresh update data", "operationId": "updates_refresh", "responses": { @@ -6880,7 +7162,9 @@ }, "/system/user": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "List built-in users", "operationId": "system_user_list", "parameters": [ @@ -6935,7 +7219,9 @@ }, "/system/user/{user_name}": { "get": { - "tags": ["system"], + "tags": [ + "system" + ], "summary": "Fetch a built-in user", "operationId": "system_user_view", "parameters": [ @@ -6971,7 +7257,9 @@ }, "/timeseries/schema": { "get": { - "tags": ["metrics"], + "tags": [ + "metrics" + ], "summary": "List timeseries schema", "operationId": "timeseries_schema_get", "parameters": [ @@ -7019,7 +7307,9 @@ }, "/users": { "get": { - "tags": ["silos"], + "tags": [ + "silos" + ], "summary": "List users", "operationId": "user_list", "parameters": [ @@ -7074,7 +7364,9 @@ }, "/v1/disks": { "get": { - "tags": ["disks"], + "tags": [ + "disks" + ], "operationId": "disk_list_v1", "parameters": [ { @@ -7083,8 +7375,7 @@ "description": "Optional filter to only return disks attached to the given instance", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", @@ -7149,7 +7440,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "Create a disk", "operationId": "disk_create_v1", "parameters": [ @@ -7201,7 +7494,9 @@ }, "/v1/disks/{disk}": { "get": { - "tags": ["disks"], + "tags": [ + "disks" + ], "operationId": "disk_view_v1", "parameters": [ { @@ -7247,7 +7542,9 @@ } }, "delete": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "Delete a disk", "operationId": "disk_delete_v1", "parameters": [ @@ -7289,7 +7586,9 @@ }, "/v1/disks/{disk}/attach": { "post": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "Attach a disk to an instance", "operationId": "disk_attach_v1", "parameters": [ @@ -7348,7 +7647,9 @@ }, "/v1/disks/{disk}/detach": { "post": { - "tags": ["disks"], + "tags": [ + "disks" + ], "summary": "Detach a disk from an instance", "operationId": "disk_detach_v1", "parameters": [ @@ -7407,7 +7708,9 @@ }, "/v1/disks/{disk}/metrics/{metric_name}": { "get": { - "tags": ["disks"], + "tags": [ + "disks" + ], "operationId": "disk_metrics_list_v1", "parameters": [ { @@ -7423,10 +7726,7 @@ "name": "metric_name", "required": true, "schema": { - "nullable": true, - "type": "integer", - "format": "uint64", - "minimum": 0 + "$ref": "#/components/schemas/DiskMetricName" } }, { @@ -7434,10 +7734,8 @@ "name": "end_time", "description": "An exclusive end time of metrics.", "schema": { - "nullable": true, - "type": "integer", - "format": "uint64", - "minimum": 0 + "type": "string", + "format": "date-time" } }, { @@ -7447,8 +7745,8 @@ "schema": { "nullable": true, "type": "integer", - "format": "uint64", - "minimum": 0 + "format": "uint32", + "minimum": 1 } }, { @@ -7465,8 +7763,7 @@ "schema": { "nullable": true, "type": "string" - }, - "style": "form" + } }, { "in": "query", @@ -7474,6 +7771,15 @@ "schema": { "$ref": "#/components/schemas/NameOrId" } + }, + { + "in": "query", + "name": "start_time", + "description": "An inclusive start time of metrics.", + "schema": { + "type": "string", + "format": "date-time" + } } ], "responses": { @@ -7499,7 +7805,9 @@ }, "/v1/instances": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "operationId": "instance_list_v1", "parameters": [ { @@ -7507,7 +7815,10 @@ "name": "limit", "description": "Maximum number of items returned by a single call", "schema": { - "$ref": "#/components/schemas/NameOrId" + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 } }, { @@ -7524,8 +7835,7 @@ "schema": { "nullable": true, "type": "string" - }, - "style": "form" + } }, { "in": "query", @@ -7534,6 +7844,13 @@ "schema": { "$ref": "#/components/schemas/NameOrId" } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameSortMode" + } } ], "responses": { @@ -7557,7 +7874,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "operationId": "instance_create_v1", "parameters": [ { @@ -7565,8 +7884,7 @@ "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", @@ -7574,8 +7892,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "requestBody": { @@ -7610,7 +7927,9 @@ }, "/v1/instances/{instance}": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "operationId": "instance_view_v1", "parameters": [ { @@ -7618,16 +7937,14 @@ "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", "name": "project", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "path", @@ -7635,8 +7952,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -7659,7 +7975,9 @@ } }, "delete": { - "tags": ["instances"], + "tags": [ + "instances" + ], "operationId": "instance_delete_v1", "parameters": [ { @@ -7667,16 +7985,14 @@ "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", "name": "project", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "path", @@ -7684,8 +8000,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -7703,7 +8018,9 @@ }, "/v1/instances/{instance}/migrate": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "operationId": "instance_migrate_v1", "parameters": [ { @@ -7711,16 +8028,14 @@ "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", "name": "project", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "path", @@ -7728,8 +8043,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "requestBody": { @@ -7764,7 +8078,9 @@ }, "/v1/instances/{instance}/reboot": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "operationId": "instance_reboot_v1", "parameters": [ { @@ -7772,16 +8088,14 @@ "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", "name": "project", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "path", @@ -7789,8 +8103,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } } ], "responses": { @@ -7815,7 +8128,9 @@ }, "/v1/instances/{instance}/serial-console": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "operationId": "instance_serial_console_v1", "parameters": [ { @@ -7824,8 +8139,7 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", @@ -7836,8 +8150,7 @@ "type": "integer", "format": "uint64", "minimum": 0 - }, - "style": "form" + } }, { "in": "query", @@ -7848,8 +8161,7 @@ "type": "integer", "format": "uint64", "minimum": 0 - }, - "style": "form" + } }, { "in": "query", @@ -7860,24 +8172,21 @@ "type": "integer", "format": "uint64", "minimum": 0 - }, - "style": "form" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", "name": "project", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -7902,7 +8211,9 @@ }, "/v1/instances/{instance}/serial-console/stream": { "get": { - "tags": ["instances"], + "tags": [ + "instances" + ], "operationId": "instance_serial_console_stream_v1", "parameters": [ { @@ -7911,24 +8222,21 @@ "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "simple" + } }, { "in": "query", "name": "organization", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } }, { "in": "query", "name": "project", "schema": { "$ref": "#/components/schemas/NameOrId" - }, - "style": "form" + } } ], "responses": { @@ -7946,7 +8254,9 @@ }, "/v1/instances/{instance}/start": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "summary": "Boot an instance", "operationId": "instance_start_v1", "parameters": [ @@ -7995,7 +8305,9 @@ }, "/v1/instances/{instance}/stop": { "post": { - "tags": ["instances"], + "tags": [ + "instances" + ], "operationId": "instance_stop_v1", "parameters": [ { @@ -8043,7 +8355,9 @@ }, "/v1/organizations": { "get": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "List organizations", "operationId": "organization_list_v1", "parameters": [ @@ -8096,7 +8410,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "summary": "Create an organization", "operationId": "organization_create_v1", "requestBody": { @@ -8131,7 +8447,9 @@ }, "/v1/organizations/{organization}": { "get": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "operationId": "organization_view_v1", "parameters": [ { @@ -8163,7 +8481,9 @@ } }, "put": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "operationId": "organization_update_v1", "parameters": [ { @@ -8205,7 +8525,9 @@ } }, "delete": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "operationId": "organization_delete_v1", "parameters": [ { @@ -8232,7 +8554,9 @@ }, "/v1/organizations/{organization}/policy": { "get": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "operationId": "organization_policy_view_v1", "parameters": [ { @@ -8264,7 +8588,9 @@ } }, "put": { - "tags": ["organizations"], + "tags": [ + "organizations" + ], "operationId": "organization_policy_update_v1", "parameters": [ { @@ -8308,7 +8634,9 @@ }, "/v1/projects": { "get": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "List projects", "operationId": "project_list_v1", "parameters": [ @@ -8369,7 +8697,9 @@ "x-dropshot-pagination": true }, "post": { - "tags": ["projects"], + "tags": [ + "projects" + ], "operationId": "project_create_v1", "parameters": [ { @@ -8413,7 +8743,9 @@ }, "/v1/projects/{project}": { "get": { - "tags": ["projects"], + "tags": [ + "projects" + ], "operationId": "project_view_v1", "parameters": [ { @@ -8452,7 +8784,9 @@ } }, "put": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Update a project", "operationId": "project_update_v1", "parameters": [ @@ -8502,7 +8836,9 @@ } }, "delete": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Delete a project", "operationId": "project_delete_v1", "parameters": [ @@ -8537,7 +8873,9 @@ }, "/v1/projects/{project}/policy": { "get": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Fetch a project's IAM policy", "operationId": "project_policy_view_v1", "parameters": [ @@ -8577,7 +8915,9 @@ } }, "put": { - "tags": ["projects"], + "tags": [ + "projects" + ], "summary": "Update a project's IAM policy", "operationId": "project_policy_update_v1", "parameters": [ @@ -8655,10 +8995,15 @@ }, "type": { "type": "string", - "enum": ["range_to"] + "enum": [ + "range_to" + ] } }, - "required": ["end", "type"] + "required": [ + "end", + "type" + ] }, { "description": "A range bounded inclusively below and exclusively above, `start..end`.", @@ -8674,10 +9019,16 @@ }, "type": { "type": "string", - "enum": ["range"] + "enum": [ + "range" + ] } }, - "required": ["end", "start", "type"] + "required": [ + "end", + "start", + "type" + ] }, { "description": "A range bounded inclusively below and unbounded above, `start..`.", @@ -8689,10 +9040,15 @@ }, "type": { "type": "string", - "enum": ["range_from"] + "enum": [ + "range_from" + ] } }, - "required": ["start", "type"] + "required": [ + "start", + "type" + ] } ] }, @@ -8709,10 +9065,15 @@ }, "type": { "type": "string", - "enum": ["range_to"] + "enum": [ + "range_to" + ] } }, - "required": ["end", "type"] + "required": [ + "end", + "type" + ] }, { "description": "A range bounded inclusively below and exclusively above, `start..end`.", @@ -8728,10 +9089,16 @@ }, "type": { "type": "string", - "enum": ["range"] + "enum": [ + "range" + ] } }, - "required": ["end", "start", "type"] + "required": [ + "end", + "start", + "type" + ] }, { "description": "A range bounded inclusively below and unbounded above, `start..`.", @@ -8743,10 +9110,15 @@ }, "type": { "type": "string", - "enum": ["range_from"] + "enum": [ + "range_from" + ] } }, - "required": ["start", "type"] + "required": [ + "start", + "type" + ] } ] }, @@ -8769,7 +9141,10 @@ ] } }, - "required": ["count", "range"] + "required": [ + "count", + "range" + ] }, "Binint64": { "description": "Type storing bin edges and a count of samples within it.", @@ -8790,12 +9165,19 @@ ] } }, - "required": ["count", "range"] + "required": [ + "count", + "range" + ] }, "BlockSize": { "title": "disk block size in bytes", "type": "integer", - "enum": [512, 2048, 4096] + "enum": [ + 512, + 2048, + 4096 + ] }, "ByteCount": { "description": "A count of bytes, typically used either for memory or storage capacity\n\nThe maximum supported byte count is [`i64::MAX`]. This makes it somewhat inconvenient to define constructors: a u32 constructor can be infallible, but an i64 constructor can fail (if the value is negative) and a u64 constructor can fail (if the value is larger than i64::MAX). We provide all of these for consumers' convenience.", @@ -8816,7 +9198,10 @@ "format": "double" } }, - "required": ["start_time", "value"] + "required": [ + "start_time", + "value" + ] }, "Cumulativeint64": { "description": "A cumulative or counter data type.", @@ -8831,7 +9216,10 @@ "format": "int64" } }, - "required": ["start_time", "value"] + "required": [ + "start_time", + "value" + ] }, "Datum": { "description": "A `Datum` is a single sampled data point from a metric.", @@ -8844,10 +9232,15 @@ }, "type": { "type": "string", - "enum": ["bool"] + "enum": [ + "bool" + ] } }, - "required": ["datum", "type"] + "required": [ + "datum", + "type" + ] }, { "type": "object", @@ -8858,10 +9251,15 @@ }, "type": { "type": "string", - "enum": ["i64"] + "enum": [ + "i64" + ] } }, - "required": ["datum", "type"] + "required": [ + "datum", + "type" + ] }, { "type": "object", @@ -8872,10 +9270,15 @@ }, "type": { "type": "string", - "enum": ["f64"] + "enum": [ + "f64" + ] } }, - "required": ["datum", "type"] + "required": [ + "datum", + "type" + ] }, { "type": "object", @@ -8885,10 +9288,15 @@ }, "type": { "type": "string", - "enum": ["string"] + "enum": [ + "string" + ] } }, - "required": ["datum", "type"] + "required": [ + "datum", + "type" + ] }, { "type": "object", @@ -8903,10 +9311,15 @@ }, "type": { "type": "string", - "enum": ["bytes"] + "enum": [ + "bytes" + ] } }, - "required": ["datum", "type"] + "required": [ + "datum", + "type" + ] }, { "type": "object", @@ -8916,10 +9329,15 @@ }, "type": { "type": "string", - "enum": ["cumulative_i64"] + "enum": [ + "cumulative_i64" + ] } }, - "required": ["datum", "type"] + "required": [ + "datum", + "type" + ] }, { "type": "object", @@ -8929,10 +9347,15 @@ }, "type": { "type": "string", - "enum": ["cumulative_f64"] + "enum": [ + "cumulative_f64" + ] } }, - "required": ["datum", "type"] + "required": [ + "datum", + "type" + ] }, { "type": "object", @@ -8942,10 +9365,15 @@ }, "type": { "type": "string", - "enum": ["histogram_i64"] + "enum": [ + "histogram_i64" + ] } }, - "required": ["datum", "type"] + "required": [ + "datum", + "type" + ] }, { "type": "object", @@ -8955,10 +9383,15 @@ }, "type": { "type": "string", - "enum": ["histogram_f64"] + "enum": [ + "histogram_f64" + ] } }, - "required": ["datum", "type"] + "required": [ + "datum", + "type" + ] } ] }, @@ -8989,7 +9422,10 @@ "type": "string" } }, - "required": ["private_key", "public_cert"] + "required": [ + "private_key", + "public_cert" + ] }, "DeviceAccessTokenRequest": { "type": "object", @@ -9005,7 +9441,11 @@ "type": "string" } }, - "required": ["client_id", "device_code", "grant_type"] + "required": [ + "client_id", + "device_code", + "grant_type" + ] }, "DeviceAuthRequest": { "type": "object", @@ -9015,7 +9455,9 @@ "format": "uuid" } }, - "required": ["client_id"] + "required": [ + "client_id" + ] }, "DeviceAuthVerify": { "type": "object", @@ -9024,7 +9466,9 @@ "type": "string" } }, - "required": ["user_code"] + "required": [ + "user_code" + ] }, "Digest": { "oneOf": [ @@ -9033,13 +9477,18 @@ "properties": { "type": { "type": "string", - "enum": ["sha256"] + "enum": [ + "sha256" + ] }, "value": { "type": "string" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] } ] }, @@ -9141,7 +9590,12 @@ ] } }, - "required": ["description", "disk_source", "name", "size"] + "required": [ + "description", + "disk_source", + "name", + "size" + ] }, "DiskIdentifier": { "description": "Parameters for the [`Disk`](omicron_common::api::external::Disk) to be attached or detached to an instance", @@ -9151,7 +9605,9 @@ "$ref": "#/components/schemas/Name" } }, - "required": ["name"] + "required": [ + "name" + ] }, "DiskResultsPage": { "description": "A single page of results", @@ -9170,7 +9626,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "DiskSource": { "description": "Different sources for a disk", @@ -9189,10 +9647,15 @@ }, "type": { "type": "string", - "enum": ["blank"] + "enum": [ + "blank" + ] } }, - "required": ["block_size", "type"] + "required": [ + "block_size", + "type" + ] }, { "description": "Create a disk from a disk snapshot", @@ -9204,10 +9667,15 @@ }, "type": { "type": "string", - "enum": ["snapshot"] + "enum": [ + "snapshot" + ] } }, - "required": ["snapshot_id", "type"] + "required": [ + "snapshot_id", + "type" + ] }, { "description": "Create a disk from a project image", @@ -9219,10 +9687,15 @@ }, "type": { "type": "string", - "enum": ["image"] + "enum": [ + "image" + ] } }, - "required": ["image_id", "type"] + "required": [ + "image_id", + "type" + ] }, { "description": "Create a disk from a global image", @@ -9234,10 +9707,15 @@ }, "type": { "type": "string", - "enum": ["global_image"] + "enum": [ + "global_image" + ] } }, - "required": ["image_id", "type"] + "required": [ + "image_id", + "type" + ] } ] }, @@ -9250,10 +9728,14 @@ "properties": { "state": { "type": "string", - "enum": ["creating"] + "enum": [ + "creating" + ] } }, - "required": ["state"] + "required": [ + "state" + ] }, { "description": "Disk is ready but detached from any Instance", @@ -9261,10 +9743,14 @@ "properties": { "state": { "type": "string", - "enum": ["detached"] + "enum": [ + "detached" + ] } }, - "required": ["state"] + "required": [ + "state" + ] }, { "description": "Disk is being attached to the given Instance", @@ -9276,10 +9762,15 @@ }, "state": { "type": "string", - "enum": ["attaching"] + "enum": [ + "attaching" + ] } }, - "required": ["instance", "state"] + "required": [ + "instance", + "state" + ] }, { "description": "Disk is attached to the given Instance", @@ -9291,10 +9782,15 @@ }, "state": { "type": "string", - "enum": ["attached"] + "enum": [ + "attached" + ] } }, - "required": ["instance", "state"] + "required": [ + "instance", + "state" + ] }, { "description": "Disk is being detached from the given Instance", @@ -9306,10 +9802,15 @@ }, "state": { "type": "string", - "enum": ["detaching"] + "enum": [ + "detaching" + ] } }, - "required": ["instance", "state"] + "required": [ + "instance", + "state" + ] }, { "description": "Disk has been destroyed", @@ -9317,10 +9818,14 @@ "properties": { "state": { "type": "string", - "enum": ["destroyed"] + "enum": [ + "destroyed" + ] } }, - "required": ["state"] + "required": [ + "state" + ] }, { "description": "Disk is unavailable", @@ -9328,10 +9833,14 @@ "properties": { "state": { "type": "string", - "enum": ["faulted"] + "enum": [ + "faulted" + ] } }, - "required": ["state"] + "required": [ + "state" + ] } ] }, @@ -9352,7 +9861,10 @@ "type": "string" } }, - "required": ["name", "version"] + "required": [ + "name", + "version" + ] }, "Error": { "description": "Error information from a response.", @@ -9368,7 +9880,10 @@ "type": "string" } }, - "required": ["message", "request_id"] + "required": [ + "message", + "request_id" + ] }, "ExternalIp": { "type": "object", @@ -9381,7 +9896,10 @@ "$ref": "#/components/schemas/IpKind" } }, - "required": ["ip", "kind"] + "required": [ + "ip", + "kind" + ] }, "ExternalIpCreate": { "description": "Parameters for creating an external IP address for instances.", @@ -9400,10 +9918,14 @@ }, "type": { "type": "string", - "enum": ["ephemeral"] + "enum": [ + "ephemeral" + ] } }, - "required": ["type"] + "required": [ + "type" + ] } ] }, @@ -9424,7 +9946,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "FieldSchema": { "description": "The name and type information for a field of a timeseries schema.", @@ -9440,21 +9964,38 @@ "$ref": "#/components/schemas/FieldType" } }, - "required": ["name", "source", "ty"] + "required": [ + "name", + "source", + "ty" + ] }, "FieldSource": { "description": "The source from which a field is derived, the target or metric.", "type": "string", - "enum": ["target", "metric"] + "enum": [ + "target", + "metric" + ] }, "FieldType": { "description": "The `FieldType` identifies the data type of a target or metric field.", "type": "string", - "enum": ["string", "i64", "ip_addr", "uuid", "bool"] + "enum": [ + "string", + "i64", + "ip_addr", + "uuid", + "bool" + ] }, "FleetRole": { "type": "string", - "enum": ["admin", "collaborator", "viewer"] + "enum": [ + "admin", + "collaborator", + "viewer" + ] }, "FleetRolePolicy": { "description": "Client view of a [`Policy`], which describes how this resource may be accessed\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", @@ -9468,7 +10009,9 @@ } } }, - "required": ["role_assignments"] + "required": [ + "role_assignments" + ] }, "FleetRoleRoleAssignment": { "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, [`RoleAssignment`]s are put into a [`Policy`] and that Policy is applied to a particular resource.", @@ -9485,7 +10028,11 @@ "$ref": "#/components/schemas/FleetRole" } }, - "required": ["identity_id", "identity_type", "role_name"] + "required": [ + "identity_id", + "identity_type", + "role_name" + ] }, "GlobalImage": { "description": "Client view of global Images", @@ -9629,7 +10176,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "Group": { "description": "Client view of a [`Group`]", @@ -9649,7 +10198,11 @@ "format": "uuid" } }, - "required": ["display_name", "id", "silo_id"] + "required": [ + "display_name", + "id", + "silo_id" + ] }, "GroupResultsPage": { "description": "A single page of results", @@ -9668,7 +10221,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "Histogramdouble": { "description": "A simple type for managing a histogram metric.\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.\n\nExample ------- ```rust use oximeter::histogram::{BinRange, Histogram};\n\nlet edges = [0i64, 10, 20]; let mut hist = Histogram::new(&edges).unwrap(); assert_eq!(hist.n_bins(), 4); // One additional bin for the range (20..) assert_eq!(hist.n_samples(), 0); hist.sample(4); hist.sample(100); assert_eq!(hist.n_samples(), 2);\n\nlet data = hist.iter().collect::>(); assert_eq!(data[0].range, BinRange::range(i64::MIN, 0)); // An additional bin for `..0` assert_eq!(data[0].count, 0); // Nothing is in this bin\n\nassert_eq!(data[1].range, BinRange::range(0, 10)); // The range `0..10` assert_eq!(data[1].count, 1); // 4 is sampled into this bin ```\n\nNotes -----\n\nHistograms may be constructed either from their left bin edges, or from a sequence of ranges. In either case, the left-most bin may be converted upon construction. In particular, if the left-most value is not equal to the minimum of the support, a new bin will be added from the minimum to that provided value. If the left-most value _is_ the support's minimum, because the provided bin was unbounded below, such as `(..0)`, then that bin will be converted into one bounded below, `(MIN..0)` in this case.\n\nThe short of this is that, most of the time, it shouldn't matter. If one specifies the extremes of the support as their bins, be aware that the left-most may be converted from a `BinRange::RangeTo` into a `BinRange::Range`. In other words, the first bin of a histogram is _always_ a `Bin::Range` or a `Bin::RangeFrom` after construction. In fact, every bin is one of those variants, the `BinRange::RangeTo` is only provided as a convenience during construction.", @@ -9690,7 +10245,11 @@ "format": "date-time" } }, - "required": ["bins", "n_samples", "start_time"] + "required": [ + "bins", + "n_samples", + "start_time" + ] }, "Histogramint64": { "description": "A simple type for managing a histogram metric.\n\nA histogram maintains the count of any number of samples, over a set of bins. Bins are specified on construction via their _left_ edges, inclusive. There can't be any \"gaps\" in the bins, and an additional bin may be added to the left, right, or both so that the bins extend to the entire range of the support.\n\nNote that any gaps, unsorted bins, or non-finite values will result in an error.\n\nExample ------- ```rust use oximeter::histogram::{BinRange, Histogram};\n\nlet edges = [0i64, 10, 20]; let mut hist = Histogram::new(&edges).unwrap(); assert_eq!(hist.n_bins(), 4); // One additional bin for the range (20..) assert_eq!(hist.n_samples(), 0); hist.sample(4); hist.sample(100); assert_eq!(hist.n_samples(), 2);\n\nlet data = hist.iter().collect::>(); assert_eq!(data[0].range, BinRange::range(i64::MIN, 0)); // An additional bin for `..0` assert_eq!(data[0].count, 0); // Nothing is in this bin\n\nassert_eq!(data[1].range, BinRange::range(0, 10)); // The range `0..10` assert_eq!(data[1].count, 1); // 4 is sampled into this bin ```\n\nNotes -----\n\nHistograms may be constructed either from their left bin edges, or from a sequence of ranges. In either case, the left-most bin may be converted upon construction. In particular, if the left-most value is not equal to the minimum of the support, a new bin will be added from the minimum to that provided value. If the left-most value _is_ the support's minimum, because the provided bin was unbounded below, such as `(..0)`, then that bin will be converted into one bounded below, `(MIN..0)` in this case.\n\nThe short of this is that, most of the time, it shouldn't matter. If one specifies the extremes of the support as their bins, be aware that the left-most may be converted from a `BinRange::RangeTo` into a `BinRange::Range`. In other words, the first bin of a histogram is _always_ a `Bin::Range` or a `Bin::RangeFrom` after construction. In fact, every bin is one of those variants, the `BinRange::RangeTo` is only provided as a convenience during construction.", @@ -9712,7 +10271,11 @@ "format": "date-time" } }, - "required": ["bins", "n_samples", "start_time"] + "required": [ + "bins", + "n_samples", + "start_time" + ] }, "IdentityProvider": { "description": "Client view of an [`IdentityProvider`]", @@ -9780,21 +10343,28 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "IdentityProviderType": { "oneOf": [ { "description": "SAML identity provider", "type": "string", - "enum": ["saml"] + "enum": [ + "saml" + ] } ] }, "IdentityType": { "description": "Describes what kind of identity is described by an id", "type": "string", - "enum": ["silo_user", "silo_group"] + "enum": [ + "silo_user", + "silo_group" + ] }, "IdpMetadataSource": { "oneOf": [ @@ -9803,13 +10373,18 @@ "properties": { "type": { "type": "string", - "enum": ["url"] + "enum": [ + "url" + ] }, "url": { "type": "string" } }, - "required": ["type", "url"] + "required": [ + "type", + "url" + ] }, { "type": "object", @@ -9819,10 +10394,15 @@ }, "type": { "type": "string", - "enum": ["base64_encoded_xml"] + "enum": [ + "base64_encoded_xml" + ] } }, - "required": ["data", "type"] + "required": [ + "data", + "type" + ] } ] }, @@ -9936,7 +10516,12 @@ ] } }, - "required": ["block_size", "description", "name", "source"] + "required": [ + "block_size", + "description", + "name", + "source" + ] }, "ImageResultsPage": { "description": "A single page of results", @@ -9955,7 +10540,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "ImageSource": { "description": "The source of the underlying image.", @@ -9965,13 +10552,18 @@ "properties": { "type": { "type": "string", - "enum": ["url"] + "enum": [ + "url" + ] }, "url": { "type": "string" } }, - "required": ["type", "url"] + "required": [ + "type", + "url" + ] }, { "type": "object", @@ -9982,10 +10574,15 @@ }, "type": { "type": "string", - "enum": ["snapshot"] + "enum": [ + "snapshot" + ] } }, - "required": ["id", "type"] + "required": [ + "id", + "type" + ] }, { "description": "Boot the Alpine ISO that ships with the Propolis zone. Intended for development purposes only.", @@ -9993,10 +10590,14 @@ "properties": { "type": { "type": "string", - "enum": ["you_can_boot_anything_as_long_as_its_alpine"] + "enum": [ + "you_can_boot_anything_as_long_as_its_alpine" + ] } }, - "required": ["type"] + "required": [ + "type" + ] } ] }, @@ -10142,7 +10743,13 @@ "format": "byte" } }, - "required": ["description", "hostname", "memory", "name", "ncpus"] + "required": [ + "description", + "hostname", + "memory", + "name", + "ncpus" + ] }, "InstanceDiskAttachment": { "description": "Describe the instance's disks at creation time", @@ -10175,10 +10782,18 @@ }, "type": { "type": "string", - "enum": ["create"] + "enum": [ + "create" + ] } }, - "required": ["description", "disk_source", "name", "size", "type"] + "required": [ + "description", + "disk_source", + "name", + "size", + "type" + ] }, { "description": "During instance creation, attach this disk", @@ -10194,10 +10809,15 @@ }, "type": { "type": "string", - "enum": ["attach"] + "enum": [ + "attach" + ] } }, - "required": ["name", "type"] + "required": [ + "name", + "type" + ] } ] }, @@ -10208,7 +10828,9 @@ "$ref": "#/components/schemas/NameOrId" } }, - "required": ["instance"] + "required": [ + "instance" + ] }, "InstanceMigrate": { "description": "Migration parameters for an [`Instance`](omicron_common::api::external::Instance)", @@ -10219,7 +10841,9 @@ "format": "uuid" } }, - "required": ["dst_sled_id"] + "required": [ + "dst_sled_id" + ] }, "InstanceNetworkInterfaceAttachment": { "description": "Describes an attachment of a `NetworkInterface` to an `Instance`, at the time the instance is created.", @@ -10236,10 +10860,15 @@ }, "type": { "type": "string", - "enum": ["create"] + "enum": [ + "create" + ] } }, - "required": ["params", "type"] + "required": [ + "params", + "type" + ] }, { "description": "The default networking configuration for an instance is to create a single primary interface with an automatically-assigned IP address. The IP will be pulled from the Project's default VPC / VPC Subnet.", @@ -10247,10 +10876,14 @@ "properties": { "type": { "type": "string", - "enum": ["default"] + "enum": [ + "default" + ] } }, - "required": ["type"] + "required": [ + "type" + ] }, { "description": "No network interfaces at all will be created for the instance.", @@ -10258,10 +10891,14 @@ "properties": { "type": { "type": "string", - "enum": ["none"] + "enum": [ + "none" + ] } }, - "required": ["type"] + "required": [ + "type" + ] } ] }, @@ -10282,7 +10919,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "InstanceSerialConsoleData": { "description": "Contents of an Instance's serial console buffer.", @@ -10304,7 +10943,10 @@ "minimum": 0 } }, - "required": ["data", "last_byte_offset"] + "required": [ + "data", + "last_byte_offset" + ] }, "InstanceState": { "description": "Running state of an Instance (primarily: booted or stopped)\n\nThis typically reflects whether it's starting, running, stopping, or stopped, but also includes states related to the Instance's lifecycle", @@ -10312,59 +10954,82 @@ { "description": "The instance is being created.", "type": "string", - "enum": ["creating"] + "enum": [ + "creating" + ] }, { "description": "The instance is currently starting up.", "type": "string", - "enum": ["starting"] + "enum": [ + "starting" + ] }, { "description": "The instance is currently running.", "type": "string", - "enum": ["running"] + "enum": [ + "running" + ] }, { "description": "The instance has been requested to stop and a transition to \"Stopped\" is imminent.", "type": "string", - "enum": ["stopping"] + "enum": [ + "stopping" + ] }, { "description": "The instance is currently stopped.", "type": "string", - "enum": ["stopped"] + "enum": [ + "stopped" + ] }, { "description": "The instance is in the process of rebooting - it will remain in the \"rebooting\" state until the VM is starting once more.", "type": "string", - "enum": ["rebooting"] + "enum": [ + "rebooting" + ] }, { "description": "The instance is in the process of migrating - it will remain in the \"migrating\" state until the migration process is complete and the destination propolis is ready to continue execution.", "type": "string", - "enum": ["migrating"] + "enum": [ + "migrating" + ] }, { "description": "The instance is attempting to recover from a failure.", "type": "string", - "enum": ["repairing"] + "enum": [ + "repairing" + ] }, { "description": "The instance has encountered a failure.", "type": "string", - "enum": ["failed"] + "enum": [ + "failed" + ] }, { "description": "The instance has been deleted.", "type": "string", - "enum": ["destroyed"] + "enum": [ + "destroyed" + ] } ] }, "IpKind": { "description": "The kind of an external IP address for an instance", "type": "string", - "enum": ["ephemeral", "floating"] + "enum": [ + "ephemeral", + "floating" + ] }, "IpNet": { "oneOf": [ @@ -10448,7 +11113,10 @@ "$ref": "#/components/schemas/Name" } }, - "required": ["description", "name"] + "required": [ + "description", + "name" + ] }, "IpPoolRange": { "type": "object", @@ -10465,7 +11133,11 @@ "format": "date-time" } }, - "required": ["id", "range", "time_created"] + "required": [ + "id", + "range", + "time_created" + ] }, "IpPoolRangeResultsPage": { "description": "A single page of results", @@ -10484,7 +11156,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "IpPoolResultsPage": { "description": "A single page of results", @@ -10503,7 +11177,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "IpPoolUpdate": { "description": "Parameters for updating an IP Pool", @@ -10563,7 +11239,10 @@ "format": "ipv4" } }, - "required": ["first", "last"] + "required": [ + "first", + "last" + ] }, "Ipv6Net": { "example": "fd12:3456::/64", @@ -10585,7 +11264,10 @@ "format": "ipv6" } }, - "required": ["first", "last"] + "required": [ + "first", + "last" + ] }, "L4PortRange": { "example": "22", @@ -10617,7 +11299,10 @@ "format": "date-time" } }, - "required": ["datum", "timestamp"] + "required": [ + "datum", + "timestamp" + ] }, "MeasurementResultsPage": { "description": "A single page of results", @@ -10636,7 +11321,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "Name": { "title": "A name unique within the parent collection", @@ -10777,7 +11464,12 @@ ] } }, - "required": ["description", "name", "subnet_name", "vpc_name"] + "required": [ + "description", + "name", + "subnet_name", + "vpc_name" + ] }, "NetworkInterfaceResultsPage": { "description": "A single page of results", @@ -10796,7 +11488,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "NetworkInterfaceUpdate": { "description": "Parameters for updating a [`NetworkInterface`](omicron_common::api::external::NetworkInterface).\n\nNote that modifying IP addresses for an interface is not yet supported, a new interface must be created instead.", @@ -10876,7 +11570,10 @@ "$ref": "#/components/schemas/Name" } }, - "required": ["description", "name"] + "required": [ + "description", + "name" + ] }, "OrganizationResultsPage": { "description": "A single page of results", @@ -10895,11 +11592,17 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "OrganizationRole": { "type": "string", - "enum": ["admin", "collaborator", "viewer"] + "enum": [ + "admin", + "collaborator", + "viewer" + ] }, "OrganizationRolePolicy": { "description": "Client view of a [`Policy`], which describes how this resource may be accessed\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", @@ -10913,7 +11616,9 @@ } } }, - "required": ["role_assignments"] + "required": [ + "role_assignments" + ] }, "OrganizationRoleRoleAssignment": { "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, [`RoleAssignment`]s are put into a [`Policy`] and that Policy is applied to a particular resource.", @@ -10930,7 +11635,11 @@ "$ref": "#/components/schemas/OrganizationRole" } }, - "required": ["identity_id", "identity_type", "role_name"] + "required": [ + "identity_id", + "identity_type", + "role_name" + ] }, "OrganizationUpdate": { "description": "Updateable properties of an [`Organization`](crate::external_api::views::Organization)", @@ -11012,7 +11721,10 @@ "$ref": "#/components/schemas/Name" } }, - "required": ["description", "name"] + "required": [ + "description", + "name" + ] }, "ProjectResultsPage": { "description": "A single page of results", @@ -11031,11 +11743,17 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "ProjectRole": { "type": "string", - "enum": ["admin", "collaborator", "viewer"] + "enum": [ + "admin", + "collaborator", + "viewer" + ] }, "ProjectRolePolicy": { "description": "Client view of a [`Policy`], which describes how this resource may be accessed\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", @@ -11049,7 +11767,9 @@ } } }, - "required": ["role_assignments"] + "required": [ + "role_assignments" + ] }, "ProjectRoleRoleAssignment": { "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, [`RoleAssignment`]s are put into a [`Policy`] and that Policy is applied to a particular resource.", @@ -11066,7 +11786,11 @@ "$ref": "#/components/schemas/ProjectRole" } }, - "required": ["identity_id", "identity_type", "role_name"] + "required": [ + "identity_id", + "identity_type", + "role_name" + ] }, "ProjectUpdate": { "description": "Updateable properties of a [`Project`](crate::external_api::views::Project)", @@ -11106,7 +11830,11 @@ "format": "date-time" } }, - "required": ["id", "time_created", "time_modified"] + "required": [ + "id", + "time_created", + "time_modified" + ] }, "RackResultsPage": { "description": "A single page of results", @@ -11125,7 +11853,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "Role": { "description": "Client view of a [`Role`]", @@ -11138,7 +11868,10 @@ "$ref": "#/components/schemas/RoleName" } }, - "required": ["description", "name"] + "required": [ + "description", + "name" + ] }, "RoleName": { "title": "A name for a built-in role", @@ -11164,7 +11897,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "RouteDestination": { "description": "A `RouteDestination` is used to match traffic with a routing rule, on the destination of that traffic.\n\nWhen traffic is to be sent to a destination that is within a given `RouteDestination`, the corresponding [`RouterRoute`] applies, and traffic will be forward to the [`RouteTarget`] for that rule.", @@ -11175,14 +11910,19 @@ "properties": { "type": { "type": "string", - "enum": ["ip"] + "enum": [ + "ip" + ] }, "value": { "type": "string", "format": "ip" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "Route applies to traffic destined for a specific IP subnet", @@ -11190,13 +11930,18 @@ "properties": { "type": { "type": "string", - "enum": ["ip_net"] + "enum": [ + "ip_net" + ] }, "value": { "$ref": "#/components/schemas/IpNet" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "Route applies to traffic destined for the given VPC.", @@ -11204,13 +11949,18 @@ "properties": { "type": { "type": "string", - "enum": ["vpc"] + "enum": [ + "vpc" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "Route applies to traffic", @@ -11218,13 +11968,18 @@ "properties": { "type": { "type": "string", - "enum": ["subnet"] + "enum": [ + "subnet" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] } ] }, @@ -11237,14 +11992,19 @@ "properties": { "type": { "type": "string", - "enum": ["ip"] + "enum": [ + "ip" + ] }, "value": { "type": "string", "format": "ip" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "Forward traffic to a VPC", @@ -11252,13 +12012,18 @@ "properties": { "type": { "type": "string", - "enum": ["vpc"] + "enum": [ + "vpc" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "Forward traffic to a VPC Subnet", @@ -11266,13 +12031,18 @@ "properties": { "type": { "type": "string", - "enum": ["subnet"] + "enum": [ + "subnet" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "Forward traffic to a specific instance", @@ -11280,13 +12050,18 @@ "properties": { "type": { "type": "string", - "enum": ["instance"] + "enum": [ + "instance" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "Forward traffic to an internet gateway", @@ -11294,13 +12069,18 @@ "properties": { "type": { "type": "string", - "enum": ["internet_gateway"] + "enum": [ + "internet_gateway" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] } ] }, @@ -11384,7 +12164,12 @@ "$ref": "#/components/schemas/RouteTarget" } }, - "required": ["description", "destination", "name", "target"] + "required": [ + "description", + "destination", + "name", + "target" + ] }, "RouterRouteKind": { "description": "The classification of a [`RouterRoute`] as defined by the system. The kind determines certain attributes such as if the route is modifiable and describes how or where the route was created.\n\nSee [RFD-21](https://rfd.shared.oxide.computer/rfd/0021#concept-router) for more context", @@ -11392,22 +12177,30 @@ { "description": "Determines the default destination of traffic, such as whether it goes to the internet or not.\n\n`Destination: An Internet Gateway` `Modifiable: true`", "type": "string", - "enum": ["default"] + "enum": [ + "default" + ] }, { "description": "Automatically added for each VPC Subnet in the VPC\n\n`Destination: A VPC Subnet` `Modifiable: false`", "type": "string", - "enum": ["vpc_subnet"] + "enum": [ + "vpc_subnet" + ] }, { "description": "Automatically added when VPC peering is established\n\n`Destination: A different VPC` `Modifiable: false`", "type": "string", - "enum": ["vpc_peering"] + "enum": [ + "vpc_peering" + ] }, { "description": "Created by a user See [`RouteTarget`]\n\n`Destination: User defined` `Modifiable: true`", "type": "string", - "enum": ["custom"] + "enum": [ + "custom" + ] } ] }, @@ -11428,7 +12221,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "RouterRouteUpdateParams": { "description": "Updateable properties of a [`RouterRoute`]", @@ -11453,7 +12248,10 @@ "$ref": "#/components/schemas/RouteTarget" } }, - "required": ["destination", "target"] + "required": [ + "destination", + "target" + ] }, "Saga": { "type": "object", @@ -11466,7 +12264,10 @@ "$ref": "#/components/schemas/SagaState" } }, - "required": ["id", "state"] + "required": [ + "id", + "state" + ] }, "SagaErrorInfo": { "oneOf": [ @@ -11475,60 +12276,84 @@ "properties": { "error": { "type": "string", - "enum": ["action_failed"] + "enum": [ + "action_failed" + ] }, "source_error": {} }, - "required": ["error", "source_error"] + "required": [ + "error", + "source_error" + ] }, { "type": "object", "properties": { "error": { "type": "string", - "enum": ["deserialize_failed"] + "enum": [ + "deserialize_failed" + ] }, "message": { "type": "string" } }, - "required": ["error", "message"] + "required": [ + "error", + "message" + ] }, { "type": "object", "properties": { "error": { "type": "string", - "enum": ["injected_error"] + "enum": [ + "injected_error" + ] } }, - "required": ["error"] + "required": [ + "error" + ] }, { "type": "object", "properties": { "error": { "type": "string", - "enum": ["serialize_failed"] + "enum": [ + "serialize_failed" + ] }, "message": { "type": "string" } }, - "required": ["error", "message"] + "required": [ + "error", + "message" + ] }, { "type": "object", "properties": { "error": { "type": "string", - "enum": ["subsaga_create_failed"] + "enum": [ + "subsaga_create_failed" + ] }, "message": { "type": "string" } }, - "required": ["error", "message"] + "required": [ + "error", + "message" + ] } ] }, @@ -11549,7 +12374,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "SagaState": { "oneOf": [ @@ -11558,20 +12385,28 @@ "properties": { "state": { "type": "string", - "enum": ["running"] + "enum": [ + "running" + ] } }, - "required": ["state"] + "required": [ + "state" + ] }, { "type": "object", "properties": { "state": { "type": "string", - "enum": ["succeeded"] + "enum": [ + "succeeded" + ] } }, - "required": ["state"] + "required": [ + "state" + ] }, { "type": "object", @@ -11584,10 +12419,16 @@ }, "state": { "type": "string", - "enum": ["failed"] + "enum": [ + "failed" + ] } }, - "required": ["error_info", "error_node_name", "state"] + "required": [ + "error_info", + "error_node_name", + "state" + ] } ] }, @@ -11801,7 +12642,12 @@ "$ref": "#/components/schemas/Name" } }, - "required": ["description", "discoverable", "identity_mode", "name"] + "required": [ + "description", + "discoverable", + "identity_mode", + "name" + ] }, "SiloIdentityMode": { "description": "Describes how identities are managed and users are authenticated in this Silo", @@ -11809,12 +12655,16 @@ { "description": "Users are authenticated with SAML using an external authentication provider. The system updates information about users and groups only during successful authentication (i.e,. \"JIT provisioning\" of users and groups).", "type": "string", - "enum": ["saml_jit"] + "enum": [ + "saml_jit" + ] }, { "description": "The system is the source of truth about users. There is no linkage to an external authentication provider or identity provider.", "type": "string", - "enum": ["local_only"] + "enum": [ + "local_only" + ] } ] }, @@ -11835,11 +12685,17 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "SiloRole": { "type": "string", - "enum": ["admin", "collaborator", "viewer"] + "enum": [ + "admin", + "collaborator", + "viewer" + ] }, "SiloRolePolicy": { "description": "Client view of a [`Policy`], which describes how this resource may be accessed\n\nNote that the Policy only describes access granted explicitly for this resource. The policies of parent resources can also cause a user to have access to this resource.", @@ -11853,7 +12709,9 @@ } } }, - "required": ["role_assignments"] + "required": [ + "role_assignments" + ] }, "SiloRoleRoleAssignment": { "description": "Describes the assignment of a particular role on a particular resource to a particular identity (user, group, etc.)\n\nThe resource is not part of this structure. Rather, [`RoleAssignment`]s are put into a [`Policy`] and that Policy is applied to a particular resource.", @@ -11870,7 +12728,11 @@ "$ref": "#/components/schemas/SiloRole" } }, - "required": ["identity_id", "identity_type", "role_name"] + "required": [ + "identity_id", + "identity_type", + "role_name" + ] }, "Sled": { "description": "Client view of an [`Sled`]", @@ -11895,7 +12757,12 @@ "format": "date-time" } }, - "required": ["id", "service_address", "time_created", "time_modified"] + "required": [ + "id", + "service_address", + "time_created", + "time_modified" + ] }, "SledResultsPage": { "description": "A single page of results", @@ -11914,7 +12781,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "Snapshot": { "description": "Client view of a Snapshot", @@ -11993,7 +12862,11 @@ "$ref": "#/components/schemas/Name" } }, - "required": ["description", "disk", "name"] + "required": [ + "description", + "disk", + "name" + ] }, "SnapshotResultsPage": { "description": "A single page of results", @@ -12012,11 +12885,18 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "SnapshotState": { "type": "string", - "enum": ["creating", "ready", "faulted", "destroyed"] + "enum": [ + "creating", + "ready", + "faulted", + "destroyed" + ] }, "SpoofLoginBody": { "type": "object", @@ -12025,7 +12905,9 @@ "type": "string" } }, - "required": ["username"] + "required": [ + "username" + ] }, "SshKey": { "description": "Client view of a [`SshKey`]", @@ -12093,7 +12975,11 @@ "type": "string" } }, - "required": ["description", "name", "public_key"] + "required": [ + "description", + "name", + "public_key" + ] }, "SshKeyResultsPage": { "description": "A single page of results", @@ -12112,7 +12998,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "TimeseriesName": { "title": "The name of a timeseries", @@ -12141,7 +13029,12 @@ "$ref": "#/components/schemas/TimeseriesName" } }, - "required": ["created", "datum_type", "field_schema", "timeseries_name"] + "required": [ + "created", + "datum_type", + "field_schema", + "timeseries_name" + ] }, "TimeseriesSchemaResultsPage": { "description": "A single page of results", @@ -12160,7 +13053,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "User": { "description": "Client view of a [`User`]", @@ -12180,7 +13075,11 @@ "format": "uuid" } }, - "required": ["display_name", "id", "silo_id"] + "required": [ + "display_name", + "id", + "silo_id" + ] }, "UserBuiltin": { "description": "Client view of a [`UserBuiltin`]", @@ -12239,7 +13138,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "UserCreate": { "description": "Create-time parameters for a [`User`](crate::external_api::views::User)", @@ -12262,7 +13163,10 @@ ] } }, - "required": ["external_id", "password"] + "required": [ + "external_id", + "password" + ] }, "UserId": { "title": "A name unique within the parent collection", @@ -12283,10 +13187,15 @@ }, "user_password_value": { "type": "string", - "enum": ["password"] + "enum": [ + "password" + ] } }, - "required": ["details", "user_password_value"] + "required": [ + "details", + "user_password_value" + ] }, { "description": "Invalidates any current password (disabling password authentication)", @@ -12294,10 +13203,14 @@ "properties": { "user_password_value": { "type": "string", - "enum": ["invalid_password"] + "enum": [ + "invalid_password" + ] } }, - "required": ["user_password_value"] + "required": [ + "user_password_value" + ] } ] }, @@ -12318,7 +13231,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "UsernamePasswordCredentials": { "description": "Credentials for local user login", @@ -12331,7 +13246,10 @@ "$ref": "#/components/schemas/UserId" } }, - "required": ["password", "username"] + "required": [ + "password", + "username" + ] }, "Vpc": { "description": "Client view of a [`Vpc`]", @@ -12426,7 +13344,11 @@ "$ref": "#/components/schemas/Name" } }, - "required": ["description", "dns_name", "name"] + "required": [ + "description", + "dns_name", + "name" + ] }, "VpcFirewallRule": { "description": "A single rule in a VPC firewall", @@ -12527,11 +13449,17 @@ }, "VpcFirewallRuleAction": { "type": "string", - "enum": ["allow", "deny"] + "enum": [ + "allow", + "deny" + ] }, "VpcFirewallRuleDirection": { "type": "string", - "enum": ["inbound", "outbound"] + "enum": [ + "inbound", + "outbound" + ] }, "VpcFirewallRuleFilter": { "description": "Filter for a firewall rule. A given packet must match every field that is present for the rule to apply to it. A packet matches a field if any entry in that field matches the packet.", @@ -12572,13 +13500,18 @@ "properties": { "type": { "type": "string", - "enum": ["vpc"] + "enum": [ + "vpc" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "The rule applies to traffic from/to all instances in the VPC Subnet", @@ -12586,13 +13519,18 @@ "properties": { "type": { "type": "string", - "enum": ["subnet"] + "enum": [ + "subnet" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "The rule applies to traffic from/to this specific instance", @@ -12600,13 +13538,18 @@ "properties": { "type": { "type": "string", - "enum": ["instance"] + "enum": [ + "instance" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "The rule applies to traffic from/to a specific IP address", @@ -12614,14 +13557,19 @@ "properties": { "type": { "type": "string", - "enum": ["ip"] + "enum": [ + "ip" + ] }, "value": { "type": "string", "format": "ip" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "The rule applies to traffic from/to a specific IP subnet", @@ -12629,24 +13577,36 @@ "properties": { "type": { "type": "string", - "enum": ["ip_net"] + "enum": [ + "ip_net" + ] }, "value": { "$ref": "#/components/schemas/IpNet" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] } ] }, "VpcFirewallRuleProtocol": { "description": "The protocols that may be specified in a firewall rule's filter", "type": "string", - "enum": ["TCP", "UDP", "ICMP"] + "enum": [ + "TCP", + "UDP", + "ICMP" + ] }, "VpcFirewallRuleStatus": { "type": "string", - "enum": ["disabled", "enabled"] + "enum": [ + "disabled", + "enabled" + ] }, "VpcFirewallRuleTarget": { "description": "A `VpcFirewallRuleTarget` is used to specify the set of [`Instance`]s to which a firewall rule applies.", @@ -12657,13 +13617,18 @@ "properties": { "type": { "type": "string", - "enum": ["vpc"] + "enum": [ + "vpc" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "The rule applies to all instances in the VPC Subnet", @@ -12671,13 +13636,18 @@ "properties": { "type": { "type": "string", - "enum": ["subnet"] + "enum": [ + "subnet" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "The rule applies to this specific instance", @@ -12685,13 +13655,18 @@ "properties": { "type": { "type": "string", - "enum": ["instance"] + "enum": [ + "instance" + ] }, "value": { "$ref": "#/components/schemas/Name" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "The rule applies to a specific IP address", @@ -12699,14 +13674,19 @@ "properties": { "type": { "type": "string", - "enum": ["ip"] + "enum": [ + "ip" + ] }, "value": { "type": "string", "format": "ip" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] }, { "description": "The rule applies to a specific IP subnet", @@ -12714,13 +13694,18 @@ "properties": { "type": { "type": "string", - "enum": ["ip_net"] + "enum": [ + "ip_net" + ] }, "value": { "$ref": "#/components/schemas/IpNet" } }, - "required": ["type", "value"] + "required": [ + "type", + "value" + ] } ] }, @@ -12808,7 +13793,9 @@ } } }, - "required": ["rules"] + "required": [ + "rules" + ] }, "VpcFirewallRules": { "description": "Collection of a Vpc's firewall rules", @@ -12821,7 +13808,9 @@ } } }, - "required": ["rules"] + "required": [ + "rules" + ] }, "VpcResultsPage": { "description": "A single page of results", @@ -12840,7 +13829,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "VpcRouter": { "description": "A VPC router defines a series of rules that indicate where traffic should be sent depending on its destination.", @@ -12903,11 +13894,17 @@ "$ref": "#/components/schemas/Name" } }, - "required": ["description", "name"] + "required": [ + "description", + "name" + ] }, "VpcRouterKind": { "type": "string", - "enum": ["system", "custom"] + "enum": [ + "system", + "custom" + ] }, "VpcRouterResultsPage": { "description": "A single page of results", @@ -12926,7 +13923,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "VpcRouterUpdate": { "description": "Updateable properties of a [`VpcRouter`](crate::external_api::views::VpcRouter)", @@ -13038,7 +14037,11 @@ "$ref": "#/components/schemas/Name" } }, - "required": ["description", "ipv4_block", "name"] + "required": [ + "description", + "ipv4_block", + "name" + ] }, "VpcSubnetResultsPage": { "description": "A single page of results", @@ -13057,7 +14060,9 @@ "type": "string" } }, - "required": ["items"] + "required": [ + "items" + ] }, "VpcSubnetUpdate": { "description": "Updateable properties of a [`VpcSubnet`](crate::external_api::views::VpcSubnet)", @@ -13109,7 +14114,9 @@ { "description": "sort in increasing order of \"id\"", "type": "string", - "enum": ["id_ascending"] + "enum": [ + "id_ascending" + ] } ] }, @@ -13119,17 +14126,23 @@ { "description": "sort in increasing order of \"name\"", "type": "string", - "enum": ["name_ascending"] + "enum": [ + "name_ascending" + ] }, { "description": "sort in decreasing order of \"name\"", "type": "string", - "enum": ["name_descending"] + "enum": [ + "name_descending" + ] }, { "description": "sort in increasing order of \"id\"", "type": "string", - "enum": ["id_ascending"] + "enum": [ + "id_ascending" + ] } ] }, @@ -13139,7 +14152,9 @@ { "description": "sort in increasing order of \"name\"", "type": "string", - "enum": ["name_ascending"] + "enum": [ + "name_ascending" + ] } ] }, @@ -13326,4 +14341,4 @@ } } ] -} +} \ No newline at end of file From d5e6c42df2f5abc950441f231b6f07b332f37055 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 20 Dec 2022 13:45:00 -0500 Subject: [PATCH 62/72] Minor touch-ups after merge --- nexus/src/external_api/http_entrypoints.rs | 6 +++--- nexus/types/src/external_api/params.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 62a68eed4e2..b77f6722cf7 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -122,7 +122,7 @@ pub fn external_api() -> NexusApiDescription { api.register(project_policy_view_v1)?; api.register(project_policy_update_v1)?; - // Customer-Accessible IP Pools API + // Operator-Accessible IP Pools API api.register(ip_pool_list)?; api.register(ip_pool_create)?; api.register(ip_pool_view)?; @@ -3506,7 +3506,7 @@ async fn instance_disk_list( } /// Attach a disk to an instance -/// Use `POST /v1/disks/{disk}/attach { instance: } ` instead +/// Use `POST /v1/disks/{disk}/attach` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach", @@ -3546,7 +3546,7 @@ async fn instance_disk_attach( } /// Detach a disk from an instance -/// Use `POST /v1/disks/{disk}/detach { instance: } ` instead +/// Use `POST /v1/disks/{disk}/detach` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach", diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 1f4ff606568..ed3a6fe2bd5 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -1024,6 +1024,7 @@ pub struct DiskIdentifier { pub name: Name, } +// TODO-v1: Post merge, consolidate with paths #[derive(Clone, Deserialize, Serialize, JsonSchema)] pub struct InstanceIdentifier { pub instance: NameOrId, From 30f83fde05647229ea957d38be5400432798b0a4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 20 Dec 2022 15:42:03 -0500 Subject: [PATCH 63/72] Fix authz tests for disk --- common/src/api/external/mod.rs | 2 +- nexus/src/app/disk.rs | 1 + nexus/src/external_api/http_entrypoints.rs | 7 +++---- nexus/tests/integration_tests/endpoints.rs | 16 ++++------------ nexus/types/src/external_api/params.rs | 2 +- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 6d988c61397..76b46c1b1e0 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -268,7 +268,7 @@ impl Name { } } -#[derive(Serialize, Deserialize, Display, Clone)] +#[derive(Debug, Serialize, Deserialize, Display, Clone)] #[display("{0}")] #[serde(untagged)] pub enum NameOrId { diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 61cd8b1b35b..600dd695eb9 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -73,6 +73,7 @@ impl super::Nexus { ) -> CreateResult { let (.., authz_project) = project_lookup.lookup_for(authz::Action::CreateChild).await?; + match ¶ms.disk_source { params::DiskSource::Blank { block_size } => { // Reject disks where the block size doesn't evenly divide the diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index b77f6722cf7..b21767955cb 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2435,13 +2435,12 @@ async fn disk_create_v1( let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); - let new_disk_params = &new_disk.into_inner(); + let params = new_disk.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let project_lookup = nexus.project_lookup(&opctx, &query)?; - let disk = nexus - .project_create_disk(&opctx, &project_lookup, new_disk_params) - .await?; + let disk = + nexus.project_create_disk(&opctx, &project_lookup, ¶ms).await?; Ok(HttpResponseCreated(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index f947d9ea87e..ff743418168 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -1144,22 +1144,14 @@ lazy_static! { ], }, - VerifyEndpoint { - url: &*DEMO_DISKS_URL, - visibility: Visibility::Protected, - unprivileged_access: UnprivilegedAccess::None, - allowed_methods: vec![ - AllowedMethod::Get, - ], - }, VerifyEndpoint { url: &*DEMO_DISKS_ATTACH_URL, visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ AllowedMethod::Post( - serde_json::to_value(params::DiskIdentifier { - name: DEMO_DISK_NAME.clone() + serde_json::to_value(params::InstanceIdentifier { + instance: DEMO_INSTANCE_NAME.clone().into() }).unwrap() ) ], @@ -1170,8 +1162,8 @@ lazy_static! { unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ AllowedMethod::Post( - serde_json::to_value(params::DiskIdentifier { - name: DEMO_DISK_NAME.clone() + serde_json::to_value(params::InstanceIdentifier { + instance: DEMO_INSTANCE_NAME.clone().into() }).unwrap() ) ], diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index ed3a6fe2bd5..52d8a75d8b3 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -1025,7 +1025,7 @@ pub struct DiskIdentifier { } // TODO-v1: Post merge, consolidate with paths -#[derive(Clone, Deserialize, Serialize, JsonSchema)] +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct InstanceIdentifier { pub instance: NameOrId, } From e02cd60667b2748d29bcc1fdc2c19270ea58ee3e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 20 Dec 2022 16:40:25 -0500 Subject: [PATCH 64/72] Fix final failing tests --- nexus/tests/output/uncovered-authz-endpoints.txt | 9 +++++++++ openapi/nexus.json | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index 1f6005f6886..5d869e31bd7 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -1,7 +1,9 @@ API endpoints with no coverage in authz tests: organization_delete (delete "/organizations/{organization_name}") project_delete (delete "/organizations/{organization_name}/projects/{project_name}") +disk_delete (delete "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}") instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") +disk_view_by_id (get "/by-id/disks/{id}") 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}") @@ -11,8 +13,12 @@ organization_view (get "/organizations/{organization_n organization_policy_view (get "/organizations/{organization_name}/policy") project_list (get "/organizations/{organization_name}/projects") project_view (get "/organizations/{organization_name}/projects/{project_name}") +disk_list (get "/organizations/{organization_name}/projects/{project_name}/disks") +disk_view (get "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}") +disk_metrics_list (get "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name}") instance_list (get "/organizations/{organization_name}/projects/{project_name}/instances") instance_view (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") +instance_disk_list (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks") instance_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") @@ -25,7 +31,10 @@ login_saml (post "/login/{silo_name}/saml/{provi logout (post "/logout") organization_create (post "/organizations") project_create (post "/organizations/{organization_name}/projects") +disk_create (post "/organizations/{organization_name}/projects/{project_name}/disks") instance_create (post "/organizations/{organization_name}/projects/{project_name}/instances") +instance_disk_attach (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach") +instance_disk_detach (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach") instance_migrate (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/migrate") instance_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") diff --git a/openapi/nexus.json b/openapi/nexus.json index baf5f5d5fc5..19ae5ce9464 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -2233,7 +2233,7 @@ "instances" ], "summary": "Attach a disk to an instance", - "description": "Use `POST /v1/disks/{disk}/attach { instance: } ` instead", + "description": "Use `POST /v1/disks/{disk}/attach` instead", "operationId": "instance_disk_attach", "parameters": [ { @@ -2298,7 +2298,7 @@ "instances" ], "summary": "Detach a disk from an instance", - "description": "Use `POST /v1/disks/{disk}/detach { instance: } ` instead", + "description": "Use `POST /v1/disks/{disk}/detach` instead", "operationId": "instance_disk_detach", "parameters": [ { From 4e602d21a329d46294f39daabedbc1db4d4cf2f4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 21 Dec 2022 14:07:53 -0500 Subject: [PATCH 65/72] Move back to old pattern of working with disks attached to instances --- nexus/src/app/instance.rs | 34 +- nexus/src/external_api/http_entrypoints.rs | 271 ++++++++-------- nexus/tests/integration_tests/endpoints.rs | 33 +- nexus/tests/output/nexus_tags.txt | 5 +- nexus/types/src/external_api/params.rs | 16 +- openapi/nexus.json | 358 ++++++++++++--------- 6 files changed, 401 insertions(+), 316 deletions(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index b5c8f8ddeb8..0e70e424462 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -707,12 +707,21 @@ impl super::Nexus { &self, opctx: &OpContext, instance_lookup: &lookup::Instance<'_>, - disk_lookup: &lookup::Disk<'_>, + disk: NameOrId, ) -> UpdateResult { - let (.., authz_instance) = + let (.., authz_project, authz_instance) = instance_lookup.lookup_for(authz::Action::Modify).await?; - let (.., authz_disk) = - disk_lookup.lookup_for(authz::Action::Modify).await?; + let (.., authz_disk) = self + .disk_lookup( + opctx, + ¶ms::DiskSelector::new( + None, + Some(authz_project.id().into()), + disk, + ), + )? + .lookup_for(authz::Action::Modify) + .await?; // TODO(https://github.com/oxidecomputer/omicron/issues/811): // Disk attach is only implemented for instances that are not // currently running. This operation therefore can operate exclusively @@ -743,12 +752,21 @@ impl super::Nexus { &self, opctx: &OpContext, instance_lookup: &lookup::Instance<'_>, - disk_lookup: &lookup::Disk<'_>, + disk: NameOrId, ) -> UpdateResult { - let (.., authz_instance) = + let (.., authz_project, authz_instance) = instance_lookup.lookup_for(authz::Action::Modify).await?; - let (.., authz_disk) = - disk_lookup.lookup_for(authz::Action::Modify).await?; + let (.., authz_disk) = self + .disk_lookup( + opctx, + ¶ms::DiskSelector::new( + None, + Some(authz_project.id().into()), + disk, + ), + )? + .lookup_for(authz::Action::Modify) + .await?; // TODO(https://github.com/oxidecomputer/omicron/issues/811): // Disk detach is only implemented for instances that are not // currently running. This operation therefore can operate exclusively diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index b21767955cb..25136bbb69b 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -152,8 +152,6 @@ pub fn external_api() -> NexusApiDescription { api.register(disk_create_v1)?; api.register(disk_view_v1)?; api.register(disk_delete_v1)?; - api.register(disk_attach_v1)?; - api.register(disk_detach_v1)?; api.register(disk_metrics_list_v1)?; api.register(instance_list)?; @@ -165,6 +163,9 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_reboot)?; api.register(instance_start)?; api.register(instance_stop)?; + api.register(instance_disk_list)?; + api.register(instance_disk_attach)?; + api.register(instance_disk_detach)?; api.register(instance_serial_console)?; api.register(instance_serial_console_stream)?; @@ -176,6 +177,9 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_reboot_v1)?; api.register(instance_start_v1)?; api.register(instance_stop_v1)?; + api.register(instance_disk_list_v1)?; + api.register(instance_disk_attach_v1)?; + api.register(instance_disk_detach_v1)?; api.register(instance_serial_console_v1)?; api.register(instance_serial_console_stream_v1)?; @@ -186,10 +190,6 @@ pub fn external_api() -> NexusApiDescription { api.register(image_view_by_id)?; api.register(image_delete)?; - api.register(instance_disk_list)?; - api.register(instance_disk_attach)?; - api.register(instance_disk_detach)?; - api.register(snapshot_list)?; api.register(snapshot_create)?; api.register(snapshot_view)?; @@ -2327,47 +2327,21 @@ async fn disk_list_v1( let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); - let instance = query.instance; - let handler = async { - let opctx = OpContext::for_external_api(&rqctx).await?; - let disks = if let Some(instance) = instance { - let instance_selector = params::InstanceSelector { - instance, - project_selector: query.project_selector, - }; - let instance_lookup = - nexus.instance_lookup(&opctx, &instance_selector)?; - nexus - .instance_list_disks( - &opctx, - &instance_lookup, - &data_page_params_for(&rqctx, &query.pagination)? - .map_name(|n| Name::ref_cast(n)), - ) - .await? - .into_iter() - .map(|disk| disk.into()) - .collect() - } else if let Some(selector) = query.project_selector { - let project_lookup = nexus.project_lookup(&opctx, &selector)?; - nexus - .project_list_disks( - &opctx, - &project_lookup, - &data_page_params_for(&rqctx, &query.pagination)? - .map_name(|n| Name::ref_cast(n)), - ) - .await? - .into_iter() - .map(|disk| disk.into()) - .collect() - } else { - Err(Error::InvalidRequest { - // TODO: Improve this error message - message: "either instance or project must be specified" - .to_string(), - })? - }; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project_lookup = + nexus.project_lookup(&opctx, &query.project_selector)?; + let disks = nexus + .project_list_disks( + &opctx, + &project_lookup, + &data_page_params_for(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)), + ) + .await? + .into_iter() + .map(|disk| disk.into()) + .collect(); Ok(HttpResponseOk(ScanByName::results_page( &query.pagination, disks, @@ -2623,82 +2597,6 @@ async fn disk_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Attach a disk to an instance -#[endpoint { - method = POST, - path = "/v1/disks/{disk}/attach", - tags = ["disks"], -}] -async fn disk_attach_v1( - rqctx: Arc>>, - path_params: Path, - query_params: Query, - body: TypedBody, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let query = query_params.into_inner(); - let body = body.into_inner(); - let instance_selector = params::InstanceSelector { - instance: body.instance, - project_selector: query.project_selector.clone(), - }; - let disk_selector = params::DiskSelector { - disk: path.disk, - project_selector: query.project_selector, - }; - let handler = async { - let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_lookup = - nexus.instance_lookup(&opctx, &instance_selector)?; - let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; - let disk = nexus - .instance_attach_disk(&opctx, &instance_lookup, &disk_lookup) - .await?; - Ok(HttpResponseAccepted(disk.into())) - }; - apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await -} - -/// Detach a disk from an instance -#[endpoint { - method = POST, - path = "/v1/disks/{disk}/detach", - tags = ["disks"], -}] -async fn disk_detach_v1( - rqctx: Arc>>, - path_params: Path, - query_params: Query, - body: TypedBody, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let query = query_params.into_inner(); - let body = body.into_inner(); - let instance_selector = params::InstanceSelector { - instance: body.instance, - project_selector: query.project_selector.clone(), - }; - let disk_selector = params::DiskSelector { - disk: path.disk, - project_selector: query.project_selector, - }; - let handler = async { - let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_lookup = - nexus.instance_lookup(&opctx, &instance_selector)?; - let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; - let disk = nexus - .instance_detach_disk(&opctx, &instance_lookup, &disk_lookup) - .await?; - Ok(HttpResponseAccepted(disk.into())) - }; - apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await -} - #[derive(Display, Deserialize, JsonSchema)] #[display(style = "snake_case")] #[serde(rename_all = "snake_case")] @@ -3457,9 +3355,50 @@ async fn instance_serial_console_stream( Ok(()) } +#[endpoint { + method = GET, + path = "/v1/instances/{instance}/disks", + tags = ["instances"], +}] +async fn instance_disk_list_v1( + rqctx: Arc>>, + query_params: Query, + path_params: Path, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let path = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_selector = params::InstanceSelector { + project_selector: Some(query.project_selector), + instance: path.instance, + }; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let disks = nexus + .instance_list_disks( + &opctx, + &instance_lookup, + &data_page_params_for(&rqctx, &query.pagination)? + .map_name(|n| Name::ref_cast(n)), + ) + .await? + .into_iter() + .map(|d| d.into()) + .collect(); + Ok(HttpResponseOk(ScanByName::results_page( + &query.pagination, + disks, + &marker_for_name, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// List an instance's disks -// TODO-scalability needs to be paginated -/// Use `GET /v1/disks?instance={instance}` instead +/// Use `GET /v1/instances/{instance}/disks` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks", @@ -3504,8 +3443,39 @@ async fn instance_disk_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = POST, + path = "/v1/instances/{instance}/disks/attach", + tags = ["instances"], +}] +async fn instance_disk_attach_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, + disk_to_attach: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let disk = disk_to_attach.into_inner().disk; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let disk = + nexus.instance_attach_disk(&opctx, &instance_lookup, disk).await?; + Ok(HttpResponseAccepted(disk.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Attach a disk to an instance -/// Use `POST /v1/disks/{disk}/attach` instead +/// Use `POST /v1/instances/{instance}/disks/attach` instead #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach", @@ -3526,24 +3496,49 @@ async fn instance_disk_attach( Some(path.project_name.clone().into()), path.instance_name.into(), ); - let disk_selector = params::DiskSelector::new( - Some(path.organization_name.into()), - Some(path.project_name.into()), - disk.name.into(), - ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; - let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; let disk = nexus - .instance_attach_disk(&opctx, &instance_lookup, &disk_lookup) + .instance_attach_disk(&opctx, &instance_lookup, disk.name.into()) .await?; Ok(HttpResponseAccepted(disk.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +#[endpoint { + method = POST, + path = "/v1/instances/{instance}/disks/detach", + tags = ["instances"], +}] +async fn instance_disk_detach_v1( + rqctx: Arc>>, + path_params: Path, + query_params: Query, + disk_to_detach: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let disk = disk_to_detach.into_inner().disk; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance_selector = params::InstanceSelector { + project_selector: query.project_selector, + instance: path.instance, + }; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + let disk = + nexus.instance_detach_disk(&opctx, &instance_lookup, disk).await?; + Ok(HttpResponseAccepted(disk.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Detach a disk from an instance /// Use `POST /v1/disks/{disk}/detach` instead #[endpoint { @@ -3560,24 +3555,18 @@ async fn instance_disk_detach( let apictx = rqctx.context(); let nexus = &apictx.nexus; let path = path_params.into_inner(); - let disk_name = disk_to_detach.into_inner().name; + let disk = disk_to_detach.into_inner(); let instance_selector = params::InstanceSelector::new( Some(path.organization_name.clone().into()), Some(path.project_name.clone().into()), path.instance_name.into(), ); - let disk_selector = params::DiskSelector::new( - Some(path.organization_name.into()), - Some(path.project_name.into()), - disk_name.into(), - ); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; - let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector)?; let disk = nexus - .instance_detach_disk(&opctx, &instance_lookup, &disk_lookup) + .instance_detach_disk(&opctx, &instance_lookup, disk.name.into()) .await?; Ok(HttpResponseAccepted(disk.into())) }; diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index ff743418168..da764915fc9 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -212,10 +212,6 @@ lazy_static! { DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 2 ), }; - pub static ref DEMO_DISKS_ATTACH_URL: String = - format!("/v1/disks/{}/attach?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_DISKS_DETACH_URL: String = - format!("/v1/disks/{}/detach?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR); pub static ref DEMO_DISK_METRICS_URL: String = format!( "/v1/disks/{}/metrics/activated?{}&start_time={:?}&end_time={:?}", @@ -247,6 +243,13 @@ lazy_static! { pub static ref DEMO_INSTANCE_SERIAL_STREAM_URL: String = format!("/v1/instances/{}/serial-console/stream?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); + pub static ref DEMO_INSTANCE_DISKS_URL: String = + format!("/v1/instances/{}/disks?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); + pub static ref DEMO_INSTANCE_DISKS_ATTACH_URL: String = + format!("/v1/instances/{}/disks/attach?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); + pub static ref DEMO_INSTANCE_DISKS_DETACH_URL: String = + format!("/v1/instances/{}/disks/detach?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); + // To be migrated... pub static ref DEMO_INSTANCE_NICS_URL: String = format!("/organizations/{}/projects/{}/instances/{}/network-interfaces", *DEMO_ORG_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); @@ -1145,25 +1148,35 @@ lazy_static! { }, VerifyEndpoint { - url: &*DEMO_DISKS_ATTACH_URL, + url: &*DEMO_INSTANCE_DISKS_URL, + visibility: Visibility::Protected, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, + ] + }, + + VerifyEndpoint { + url: &*DEMO_INSTANCE_DISKS_ATTACH_URL, visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ AllowedMethod::Post( - serde_json::to_value(params::InstanceIdentifier { - instance: DEMO_INSTANCE_NAME.clone().into() + serde_json::to_value(params::DiskPath { + disk: DEMO_DISK_NAME.clone().into() }).unwrap() ) ], }, + VerifyEndpoint { - url: &*DEMO_DISKS_DETACH_URL, + url: &*DEMO_INSTANCE_DISKS_DETACH_URL, visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ AllowedMethod::Post( - serde_json::to_value(params::InstanceIdentifier { - instance: DEMO_INSTANCE_NAME.clone().into() + serde_json::to_value(params::DiskPath { + disk: DEMO_DISK_NAME.clone().into() }).unwrap() ) ], diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 8d023b1afba..31dfa75f2c8 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -1,11 +1,9 @@ API operations found with tag "disks" OPERATION ID URL PATH -disk_attach_v1 /v1/disks/{disk}/attach disk_create /organizations/{organization_name}/projects/{project_name}/disks disk_create_v1 /v1/disks disk_delete /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} disk_delete_v1 /v1/disks/{disk} -disk_detach_v1 /v1/disks/{disk}/detach disk_list /organizations/{organization_name}/projects/{project_name}/disks disk_list_v1 /v1/disks disk_metrics_list /organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name} @@ -39,8 +37,11 @@ instance_create_v1 /v1/instances instance_delete /organizations/{organization_name}/projects/{project_name}/instances/{instance_name} instance_delete_v1 /v1/instances/{instance} instance_disk_attach /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/attach +instance_disk_attach_v1 /v1/instances/{instance}/disks/attach instance_disk_detach /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks/detach +instance_disk_detach_v1 /v1/instances/{instance}/disks/detach instance_disk_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks +instance_disk_list_v1 /v1/instances/{instance}/disks instance_external_ip_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/external-ips instance_list /organizations/{organization_name}/projects/{project_name}/instances instance_list_v1 /v1/instances diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 52d8a75d8b3..d7e5bd30d20 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -19,6 +19,7 @@ use serde::{ use std::{net::IpAddr, str::FromStr}; use uuid::Uuid; +// TODO-v1: Post migration rename `*Path` to `*Identifier` #[derive(Deserialize, JsonSchema)] pub struct OrganizationPath { pub organization: NameOrId, @@ -34,7 +35,7 @@ pub struct InstancePath { pub instance: NameOrId, } -#[derive(Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema)] pub struct DiskPath { pub disk: NameOrId, } @@ -111,11 +112,8 @@ impl DiskSelector { #[derive(Deserialize, JsonSchema)] pub struct DiskList { - /// Optional filter to only return disks attached to the given instance - pub instance: Option, - /// If `instance` is supplied as an ID this field should be left empty #[serde(flatten)] - pub project_selector: Option, + pub project_selector: ProjectSelector, #[serde(flatten)] pub pagination: PaginatedByName, } @@ -1017,6 +1015,7 @@ pub struct DiskCreate { pub size: ByteCount, } +/// TODO-v1: Delete this /// Parameters for the [`Disk`](omicron_common::api::external::Disk) to be /// attached or detached to an instance #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] @@ -1024,12 +1023,7 @@ pub struct DiskIdentifier { pub name: Name, } -// TODO-v1: Post merge, consolidate with paths -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct InstanceIdentifier { - pub instance: NameOrId, -} - +/// TODO-v1: Delete this /// Parameters for the /// [`NetworkInterface`](omicron_common::api::external::NetworkInterface) to be /// attached or detached to an instance. diff --git a/openapi/nexus.json b/openapi/nexus.json index 19ae5ce9464..c444cc3612d 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -2150,7 +2150,7 @@ "instances" ], "summary": "List an instance's disks", - "description": "Use `GET /v1/disks?instance={instance}` instead", + "description": "Use `GET /v1/instances/{instance}/disks` instead", "operationId": "instance_disk_list", "parameters": [ { @@ -2233,7 +2233,7 @@ "instances" ], "summary": "Attach a disk to an instance", - "description": "Use `POST /v1/disks/{disk}/attach` instead", + "description": "Use `POST /v1/instances/{instance}/disks/attach` instead", "operationId": "instance_disk_attach", "parameters": [ { @@ -7327,14 +7327,6 @@ ], "operationId": "disk_list_v1", "parameters": [ - { - "in": "query", - "name": "instance", - "description": "Optional filter to only return disks attached to the given instance", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, { "in": "query", "name": "limit", @@ -7365,6 +7357,7 @@ { "in": "query", "name": "project", + "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } @@ -7542,128 +7535,6 @@ } } }, - "/v1/disks/{disk}/attach": { - "post": { - "tags": [ - "disks" - ], - "summary": "Attach a disk to an instance", - "operationId": "disk_attach_v1", - "parameters": [ - { - "in": "path", - "name": "disk", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceIdentifier" - } - } - }, - "required": true - }, - "responses": { - "202": { - "description": "successfully enqueued operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Disk" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/v1/disks/{disk}/detach": { - "post": { - "tags": [ - "disks" - ], - "summary": "Detach a disk from an instance", - "operationId": "disk_detach_v1", - "parameters": [ - { - "in": "path", - "name": "disk", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceIdentifier" - } - } - }, - "required": true - }, - "responses": { - "202": { - "description": "successfully enqueued operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Disk" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, "/v1/disks/{disk}/metrics/{metric_name}": { "get": { "tags": [ @@ -7974,6 +7845,205 @@ } } }, + "/v1/instances/{instance}/disks": { + "get": { + "tags": [ + "instances" + ], + "operationId": "instance_disk_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameSortMode" + } + }, + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/v1/instances/{instance}/disks/attach": { + "post": { + "tags": [ + "instances" + ], + "operationId": "instance_disk_attach_v1", + "parameters": [ + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskPath" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/instances/{instance}/disks/detach": { + "post": { + "tags": [ + "instances" + ], + "operationId": "instance_disk_detach_v1", + "parameters": [ + { + "in": "path", + "name": "instance", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "organization", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskPath" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/instances/{instance}/migrate": { "post": { "tags": [ @@ -9556,7 +9626,7 @@ ] }, "DiskIdentifier": { - "description": "Parameters for the [`Disk`](omicron_common::api::external::Disk) to be attached or detached to an instance", + "description": "TODO-v1: Delete this Parameters for the [`Disk`](omicron_common::api::external::Disk) to be attached or detached to an instance", "type": "object", "properties": { "name": { @@ -9567,6 +9637,17 @@ "name" ] }, + "DiskPath": { + "type": "object", + "properties": { + "disk": { + "$ref": "#/components/schemas/NameOrId" + } + }, + "required": [ + "disk" + ] + }, "DiskResultsPage": { "description": "A single page of results", "type": "object", @@ -10779,17 +10860,6 @@ } ] }, - "InstanceIdentifier": { - "type": "object", - "properties": { - "instance": { - "$ref": "#/components/schemas/NameOrId" - } - }, - "required": [ - "instance" - ] - }, "InstanceMigrate": { "description": "Migration parameters for an [`Instance`](omicron_common::api::external::Instance)", "type": "object", From 8f79dc630002f082eab9f655e2a03a194ebe52ce Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 21 Dec 2022 14:28:12 -0500 Subject: [PATCH 66/72] Add checks to ensure disk from different project can be attached to instance --- nexus/src/app/instance.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 0e70e424462..6bc5e8a5d6e 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -711,7 +711,7 @@ impl super::Nexus { ) -> UpdateResult { let (.., authz_project, authz_instance) = instance_lookup.lookup_for(authz::Action::Modify).await?; - let (.., authz_disk) = self + let (.., authz_project_disk, authz_disk) = self .disk_lookup( opctx, ¶ms::DiskSelector::new( @@ -722,6 +722,19 @@ impl super::Nexus { )? .lookup_for(authz::Action::Modify) .await?; + + // TODO-v1: Write test to verify this case + // Because both instance and disk can be provided by ID it's possible for someone + // to specify resources from different projects. The lookups would resolve the resources + // (assuming the user had sufficient permissions on both) without verifying the shared hierarchy. + // To mitigate that we verify that their parent projects have the same ID. + if authz_project.id() != authz_project_disk.id() { + return Err(Error::InvalidRequest { + message: "disk must be in the same project as the instance" + .to_string(), + }); + } + // TODO(https://github.com/oxidecomputer/omicron/issues/811): // Disk attach is only implemented for instances that are not // currently running. This operation therefore can operate exclusively From 2f7575623bdd408036a46acf9c3d529fe07b7b8e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 21 Dec 2022 18:07:28 -0500 Subject: [PATCH 67/72] Update nexus/types/src/external_api/params.rs Co-authored-by: Adam Leventhal --- nexus/types/src/external_api/params.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index d7e5bd30d20..79e52d8f8e5 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -121,7 +121,18 @@ pub struct DiskList { #[derive(Deserialize, JsonSchema)] pub struct DiskMetricsList { #[serde(flatten)] - pub pagination: PaginationParams, +pub struct DiskMetricsListParams { + #[serde(flatten)] + pub metrics: ResourceMetrics, + + #[serde(flatten)] + pub project_selector: Option, +} + +pub struct DiskMetricsListSelector { + pub metrics: ResourceMetrics, + pub disk_select: DiskSelector, +} #[serde(flatten)] pub project_selector: Option, From 8ca6b353a3591357ded9a182a1eee5692d6e08e5 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 3 Jan 2023 22:32:42 -0500 Subject: [PATCH 68/72] Revert "Update nexus/types/src/external_api/params.rs" This reverts commit 2f7575623bdd408036a46acf9c3d529fe07b7b8e. --- nexus/types/src/external_api/params.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 79e52d8f8e5..d7e5bd30d20 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -121,18 +121,7 @@ pub struct DiskList { #[derive(Deserialize, JsonSchema)] pub struct DiskMetricsList { #[serde(flatten)] -pub struct DiskMetricsListParams { - #[serde(flatten)] - pub metrics: ResourceMetrics, - - #[serde(flatten)] - pub project_selector: Option, -} - -pub struct DiskMetricsListSelector { - pub metrics: ResourceMetrics, - pub disk_select: DiskSelector, -} + pub pagination: PaginationParams, #[serde(flatten)] pub project_selector: Option, From 7f608d1d79151efb1ab0c21056f31fd688a92d9c Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 12 Jan 2023 16:54:35 -0500 Subject: [PATCH 69/72] Drop metrics for v1; update disk integration tests; update pagination --- nexus/src/app/disk.rs | 17 ++- nexus/src/app/instance.rs | 22 ++- nexus/src/app/sagas/disk_create.rs | 22 ++- nexus/src/app/sagas/disk_delete.rs | 18 +-- nexus/src/db/datastore/disk.rs | 42 +++++- nexus/src/external_api/http_entrypoints.rs | 158 ++++++++++----------- nexus/tests/integration_tests/disks.rs | 83 +++++------ nexus/tests/output/nexus_tags.txt | 1 - nexus/types/src/external_api/params.rs | 59 +------- openapi/nexus.json | 155 +++----------------- 10 files changed, 226 insertions(+), 351 deletions(-) diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 1c3f1224206..327fcd627e2 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -258,7 +258,20 @@ impl super::Nexus { Ok(disk_created) } - pub async fn project_list_disks( + pub async fn project_list_disks_by_id( + &self, + opctx: &OpContext, + project_lookup: &lookup::Project<'_>, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ListChildren).await?; + self.db_datastore + .project_list_disks_by_id(opctx, &authz_project, pagparams) + .await + } + + pub async fn project_list_disks_by_name( &self, opctx: &OpContext, project_lookup: &lookup::Project<'_>, @@ -267,7 +280,7 @@ impl super::Nexus { let (.., authz_project) = project_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore - .project_list_disks(opctx, &authz_project, pagparams) + .project_list_disks_by_name(opctx, &authz_project, pagparams) .await } diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 7d96a832b35..782c6c91975 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -478,7 +478,7 @@ impl super::Nexus { // Gather disk information and turn that into DiskRequests let disks = self .db_datastore - .instance_list_disks( + .instance_list_disks_by_name( &opctx, &authz_instance, &DataPageParams { @@ -699,8 +699,22 @@ impl super::Nexus { } } - /// Lists disks attached to the instance. - pub async fn instance_list_disks( + /// Lists disks attached to the instance by id. + pub async fn instance_list_disks_by_id( + &self, + opctx: &OpContext, + instance_lookup: &lookup::Instance<'_>, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + let (.., authz_instance) = + instance_lookup.lookup_for(authz::Action::ListChildren).await?; + self.db_datastore + .instance_list_disks_by_id(opctx, &authz_instance, pagparams) + .await + } + + /// Lists disks attached to the instance by name. + pub async fn instance_list_disks_by_name( &self, opctx: &OpContext, instance_lookup: &lookup::Instance<'_>, @@ -709,7 +723,7 @@ impl super::Nexus { let (.., authz_instance) = instance_lookup.lookup_for(authz::Action::ListChildren).await?; self.db_datastore - .instance_list_disks(opctx, &authz_instance, pagparams) + .instance_list_disks_by_name(opctx, &authz_instance, pagparams) .await } diff --git a/nexus/src/app/sagas/disk_create.rs b/nexus/src/app/sagas/disk_create.rs index 9a678ec5f75..efda83b1f4b 100644 --- a/nexus/src/app/sagas/disk_create.rs +++ b/nexus/src/app/sagas/disk_create.rs @@ -585,7 +585,7 @@ pub(crate) mod test { use crate::{ app::saga::create_saga_dag, app::sagas::disk_create::Params, app::sagas::disk_create::SagaDiskCreate, authn::saga::Serialized, - context::OpContext, db, db::datastore::DataStore, external_api::params, + context::OpContext, db::datastore::DataStore, external_api::params, }; use async_bb8_diesel::{AsyncRunQueryDsl, OptionalExtension}; use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; @@ -599,7 +599,6 @@ pub(crate) mod test { use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::Name; use omicron_sled_agent::sim::SledAgent; - use ref_cast::RefCast; use std::num::NonZeroU32; use uuid::Uuid; @@ -863,20 +862,15 @@ pub(crate) mod test { async fn destroy_disk(cptestctx: &ControlPlaneTestContext) { let nexus = &cptestctx.server.apictx.nexus; let opctx = test_opctx(&cptestctx); + let disk_selector = params::DiskSelector::new( + Some(Name::try_from(ORG_NAME.to_string()).unwrap().into()), + Some(Name::try_from(PROJECT_NAME.to_string()).unwrap().into()), + Name::try_from(DISK_NAME.to_string()).unwrap().into(), + ); + let disk_lookup = nexus.disk_lookup(&opctx, &disk_selector).unwrap(); nexus - .project_delete_disk( - &opctx, - db::model::Name::ref_cast( - &Name::try_from(ORG_NAME.to_string()).unwrap(), - ), - db::model::Name::ref_cast( - &Name::try_from(PROJECT_NAME.to_string()).unwrap(), - ), - db::model::Name::ref_cast( - &Name::try_from(DISK_NAME.to_string()).unwrap(), - ), - ) + .project_delete_disk(&disk_lookup) .await .expect("Failed to delete disk"); } diff --git a/nexus/src/app/sagas/disk_delete.rs b/nexus/src/app/sagas/disk_delete.rs index e0c78e94dd5..3634217a3a9 100644 --- a/nexus/src/app/sagas/disk_delete.rs +++ b/nexus/src/app/sagas/disk_delete.rs @@ -88,7 +88,7 @@ async fn sdd_delete_volume( pub(crate) mod test { use crate::{ app::saga::create_saga_dag, app::sagas::disk_delete::Params, - app::sagas::disk_delete::SagaDiskDelete, context::OpContext, db, + app::sagas::disk_delete::SagaDiskDelete, context::OpContext, }; use dropshot::test_util::ClientTestContext; use nexus_test_utils::resource_helpers::create_ip_pool; @@ -96,8 +96,8 @@ pub(crate) mod test { use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::DiskTest; use nexus_test_utils_macros::nexus_test; + use nexus_types::external_api::params; use omicron_common::api::external::Name; - use ref_cast::RefCast; use std::num::NonZeroU32; use uuid::Uuid; @@ -125,15 +125,17 @@ pub(crate) mod test { let nexus = &cptestctx.server.apictx.nexus; let opctx = test_opctx(&cptestctx); + let project_selector = params::ProjectSelector::new( + Some(Name::try_from(ORG_NAME.to_string()).unwrap().into()), + Name::try_from(PROJECT_NAME.to_string()).unwrap().into(), + ); + let project_lookup = + nexus.project_lookup(&opctx, &project_selector).unwrap(); + nexus .project_create_disk( &opctx, - db::model::Name::ref_cast( - &Name::try_from(ORG_NAME.to_string()).unwrap(), - ), - db::model::Name::ref_cast( - &Name::try_from(PROJECT_NAME.to_string()).unwrap(), - ), + &project_lookup, &crate::app::sagas::disk_create::test::new_disk_create_params(), ) .await diff --git a/nexus/src/db/datastore/disk.rs b/nexus/src/db/datastore/disk.rs index cc1a3cf7216..33a6b257cf1 100644 --- a/nexus/src/db/datastore/disk.rs +++ b/nexus/src/db/datastore/disk.rs @@ -42,8 +42,27 @@ use omicron_common::bail_unless; use uuid::Uuid; impl DataStore { - /// List disks associated with a given instance. - pub async fn instance_list_disks( + /// List disks associated with a given instance by id. + pub async fn instance_list_disks_by_id( + &self, + opctx: &OpContext, + authz_instance: &authz::Instance, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + use db::schema::disk::dsl; + + opctx.authorize(authz::Action::ListChildren, authz_instance).await?; + + paginated(dsl::disk, dsl::id, &pagparams) + .filter(dsl::time_deleted.is_null()) + .filter(dsl::attach_instance_id.eq(authz_instance.id())) + .select(Disk::as_select()) + .load_async::(self.pool_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) + } + /// List disks associated with a given instance by name. + pub async fn instance_list_disks_by_name( &self, opctx: &OpContext, authz_instance: &authz::Instance, @@ -110,7 +129,24 @@ impl DataStore { Ok(disk) } - pub async fn project_list_disks( + pub async fn project_list_disks_by_id( + &self, + opctx: &OpContext, + authz_project: &authz::Project, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + opctx.authorize(authz::Action::ListChildren, authz_project).await?; + + use db::schema::disk::dsl; + paginated(dsl::disk, dsl::id, &pagparams) + .filter(dsl::time_deleted.is_null()) + .filter(dsl::project_id.eq(authz_project.id())) + .select(Disk::as_select()) + .load_async::(self.pool_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) + } + pub async fn project_list_disks_by_name( &self, opctx: &OpContext, authz_project: &authz::Project, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index a8e8d6b4f5a..71f70d76be3 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -150,7 +150,6 @@ pub fn external_api() -> NexusApiDescription { api.register(disk_create_v1)?; api.register(disk_view_v1)?; api.register(disk_delete_v1)?; - api.register(disk_metrics_list_v1)?; api.register(instance_list)?; api.register(instance_create)?; @@ -2311,30 +2310,43 @@ async fn ip_pool_service_range_remove( }] async fn disk_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query>, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let project_lookup = - nexus.project_lookup(&opctx, &query.project_selector)?; - let disks = nexus - .project_list_disks( - &opctx, - &project_lookup, - &data_page_params_for(&rqctx, &query.pagination)? - .map_name(|n| Name::ref_cast(n)), - ) - .await? - .into_iter() - .map(|disk| disk.into()) - .collect(); - Ok(HttpResponseOk(ScanByName::results_page( - &query.pagination, + let disks = match name_or_id_pagination(&query, &pag_params)? { + PaginatedBy::Id(pag_params, selector) => { + let project_lookup = nexus.project_lookup(&opctx, &selector)?; + nexus + .project_list_disks_by_id( + &opctx, + &project_lookup, + &pag_params, + ) + .await? + } + PaginatedBy::Name(pag_params, selector) => { + let project_lookup = nexus.project_lookup(&opctx, &selector)?; + nexus + .project_list_disks_by_name( + &opctx, + &project_lookup, + &pag_params.map_name(|n| Name::ref_cast(n)), + ) + .await? + } + } + .into_iter() + .map(|disk| disk.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, disks, - &marker_for_name, + &marker_for_name_or_id, )?)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -2365,7 +2377,7 @@ async fn disk_list( let opctx = OpContext::for_external_api(&rqctx).await?; let authz_project = nexus.project_lookup(&opctx, &project_selector)?; let disks = nexus - .project_list_disks( + .project_list_disks_by_name( &opctx, &authz_project, &data_page_params_for(&rqctx, &query)? @@ -2598,56 +2610,11 @@ pub enum DiskMetricName { WriteBytes, } -#[endpoint { - method = GET, - path = "/v1/disks/{disk}/metrics/{metric_name}", - tags = ["disks"], -}] -async fn disk_metrics_list_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 metric_name = path.metric_name; - let query = query_params.into_inner(); - let limit = rqctx.page_limit(&query.pagination)?; - - let disk_selector = params::DiskSelector { - disk: path.inner.disk, - project_selector: query.project_selector, - }; - let handler = async { - let opctx = OpContext::for_external_api(&rqctx).await?; - let (.., authz_disk) = nexus - .disk_lookup(&opctx, &disk_selector)? - .lookup_for(authz::Action::Read) - .await?; - - let result = nexus - .select_timeseries( - &format!("crucible_upstairs:{}", metric_name), - &[&format!("upstairs_uuid=={}", authz_disk.id())], - query.pagination, - limit, - ) - .await?; - - Ok(HttpResponseOk(result)) - }; - - apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await -} - /// Fetch disk metrics -/// Use `GET /v1/disks/{disk}/metrics/{metric_name}` instead #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name}", tags = ["disks"], - deprecated = true }] async fn disk_metrics_list( rqctx: Arc>>, @@ -3389,36 +3356,55 @@ async fn instance_serial_console_stream( }] async fn instance_disk_list_v1( rqctx: Arc>>, - query_params: Query, + query_params: Query>, path_params: Path, ) -> Result>, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; let query = query_params.into_inner(); let path = path_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let instance_selector = params::InstanceSelector { - project_selector: Some(query.project_selector), - instance: path.instance, - }; - let instance_lookup = - nexus.instance_lookup(&opctx, &instance_selector)?; - let disks = nexus - .instance_list_disks( - &opctx, - &instance_lookup, - &data_page_params_for(&rqctx, &query.pagination)? - .map_name(|n| Name::ref_cast(n)), - ) - .await? - .into_iter() - .map(|d| d.into()) - .collect(); - Ok(HttpResponseOk(ScanByName::results_page( - &query.pagination, + let disks = match name_or_id_pagination(&query, &pag_params)? { + PaginatedBy::Id(pag_params, selector) => { + let instance_selector = params::InstanceSelector { + project_selector: selector.project_selector, + instance: path.instance, + }; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + nexus + .instance_list_disks_by_id( + &opctx, + &instance_lookup, + &pag_params, + ) + .await? + } + PaginatedBy::Name(pag_params, selector) => { + let instance_selector = params::InstanceSelector { + project_selector: selector.project_selector, + instance: path.instance, + }; + let instance_lookup = + nexus.instance_lookup(&opctx, &instance_selector)?; + nexus + .instance_list_disks_by_name( + &opctx, + &instance_lookup, + &pag_params.map_name(|n| Name::ref_cast(n)), + ) + .await? + } + } + .into_iter() + .map(|d| d.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, disks, - &marker_for_name, + &marker_for_name_or_id, )?)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -3451,7 +3437,7 @@ async fn instance_disk_list( let instance_lookup = nexus.instance_lookup(&opctx, &instance_selector)?; let disks = nexus - .instance_list_disks( + .instance_list_disks_by_name( &opctx, &instance_lookup, &data_page_params_for(&rqctx, &query)? diff --git a/nexus/tests/integration_tests/disks.rs b/nexus/tests/integration_tests/disks.rs index ae631019d59..2ddacc27470 100644 --- a/nexus/tests/integration_tests/disks.rs +++ b/nexus/tests/integration_tests/disks.rs @@ -48,28 +48,36 @@ const PROJECT_NAME: &str = "springfield-squidport-disks"; const DISK_NAME: &str = "just-rainsticks"; const INSTANCE_NAME: &str = "just-rainsticks"; -fn get_project_url() -> String { - format!("/organizations/{}/projects/{}", ORG_NAME, PROJECT_NAME) -} - fn get_disks_url() -> String { - format!("{}/disks", get_project_url()) + format!("/v1/disks?organization={}&project={}", ORG_NAME, PROJECT_NAME) } -fn get_instances_url() -> String { - format!("{}/instances", get_project_url()) +fn get_disk_url(disk_name: &str) -> String { + format!( + "/v1/disks/{disk_name}?organization={}&project={}", + ORG_NAME, PROJECT_NAME + ) } fn get_instance_disks_url(instance_name: &str) -> String { - format!("{}/{}/disks", get_instances_url(), instance_name) + format!( + "/v1/instances/{instance_name}/disks?organization={}&project={}", + ORG_NAME, PROJECT_NAME + ) } fn get_disk_attach_url(instance_name: &str) -> String { - format!("{}/attach", get_instance_disks_url(instance_name)) + format!( + "/v1/instances/{instance_name}/disks/attach?organization={}&project={}", + ORG_NAME, PROJECT_NAME + ) } fn get_disk_detach_url(instance_name: &str) -> String { - format!("{}/detach", get_instance_disks_url(instance_name)) + format!( + "/v1/instances/{instance_name}/disks/detach?organization={}&project={}", + ORG_NAME, PROJECT_NAME + ) } async fn create_org_and_project(client: &ClientTestContext) -> Uuid { @@ -93,7 +101,7 @@ async fn test_disk_not_found_before_creation( assert_eq!(disks.len(), 0); // Make sure we get a 404 if we fetch one. - let disk_url = format!("{}/{}", disks_url, DISK_NAME); + let disk_url = get_disk_url(DISK_NAME); let error = NexusRequest::new( RequestBuilder::new(client, Method::GET, &disk_url) .expect_status(Some(StatusCode::NOT_FOUND)), @@ -128,10 +136,14 @@ async fn test_disk_not_found_before_creation( async fn set_instance_state( client: &ClientTestContext, - instance_url: &str, + instance_name: &str, state: &str, ) -> Instance { - let url = format!("{}/{}", instance_url, state); + let url = format!( + "/v1/instances/{instance_name}/{state}?organization={}&project={}", + ORG_NAME, PROJECT_NAME + ); + NexusRequest::new( RequestBuilder::new(client, Method::POST, &url) .body(None as Option<&serde_json::Value>) @@ -161,7 +173,7 @@ async fn test_disk_create_attach_detach_delete( let disks_url = get_disks_url(); // Create a disk. - let disk_url = format!("{}/{}", disks_url, DISK_NAME); + let disk_url = get_disk_url(DISK_NAME); let disk = create_disk(&client, ORG_NAME, PROJECT_NAME, DISK_NAME).await; assert_eq!(disk.identity.name, DISK_NAME); assert_eq!(disk.identity.description, "sells rainsticks"); @@ -198,12 +210,8 @@ async fn test_disk_create_attach_detach_delete( // // Instances must be stopped before disks can be attached - this // is an artificial limitation without hotplug support. - let instance1_url = format!( - "/organizations/{}/projects/{}/instances/{}", - ORG_NAME, PROJECT_NAME, INSTANCE_NAME - ); let instance_next = - set_instance_state(&client, &instance1_url, "stop").await; + set_instance_state(&client, INSTANCE_NAME, "stop").await; instance_simulate(nexus, &instance_next.identity.id).await; // Verify that there are no disks attached to the instance, and specifically @@ -305,7 +313,7 @@ async fn test_disk_create_disk_that_already_exists_fails( size: ByteCount::from_gibibytes_u32(1), }; let _ = create_disk(&client, ORG_NAME, PROJECT_NAME, DISK_NAME).await; - let disk_url = format!("{}/{}", disks_url, DISK_NAME); + let disk_url = get_disk_url(DISK_NAME); let disk = disk_get(&client, &disk_url).await; // Attempt to create a second disk with a conflicting name. @@ -340,7 +348,7 @@ async fn test_disk_move_between_instances(cptestctx: &ControlPlaneTestContext) { let disks_url = get_disks_url(); // Create a disk. - let disk_url = format!("{}/{}", disks_url, DISK_NAME); + let disk_url = get_disk_url(DISK_NAME); let disk = create_disk(client, ORG_NAME, PROJECT_NAME, DISK_NAME).await; // Create an instance to attach the disk. @@ -350,12 +358,8 @@ async fn test_disk_move_between_instances(cptestctx: &ControlPlaneTestContext) { // // Instances must be stopped before disks can be attached - this // is an artificial limitation without hotplug support. - let instance_url = format!( - "/organizations/{}/projects/{}/instances/{}", - ORG_NAME, PROJECT_NAME, INSTANCE_NAME - ); let instance_next = - set_instance_state(&client, &instance_url, "stop").await; + set_instance_state(&client, INSTANCE_NAME, "stop").await; instance_simulate(nexus, &instance_next.identity.id).await; // Verify that there are no disks attached to the instance, and specifically @@ -392,12 +396,7 @@ async fn test_disk_move_between_instances(cptestctx: &ControlPlaneTestContext) { // fail and the disk should remain attached to the first instance. let instance2 = create_instance(&client, ORG_NAME, PROJECT_NAME, "instance2").await; - let instance2_url = format!( - "/organizations/{}/projects/{}/instances/{}", - ORG_NAME, PROJECT_NAME, "instance2" - ); - let instance_next = - set_instance_state(&client, &instance2_url, "stop").await; + let instance_next = set_instance_state(&client, "instance2", "stop").await; instance_simulate(nexus, &instance_next.identity.id).await; let url_instance2_attach_disk = @@ -407,8 +406,8 @@ async fn test_disk_move_between_instances(cptestctx: &ControlPlaneTestContext) { let error: HttpErrorResponseBody = NexusRequest::new( RequestBuilder::new(client, Method::POST, &url_instance2_attach_disk) - .body(Some(¶ms::DiskIdentifier { - name: disk.identity.name.clone(), + .body(Some(¶ms::DiskPath { + disk: disk.identity.name.clone().into(), })) .expect_status(Some(StatusCode::BAD_REQUEST)), ) @@ -463,8 +462,8 @@ async fn test_disk_move_between_instances(cptestctx: &ControlPlaneTestContext) { // instance (the first one). let error: HttpErrorResponseBody = NexusRequest::new( RequestBuilder::new(client, Method::POST, &url_instance_attach_disk) - .body(Some(¶ms::DiskIdentifier { - name: disk.identity.name.clone(), + .body(Some(¶ms::DiskPath { + disk: disk.identity.name.clone().into(), })) .expect_status(Some(StatusCode::BAD_REQUEST)), ) @@ -1004,7 +1003,7 @@ async fn test_disk_size_accounting(cptestctx: &ControlPlaneTestContext) { } // Delete the first disk, freeing up 7 gibibytes. - let disk_url = format!("{}/{}", disks_url, "disk-one"); + let disk_url = get_disk_url("disk-one"); NexusRequest::new( RequestBuilder::new(client, Method::DELETE, &disk_url) .expect_status(Some(StatusCode::NO_CONTENT)), @@ -1179,7 +1178,7 @@ async fn test_disk_metrics(cptestctx: &ControlPlaneTestContext) { // Whenever we grab this URL, get the surrounding few seconds of metrics. let metric_url = |metric_type: &str| { - let disk_url = format!("{}/{}", get_disks_url(), DISK_NAME); + let disk_url = format!("/organizations/{ORG_NAME}/projects/{PROJECT_NAME}/disks/{DISK_NAME}"); format!( "{disk_url}/metrics/{metric_type}?start_time={:?}&end_time={:?}", Utc::now() - chrono::Duration::seconds(2), @@ -1221,10 +1220,12 @@ async fn test_disk_metrics_paginated(cptestctx: &ControlPlaneTestContext) { create_org_and_project(client).await; create_disk(&client, ORG_NAME, PROJECT_NAME, DISK_NAME).await; create_instance_with_disk(client).await; + let disk_url = format!( + "/organizations/{ORG_NAME}/projects/{PROJECT_NAME}/disks/{DISK_NAME}" + ); for metric in &ALL_METRICS { - let collection_url = - format!("{}/{DISK_NAME}/metrics/{metric}", get_disks_url()); + let collection_url = format!("{}/metrics/{metric}", disk_url); let initial_params = format!( "start_time={:?}&end_time={:?}", Utc::now() - chrono::Duration::seconds(2), @@ -1296,7 +1297,7 @@ async fn disk_post( ) -> Disk { NexusRequest::new( RequestBuilder::new(client, Method::POST, url) - .body(Some(¶ms::DiskIdentifier { name: disk_name })) + .body(Some(¶ms::DiskPath { disk: disk_name.into() })) .expect_status(Some(StatusCode::ACCEPTED)), ) .authn_as(AuthnMode::PrivilegedUser) diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 31dfa75f2c8..8c1b793a07f 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -7,7 +7,6 @@ disk_delete_v1 /v1/disks/{disk} disk_list /organizations/{organization_name}/projects/{project_name}/disks disk_list_v1 /v1/disks disk_metrics_list /organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name} -disk_metrics_list_v1 /v1/disks/{disk}/metrics/{metric_name} disk_view /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} disk_view_by_id /by-id/disks/{id} disk_view_v1 /v1/disks/{disk} diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 98647898d0b..7a69b497096 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -34,7 +34,7 @@ pub struct InstancePath { pub instance: NameOrId, } -#[derive(Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema)] pub struct DiskPath { pub disk: NameOrId, } @@ -74,7 +74,7 @@ impl ProjectSelector { } } -#[derive(Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct OptionalProjectSelector { #[serde(flatten)] pub project_selector: Option, @@ -101,61 +101,6 @@ impl DiskSelector { } } -#[derive(Deserialize, JsonSchema)] -pub struct DiskList { - #[serde(flatten)] - pub project_selector: ProjectSelector, - #[serde(flatten)] - pub pagination: PaginatedByName, -} - -#[derive(Deserialize, JsonSchema)] -pub struct DiskMetricsList { - #[serde(flatten)] - pub pagination: PaginationParams, - - #[serde(flatten)] - pub project_selector: Option, -} - -#[derive(Deserialize, JsonSchema)] -pub struct DiskSelector { - #[serde(flatten)] - pub project_selector: Option, - pub disk: NameOrId, -} - -impl DiskSelector { - pub fn new( - organization: Option, - project: Option, - disk: NameOrId, - ) -> Self { - DiskSelector { - project_selector: project - .map(|p| ProjectSelector::new(organization, p)), - disk, - } - } -} - -#[derive(Deserialize, JsonSchema)] -pub struct DiskList { - #[serde(flatten)] - pub project_selector: ProjectSelector, - #[serde(flatten)] - pub pagination: PaginatedByName, -} - -#[derive(Deserialize, JsonSchema)] -pub struct DiskMetricsList { - #[serde(flatten)] - pub pagination: PaginationParams, - - #[serde(flatten)] - pub project_selector: Option, -} - #[derive(Deserialize, JsonSchema)] pub struct InstanceSelector { #[serde(flatten)] diff --git a/openapi/nexus.json b/openapi/nexus.json index b5191331201..8446c1cead4 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -1590,7 +1590,6 @@ "disks" ], "summary": "Fetch disk metrics", - "description": "Use `GET /v1/disks/{disk}/metrics/{metric_name}` instead", "operationId": "disk_metrics_list", "parameters": [ { @@ -1682,7 +1681,6 @@ "$ref": "#/components/responses/Error" } }, - "deprecated": true, "x-dropshot-pagination": true } }, @@ -7344,8 +7342,7 @@ "tags": [ "disks" ], - "summary": "List instances", - "operationId": "instance_list_v1", + "operationId": "disk_list_v1", "parameters": [ { "in": "query", @@ -7413,8 +7410,8 @@ "tags": [ "disks" ], - "summary": "Create an instance", - "operationId": "instance_create_v1", + "summary": "Create a disk", + "operationId": "disk_create_v1", "parameters": [ { "in": "query", @@ -7467,8 +7464,7 @@ "tags": [ "disks" ], - "summary": "Fetch an instance", - "operationId": "instance_view_v1", + "operationId": "disk_view_v1", "parameters": [ { "in": "path", @@ -7516,8 +7512,8 @@ "tags": [ "disks" ], - "summary": "Delete an instance", - "operationId": "instance_delete_v1", + "summary": "Delete a disk", + "operationId": "disk_delete_v1", "parameters": [ { "in": "path", @@ -7555,128 +7551,13 @@ } } }, - "/v1/disks/{disk}/metrics/{metric_name}": { - "get": { - "tags": [ - "disks" - ], - "summary": "Migrate an instance", - "operationId": "instance_migrate_v1", - "parameters": [ - { - "in": "path", - "name": "disk", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "path", - "name": "metric_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/DiskMetricName" - } - }, - { - "in": "query", - "name": "end_time", - "description": "An exclusive end time of metrics.", - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "in": "query", - "name": "limit", - "description": "Maximum number of items returned by a single call", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint32", - "minimum": 1 - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/v1/instances/{instance}/reboot": { - "post": { - "tags": [ - "instances" - ], - "summary": "Reboot an instance", - "operationId": "instance_reboot_v1", - "parameters": [ - { - "in": "query", - "name": "organization", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "page_token", - "description": "Token returned by previous call to retrieve the subsequent page", - "schema": { - "nullable": true, - "type": "string" - } - }, - { - "in": "query", - "name": "project", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "start_time", - "description": "An inclusive start time of metrics.", - "schema": { - "type": "string", - "format": "date-time" - } - } - ], - "responses": { - "202": { - "description": "successfully enqueued operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MeasurementResultsPage" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - }, - "x-dropshot-pagination": true - } - }, "/v1/instances": { "get": { "tags": [ "instances" ], - "summary": "Fetch an instance's serial console", - "operationId": "instance_serial_console_v1", + "summary": "List instances", + "operationId": "instance_list_v1", "parameters": [ { "in": "query", @@ -7708,7 +7589,6 @@ { "in": "query", "name": "project", - "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } @@ -7717,7 +7597,7 @@ "in": "query", "name": "sort_by", "schema": { - "$ref": "#/components/schemas/NameSortMode" + "$ref": "#/components/schemas/NameOrIdSortMode" } } ], @@ -7745,8 +7625,8 @@ "tags": [ "instances" ], - "summary": "Stream an instance's serial console", - "operationId": "instance_serial_console_stream_v1", + "summary": "Create an instance", + "operationId": "instance_create_v1", "parameters": [ { "in": "query", @@ -7799,6 +7679,7 @@ "tags": [ "instances" ], + "summary": "Fetch an instance", "operationId": "instance_view_v1", "parameters": [ { @@ -7847,6 +7728,7 @@ "tags": [ "instances" ], + "summary": "Delete an instance", "operationId": "instance_delete_v1", "parameters": [ { @@ -7922,7 +7804,6 @@ { "in": "query", "name": "project", - "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } @@ -7931,7 +7812,7 @@ "in": "query", "name": "sort_by", "schema": { - "$ref": "#/components/schemas/NameSortMode" + "$ref": "#/components/schemas/NameOrIdSortMode" } }, { @@ -8089,6 +7970,7 @@ "tags": [ "instances" ], + "summary": "Migrate an instance", "operationId": "instance_migrate_v1", "parameters": [ { @@ -8149,6 +8031,7 @@ "tags": [ "instances" ], + "summary": "Reboot an instance", "operationId": "instance_reboot_v1", "parameters": [ { @@ -8175,8 +8058,8 @@ } ], "responses": { - "200": { - "description": "successful operation", + "202": { + "description": "successfully enqueued operation", "content": { "application/json": { "schema": { @@ -8199,6 +8082,7 @@ "tags": [ "instances" ], + "summary": "Fetch an instance's serial console", "operationId": "instance_serial_console_v1", "parameters": [ { @@ -8282,6 +8166,7 @@ "tags": [ "instances" ], + "summary": "Stream an instance's serial console", "operationId": "instance_serial_console_stream_v1", "parameters": [ { From 2b9369a57b3837d885254a00892342a193e4678a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 12 Jan 2023 23:24:02 -0500 Subject: [PATCH 70/72] Fix unauth test --- nexus/tests/integration_tests/endpoints.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index da764915fc9..fa83cd73dbb 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -214,9 +214,10 @@ lazy_static! { }; pub static ref DEMO_DISK_METRICS_URL: String = format!( - "/v1/disks/{}/metrics/activated?{}&start_time={:?}&end_time={:?}", + "/organizations/{}/projects/{}/disks/{}/metrics/activated?start_time={:?}&end_time={:?}", + *DEMO_ORG_NAME, + *DEMO_PROJECT_NAME, *DEMO_DISK_NAME, - *DEMO_PROJECT_SELECTOR, Utc::now(), Utc::now(), ); From f357742f9fcf2086dd159d552e174dac6cb4c938 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 17 Jan 2023 10:41:17 -0500 Subject: [PATCH 71/72] Restore disk metrics coverage --- nexus/tests/output/uncovered-authz-endpoints.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index 5d869e31bd7..653d8046f04 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -15,7 +15,6 @@ project_list (get "/organizations/{organization_n project_view (get "/organizations/{organization_name}/projects/{project_name}") disk_list (get "/organizations/{organization_name}/projects/{project_name}/disks") disk_view (get "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}") -disk_metrics_list (get "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/metrics/{metric_name}") instance_list (get "/organizations/{organization_name}/projects/{project_name}/instances") instance_view (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") instance_disk_list (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks") From 178e86cbc0176f15621052a400d31e1b956e6589 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 17 Jan 2023 12:19:56 -0500 Subject: [PATCH 72/72] Revert pub change --- common/src/api/external/http_pagination.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/api/external/http_pagination.rs b/common/src/api/external/http_pagination.rs index b3a3a5c8b8e..601534db431 100644 --- a/common/src/api/external/http_pagination.rs +++ b/common/src/api/external/http_pagination.rs @@ -46,7 +46,7 @@ use crate::api::external::NameOrId; use crate::api::external::ObjectIdentity; use crate::api::external::PaginationOrder; use dropshot::HttpError; -pub use dropshot::PaginationParams; +use dropshot::PaginationParams; use dropshot::RequestContext; use dropshot::ResultsPage; use dropshot::WhichPage;