diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index dd4aade4df..21964cb09d 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -214,6 +214,18 @@ impl super::Nexus { Ok(db_disk) } + pub async fn disk_fetch_by_id( + &self, + opctx: &OpContext, + disk_id: &Uuid, + ) -> LookupResult { + let (.., db_disk) = LookupPath::new(opctx, &self.db_datastore) + .disk_id(*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): @@ -386,6 +398,18 @@ impl super::Nexus { Err(self.unimplemented_todo(opctx, unimp).await) } + pub async fn snapshot_fetch_by_id( + &self, + opctx: &OpContext, + snapshot_id: &Uuid, + ) -> LookupResult { + let lookup_type = LookupType::ById(*snapshot_id); + let not_found_error = + lookup_type.into_not_found(ResourceType::Snapshot); + let unimp = Unimpl::ProtectedLookup(not_found_error); + Err(self.unimplemented_todo(opctx, unimp).await) + } + pub async fn project_delete_snapshot( self: &Arc, opctx: &OpContext, diff --git a/nexus/src/app/image.rs b/nexus/src/app/image.rs index cd3edee548..61a9d84093 100644 --- a/nexus/src/app/image.rs +++ b/nexus/src/app/image.rs @@ -71,6 +71,17 @@ impl super::Nexus { Err(self.unimplemented_todo(opctx, unimp).await) } + pub async fn project_image_fetch_by_id( + &self, + opctx: &OpContext, + image_id: &Uuid, + ) -> LookupResult { + let lookup_type = LookupType::ById(*image_id); + let not_found_error = lookup_type.into_not_found(ResourceType::Image); + let unimp = Unimpl::ProtectedLookup(not_found_error); + Err(self.unimplemented_todo(opctx, unimp).await) + } + pub async fn project_delete_image( self: &Arc, opctx: &OpContext, @@ -293,6 +304,18 @@ impl super::Nexus { Ok(db_disk) } + pub async fn global_image_fetch_by_id( + &self, + opctx: &OpContext, + global_image_id: &Uuid, + ) -> LookupResult { + let (.., db_global_image) = LookupPath::new(opctx, &self.db_datastore) + .global_image_id(*global_image_id) + .fetch() + .await?; + Ok(db_global_image) + } + pub async fn global_image_delete( self: &Arc, opctx: &OpContext, diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 168708a4e2..7b046a924d 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -179,6 +179,18 @@ impl super::Nexus { Ok(db_instance) } + pub async fn instance_fetch_by_id( + &self, + opctx: &OpContext, + instance_id: &Uuid, + ) -> LookupResult { + let (.., db_instance) = LookupPath::new(opctx, &self.db_datastore) + .instance_id(*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. @@ -776,6 +788,18 @@ impl super::Nexus { Ok(db_interface) } + pub async fn network_interface_fetch_by_id( + &self, + opctx: &OpContext, + interface_id: &Uuid, + ) -> LookupResult { + let (.., db_interface) = LookupPath::new(opctx, &self.db_datastore) + .network_interface_id(*interface_id) + .fetch() + .await?; + Ok(db_interface) + } + /// Update a network interface for the given instance. pub async fn network_interface_update( &self, diff --git a/nexus/src/app/organization.rs b/nexus/src/app/organization.rs index bcb2e90cbf..d7783d2a7a 100644 --- a/nexus/src/app/organization.rs +++ b/nexus/src/app/organization.rs @@ -42,6 +42,18 @@ impl super::Nexus { 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, diff --git a/nexus/src/app/project.rs b/nexus/src/app/project.rs index f6fca8f82f..f5cb97255e 100644 --- a/nexus/src/app/project.rs +++ b/nexus/src/app/project.rs @@ -88,6 +88,18 @@ impl super::Nexus { 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, diff --git a/nexus/src/app/vpc.rs b/nexus/src/app/vpc.rs index fe6d58d4c8..af6017ccfe 100644 --- a/nexus/src/app/vpc.rs +++ b/nexus/src/app/vpc.rs @@ -209,6 +209,18 @@ impl super::Nexus { Ok(db_vpc) } + pub async fn vpc_fetch_by_id( + &self, + opctx: &OpContext, + vpc_id: &Uuid, + ) -> LookupResult { + let (.., db_vpc) = LookupPath::new(opctx, &self.db_datastore) + .vpc_id(*vpc_id) + .fetch() + .await?; + Ok(db_vpc) + } + pub async fn project_update_vpc( &self, opctx: &OpContext, diff --git a/nexus/src/app/vpc_router.rs b/nexus/src/app/vpc_router.rs index 20b38e4233..9af86c6ed2 100644 --- a/nexus/src/app/vpc_router.rs +++ b/nexus/src/app/vpc_router.rs @@ -96,6 +96,18 @@ impl super::Nexus { Ok(db_router) } + pub async fn vpc_router_fetch_by_id( + &self, + opctx: &OpContext, + vpc_router_id: &Uuid, + ) -> LookupResult { + let (.., db_router) = LookupPath::new(opctx, &self.db_datastore) + .vpc_router_id(*vpc_router_id) + .fetch() + .await?; + Ok(db_router) + } + pub async fn vpc_update_router( &self, opctx: &OpContext, @@ -223,6 +235,18 @@ impl super::Nexus { Ok(db_route) } + pub async fn route_fetch_by_id( + &self, + opctx: &OpContext, + route_id: &Uuid, + ) -> LookupResult { + let (.., db_route) = LookupPath::new(opctx, &self.db_datastore) + .router_route_id(*route_id) + .fetch() + .await?; + Ok(db_route) + } + #[allow(clippy::too_many_arguments)] pub async fn router_update_route( &self, diff --git a/nexus/src/app/vpc_subnet.rs b/nexus/src/app/vpc_subnet.rs index 84d7f458ee..26f9089c3e 100644 --- a/nexus/src/app/vpc_subnet.rs +++ b/nexus/src/app/vpc_subnet.rs @@ -218,6 +218,18 @@ impl super::Nexus { Ok(db_vpc) } + pub async fn vpc_subnet_fetch_by_id( + &self, + opctx: &OpContext, + vpc_subnet_id: &Uuid, + ) -> LookupResult { + let (.., db_vpc) = LookupPath::new(opctx, &self.db_datastore) + .vpc_subnet_id(*vpc_subnet_id) + .fetch() + .await?; + Ok(db_vpc) + } + pub async fn vpc_update_subnet( &self, opctx: &OpContext, diff --git a/nexus/src/authz/api_resources.rs b/nexus/src/authz/api_resources.rs index b684a608a5..89d3a947ab 100644 --- a/nexus/src/authz/api_resources.rs +++ b/nexus/src/authz/api_resources.rs @@ -600,6 +600,22 @@ authz_resource! { polar_snippet = InProject, } +authz_resource! { + name = "Image", + parent = "Project", + primary_key = Uuid, + roles_allowed = false, + polar_snippet = InProject, +} + +authz_resource! { + name = "Snapshot", + parent = "Project", + primary_key = Uuid, + roles_allowed = false, + polar_snippet = InProject, +} + authz_resource! { name = "Instance", parent = "Project", diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 45306acba0..1f3891a8c6 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -245,6 +245,28 @@ impl<'a> LookupPath<'a> { Disk { key: DiskKey::PrimaryKey(Root { lookup_root: self }, id) } } + /// Select a resource of type Image, identified by its id + pub fn image_id(self, id: Uuid) -> Image<'a> { + Image { key: ImageKey::PrimaryKey(Root { lookup_root: self }, id) } + } + + /// Select a resource of type Snapshot, identified by its id + pub fn snapshot_id(self, id: Uuid) -> Snapshot<'a> { + Snapshot { + key: SnapshotKey::PrimaryKey(Root { lookup_root: self }, id), + } + } + + /// Select a resource of type NetworkInterface, identified by its id + pub fn network_interface_id(self, id: Uuid) -> NetworkInterface<'a> { + NetworkInterface { + key: NetworkInterfaceKey::PrimaryKey( + Root { lookup_root: self }, + id, + ), + } + } + /// Select a resource of type Vpc, identified by its id pub fn vpc_id(self, id: Uuid) -> Vpc<'a> { Vpc { key: VpcKey::PrimaryKey(Root { lookup_root: self }, id) } @@ -533,6 +555,24 @@ lookup_resource! { primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] } +lookup_resource! { + name = "Image", + ancestors = [ "Silo", "Organization", "Project" ], + children = [], + lookup_by_name = true, + soft_deletes = true, + primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] +} + +lookup_resource! { + name = "Snapshot", + ancestors = [ "Silo", "Organization", "Project" ], + children = [], + lookup_by_name = true, + soft_deletes = true, + primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] +} + lookup_resource! { name = "Instance", ancestors = [ "Silo", "Organization", "Project" ], diff --git a/nexus/src/db/model/snapshot.rs b/nexus/src/db/model/snapshot.rs index 08b3ab44ee..6ea9b934de 100644 --- a/nexus/src/db/model/snapshot.rs +++ b/nexus/src/db/model/snapshot.rs @@ -25,9 +25,9 @@ pub struct Snapshot { #[diesel(embed)] identity: SnapshotIdentity, - project_id: Uuid, - disk_id: Uuid, - volume_id: Uuid, + pub project_id: Uuid, + pub disk_id: Uuid, + pub volume_id: Uuid, #[diesel(column_name = size_bytes)] pub size: ByteCount, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 8f97431c07..85f7c7158d 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -84,6 +84,7 @@ pub fn external_api() -> NexusApiDescription { api.register(organization_list)?; api.register(organization_create)?; api.register(organization_view)?; + api.register(organization_view_by_id)?; api.register(organization_delete)?; api.register(organization_update)?; api.register(organization_policy_view)?; @@ -92,6 +93,7 @@ pub fn external_api() -> NexusApiDescription { api.register(project_list)?; api.register(project_create)?; api.register(project_view)?; + api.register(project_view_by_id)?; api.register(project_delete)?; api.register(project_update)?; api.register(project_policy_view)?; @@ -110,11 +112,13 @@ pub fn external_api() -> NexusApiDescription { api.register(disk_list)?; api.register(disk_create)?; api.register(disk_view)?; + api.register(disk_view_by_id)?; api.register(disk_delete)?; api.register(instance_list)?; api.register(instance_create)?; api.register(instance_view)?; + api.register(instance_view_by_id)?; api.register(instance_delete)?; api.register(instance_migrate)?; api.register(instance_reboot)?; @@ -126,6 +130,7 @@ pub fn external_api() -> NexusApiDescription { api.register(image_list)?; api.register(image_create)?; api.register(image_view)?; + api.register(image_view_by_id)?; api.register(image_delete)?; api.register(instance_disk_list)?; @@ -135,16 +140,19 @@ pub fn external_api() -> NexusApiDescription { api.register(snapshot_list)?; api.register(snapshot_create)?; api.register(snapshot_view)?; + api.register(snapshot_view_by_id)?; api.register(snapshot_delete)?; api.register(vpc_list)?; api.register(vpc_create)?; api.register(vpc_view)?; + api.register(vpc_view_by_id)?; api.register(vpc_update)?; api.register(vpc_delete)?; api.register(vpc_subnet_list)?; api.register(vpc_subnet_view)?; + api.register(vpc_subnet_view_by_id)?; api.register(vpc_subnet_create)?; api.register(vpc_subnet_delete)?; api.register(vpc_subnet_update)?; @@ -153,17 +161,20 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_network_interface_create)?; api.register(instance_network_interface_list)?; api.register(instance_network_interface_view)?; + api.register(instance_network_interface_view_by_id)?; api.register(instance_network_interface_update)?; api.register(instance_network_interface_delete)?; api.register(vpc_router_list)?; api.register(vpc_router_view)?; + api.register(vpc_router_view_by_id)?; api.register(vpc_router_create)?; api.register(vpc_router_delete)?; api.register(vpc_router_update)?; api.register(vpc_router_route_list)?; api.register(vpc_router_route_view)?; + api.register(vpc_router_route_view_by_id)?; api.register(vpc_router_route_create)?; api.register(vpc_router_route_delete)?; api.register(vpc_router_route_update)?; @@ -207,6 +218,7 @@ pub fn external_api() -> NexusApiDescription { api.register(image_global_list)?; api.register(image_global_create)?; api.register(image_global_view)?; + api.register(image_global_view_by_id)?; api.register(image_global_delete)?; api.register(updates_refresh)?; @@ -255,6 +267,11 @@ pub fn external_api() -> NexusApiDescription { // DELETE /organizations/{org_name} (delete a organization in the collection) // PUT /organizations/{org_name} (update a organization in the collection) // +// An exception to this are id lookup operations which have a different top-level route +// but will still be grouped with the collection. For example: +// +// GET /by-id/organizations/{id} (look up a organization in the collection by id) +// // We pick a name for the function that implements a given API entrypoint // based on how we expect it to appear in the CLI subcommand hierarchy. For // example: @@ -301,6 +318,12 @@ async fn policy_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Path parameters for `/by-id/` endpoints +#[derive(Deserialize, JsonSchema)] +struct ByIdPathParams { + id: Uuid, +} + /// Update the top-level IAM policy #[endpoint { method = PUT, @@ -696,6 +719,28 @@ async fn organization_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get an organization by id +#[endpoint { + method = GET, + path = "/by-id/organizations/{id}", + tags = ["organizations"], +}] +async fn organization_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let organization = nexus.organization_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(organization.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete a specific organization. #[endpoint { method = DELETE, @@ -929,6 +974,28 @@ async fn project_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get a project by id +#[endpoint { + method = GET, + path = "/by-id/projects/{id}", + tags = ["projects"], +}] +async fn project_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let project = nexus.project_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(project.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete a specific project. #[endpoint { method = DELETE, @@ -1366,7 +1433,7 @@ struct DiskPathParam { disk_name: Name, } -/// Fetch a single disk in a project. +/// Get a single disk in a project. #[endpoint { method = GET, path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}", @@ -1392,6 +1459,28 @@ async fn disk_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get a disk by id +#[endpoint { + method = GET, + path = "/by-id/disks/{id}", + tags = ["disks"], +}] +async fn disk_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let disk = nexus.disk_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(disk.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete a disk from a project. #[endpoint { method = DELETE, @@ -1543,6 +1632,28 @@ async fn instance_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get an instance by id. +#[endpoint { + method = GET, + path = "/by-id/instances/{id}", + tags = ["instances"], +}] +async fn instance_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let instance = nexus.instance_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(instance.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete an instance from a project. #[endpoint { method = DELETE, @@ -1935,6 +2046,28 @@ async fn image_global_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get a global image by id. +#[endpoint { + method = GET, + path = "/by-id/global-images/{id}", + tags = ["images:global"], +}] +async fn image_global_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let image = nexus.global_image_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(image.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete a global image. /// /// Permanently delete a global image. This operation cannot be undone. Any @@ -2079,6 +2212,28 @@ async fn image_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Fetch an image by id +#[endpoint { + method = GET, + path = "/by-id/images/{id}", + tags = ["images"], +}] +async fn image_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let image = nexus.project_image_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(image.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete an image /// /// Permanently delete an image from a project. This operation cannot be undone. @@ -2272,6 +2427,29 @@ async fn instance_network_interface_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get an instance's network interface by id. +#[endpoint { + method = GET, + path = "/by-id/network-interfaces/{id}", + tags = ["instances"], +}] +async fn instance_network_interface_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let network_interface = + nexus.network_interface_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(network_interface.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Update information about an instance's network interface #[endpoint { method = PUT, @@ -2421,6 +2599,28 @@ async fn snapshot_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get a snapshot by id. +#[endpoint { + method = GET, + path = "/by-id/snapshots/{id}", + tags = ["snapshots"], +}] +async fn snapshot_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let snapshot = nexus.snapshot_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(snapshot.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete a snapshot from a project. #[endpoint { method = DELETE, @@ -2529,6 +2729,28 @@ async fn vpc_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get a VPC by id. +#[endpoint { + method = GET, + path = "/by-id/vpcs/{id}", + tags = ["vpcs"], +}] +async fn vpc_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let vpc = nexus.vpc_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(vpc.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a VPC in a project. #[endpoint { method = POST, @@ -2699,6 +2921,28 @@ async fn vpc_subnet_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get a VPC subnet by id. +#[endpoint { + method = GET, + path = "/by-id/vpc-subnets/{id}", + tags = ["vpcs"], +}] +async fn vpc_subnet_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let subnet = nexus.vpc_subnet_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(subnet.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a subnet in a VPC. #[endpoint { method = POST, @@ -2978,6 +3222,28 @@ async fn vpc_router_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get a VPC Router by id +#[endpoint { + method = GET, + path = "/by-id/vpc-routers/{id}", + tags = ["vpcs"], +}] +async fn vpc_router_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let router = nexus.vpc_router_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(router.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a VPC Router #[endpoint { method = POST, @@ -3151,6 +3417,28 @@ async fn vpc_router_route_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Get a vpc router route by id +#[endpoint { + method = GET, + path = "/by-id/vpc-router-routes/{id}", + tags = ["vpcs"] +}] +async fn vpc_router_route_view_by_id( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let id = &path.id; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let route = nexus.route_fetch_by_id(&opctx, id).await?; + Ok(HttpResponseOk(route.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a VPC Router #[endpoint { method = POST, diff --git a/nexus/test-utils/src/http_testing.rs b/nexus/test-utils/src/http_testing.rs index 03122218bc..ac00ddbc29 100644 --- a/nexus/test-utils/src/http_testing.rs +++ b/nexus/test-utils/src/http_testing.rs @@ -427,7 +427,7 @@ where } /// Represents a response from an HTTP server -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct TestResponse { pub status: http::StatusCode, pub headers: http::HeaderMap, diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index cbb1dfd6ca..e73c69851f 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -579,6 +579,15 @@ lazy_static! { ) ], }, + + VerifyEndpoint { + url: "/by-id/organizations/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_ORG_URL, visibility: Visibility::Protected, @@ -595,6 +604,7 @@ lazy_static! { ), ], }, + VerifyEndpoint { url: &*DEMO_ORG_POLICY_URL, visibility: Visibility::Protected, @@ -630,6 +640,15 @@ lazy_static! { ), ], }, + + VerifyEndpoint { + url: "/by-id/projects/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_PROJECT_URL, visibility: Visibility::Protected, @@ -646,6 +665,7 @@ lazy_static! { ), ], }, + VerifyEndpoint { url: &*DEMO_PROJECT_POLICY_URL, visibility: Visibility::Protected, @@ -673,6 +693,14 @@ lazy_static! { ], }, + VerifyEndpoint { + url: "/by-id/vpcs/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_VPC_URL, visibility: Visibility::Protected, @@ -717,6 +745,14 @@ lazy_static! { ], }, + VerifyEndpoint { + url: "/by-id/vpc-subnets/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_VPC_SUBNET_URL, visibility: Visibility::Protected, @@ -755,6 +791,14 @@ lazy_static! { ], }, + VerifyEndpoint { + url: "/by-id/vpc-routers/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_VPC_ROUTER_URL, visibility: Visibility::Protected, @@ -785,6 +829,14 @@ lazy_static! { ], }, + VerifyEndpoint { + url: "/by-id/vpc-router-routes/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_ROUTER_ROUTE_URL, visibility: Visibility::Protected, @@ -820,6 +872,14 @@ lazy_static! { ], }, + VerifyEndpoint { + url: "/by-id/disks/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_DISK_URL, visibility: Visibility::Protected, @@ -872,6 +932,14 @@ lazy_static! { ], }, + VerifyEndpoint { + url: "/by-id/images/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::GetUnimplemented, + ], + }, + VerifyEndpoint { url: &*DEMO_PROJECT_IMAGE_URL, visibility: Visibility::Protected, @@ -893,6 +961,15 @@ lazy_static! { ) ] }, + + VerifyEndpoint { + url: "/by-id/snapshots/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::GetUnimplemented, + ], + }, + VerifyEndpoint { url: &*DEMO_SNAPSHOT_URL, visibility: Visibility::Protected, @@ -914,6 +991,14 @@ lazy_static! { ], }, + VerifyEndpoint { + url: "/by-id/instances/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_INSTANCE_URL, visibility: Visibility::Protected, @@ -974,6 +1059,15 @@ lazy_static! { ), ], }, + + VerifyEndpoint { + url: "/by-id/network-interfaces/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_INSTANCE_NIC_URL, visibility: Visibility::Protected, @@ -1081,6 +1175,14 @@ lazy_static! { ], }, + VerifyEndpoint { + url: "/by-id/global-images/{id}", + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + VerifyEndpoint { url: &*DEMO_GLOBAL_IMAGE_URL, visibility: Visibility::Protected, diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index d83f63c64c..32504da9cf 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -21,6 +21,7 @@ use nexus_test_utils::http_testing::TestResponse; use nexus_test_utils::resource_helpers::DiskTest; use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; +use omicron_common::api::external::IdentityMetadata; use omicron_nexus::authn::external::spoof; // This test hits a list Nexus API endpoints using both unauthenticated and @@ -55,22 +56,44 @@ async fn test_unauthorized(cptestctx: &ControlPlaneTestContext) { DiskTest::new(cptestctx).await; let client = &cptestctx.external_client; let log = &cptestctx.logctx.log; + let mut setup_results = std::collections::BTreeMap::new(); // Create test data. info!(log, "setting up resource hierarchy"); for request in &*SETUP_REQUESTS { - NexusRequest::objects_post(client, request.url, &request.body) - .authn_as(AuthnMode::PrivilegedUser) - .execute() - .await - .unwrap(); + let (url, result, id_routes) = match request { + SetupReq::Get { url, id_routes } => ( + url, + NexusRequest::object_get(client, url) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(), + id_routes, + ), + SetupReq::Post { url, body, id_routes } => ( + url, + NexusRequest::objects_post(client, url, body) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(), + id_routes, + ), + }; + + setup_results.insert(url, result.clone()); + id_routes.iter().for_each(|id_route| { + setup_results.insert(id_route, result.clone()); + }); } // Verify the hardcoded endpoints. info!(log, "verifying endpoints"); print!("{}", VERIFY_HEADER); for endpoint in &*VERIFY_ENDPOINTS { - verify_endpoint(&log, client, endpoint).await; + let setup_response = setup_results.get(&endpoint.url); + verify_endpoint(&log, client, endpoint, setup_response).await; } } @@ -114,13 +137,22 @@ G GET PUT POST DEL TRCE G URL /// Describes a request made during the setup phase to create a resource that /// we'll use later in the verification phase /// -/// The setup phase takes a list of `SetupReq` structs and issues `POST` -/// requests to each one's `url` with the specific `body`. -struct SetupReq { - /// url to send the `POST` to - url: &'static str, - /// body of the `POST` request - body: serde_json::Value, +/// The setup phase takes a list of `SetupReq` enums and issues a `GET` or `POST` +/// request to each one's `url`. `id_results` is a list of URLs that are associated +/// to the results of the setup request with any `{id}` params in the URL replaced with +/// the result's URL. This is used to later verify ID endpoints without first having to +/// know the ID. + +enum SetupReq { + Get { + url: &'static str, + id_routes: Vec<&'static str>, + }, + Post { + url: &'static str, + body: serde_json::Value, + id_routes: Vec<&'static str>, + }, } lazy_static! { @@ -152,69 +184,87 @@ lazy_static! { /// List of requests to execute at setup time static ref SETUP_REQUESTS: Vec = vec![ // Create a separate Silo (not used for anything else) - SetupReq { + SetupReq::Post { url: "/silos", body: serde_json::to_value(&*DEMO_SILO_CREATE).unwrap(), + id_routes: vec!["/by-id/silos/{id}"], }, // Create an IP pool - SetupReq { + SetupReq::Post { url: &*DEMO_IP_POOLS_URL, body: serde_json::to_value(&*DEMO_IP_POOL_CREATE).unwrap(), + id_routes: vec!["/by-id/ip-pools/{id}"], }, // Create an IP Pool range - SetupReq { + SetupReq::Post { url: &*DEMO_IP_POOL_RANGES_ADD_URL, body: serde_json::to_value(&*DEMO_IP_POOL_RANGE).unwrap(), + id_routes: vec![], }, // Create an Organization - SetupReq { + SetupReq::Post { url: "/organizations", - body: serde_json::to_value(&*DEMO_ORG_CREATE).unwrap() + body: serde_json::to_value(&*DEMO_ORG_CREATE).unwrap(), + id_routes: vec!["/by-id/organizations/{id}"], }, // Create a Project in the Organization - SetupReq { + SetupReq::Post { url: &*DEMO_ORG_PROJECTS_URL, body: serde_json::to_value(&*DEMO_PROJECT_CREATE).unwrap(), + id_routes: vec!["/by-id/projects/{id}"], }, // Create a VPC in the Project - SetupReq { + SetupReq::Post { url: &*DEMO_PROJECT_URL_VPCS, body: serde_json::to_value(&*DEMO_VPC_CREATE).unwrap(), + id_routes: vec!["/by-id/vpcs/{id}"], }, // Create a VPC Subnet in the Vpc - SetupReq { + SetupReq::Post { url: &*DEMO_VPC_URL_SUBNETS, body: serde_json::to_value(&*DEMO_VPC_SUBNET_CREATE).unwrap(), + id_routes: vec!["/by-id/vpc-subnets/{id}"], }, // Create a VPC Router in the Vpc - SetupReq { + SetupReq::Post { url: &*DEMO_VPC_URL_ROUTERS, body: serde_json::to_value(&*DEMO_VPC_ROUTER_CREATE).unwrap(), + id_routes: vec!["/by-id/vpc-routers/{id}"], }, // Create a VPC Router in the Vpc - SetupReq { + SetupReq::Post { url: &*DEMO_VPC_ROUTER_URL_ROUTES, body: serde_json::to_value(&*DEMO_ROUTER_ROUTE_CREATE).unwrap(), + id_routes: vec!["/by-id/vpc-router-routes/{id}"], }, // Create a Disk in the Project - SetupReq { + SetupReq::Post { url: &*DEMO_PROJECT_URL_DISKS, body: serde_json::to_value(&*DEMO_DISK_CREATE).unwrap(), + id_routes: vec!["/by-id/disks/{id}"], }, // Create an Instance in the Project - SetupReq { + SetupReq::Post { url: &*DEMO_PROJECT_URL_INSTANCES, body: serde_json::to_value(&*DEMO_INSTANCE_CREATE).unwrap(), + id_routes: vec!["/by-id/instances/{id}"], + }, + // Lookup the previously created NIC + SetupReq::Get { + url: &*DEMO_INSTANCE_NIC_URL, + id_routes: vec!["/by-id/network-interfaces/{id}"], }, // Create a GlobalImage - SetupReq { + SetupReq::Post { url: "/images", body: serde_json::to_value(&*DEMO_GLOBAL_IMAGE_CREATE).unwrap(), + id_routes: vec!["/by-id/global-images/{id}"], }, // Create a SAML identity provider - SetupReq { + SetupReq::Post { url: &*SAML_IDENTITY_PROVIDERS_URL, body: serde_json::to_value(&*SAML_IDENTITY_PROVIDER).unwrap(), + id_routes: vec![], }, ]; } @@ -273,6 +323,7 @@ async fn verify_endpoint( log: &slog::Logger, client: &ClientTestContext, endpoint: &VerifyEndpoint, + setup_response: Option<&TestResponse>, ) { let log = log.new(o!("url" => endpoint.url)); info!(log, "test: begin endpoint"); @@ -288,6 +339,26 @@ async fn verify_endpoint( Visibility::Protected => StatusCode::NOT_FOUND, }; + // For routes with an id param, replace the id param with the setup response if present. + let uri = if endpoint.url.contains("{id}") { + match setup_response { + Some(response) => endpoint.url.replace( + "{id}", + response + .parsed_body::() + .unwrap() + .id + .to_string() + .as_str(), + ), + None => endpoint + .url + .replace("{id}", "00000000-0000-0000-0000-000000000000"), + } + } else { + endpoint.url.to_string() + }; + // Make one GET request as an authorized user to make sure we get a "200 OK" // response. Otherwise, the test might later succeed by coincidence. We // might find a 404 because of something that actually doesn't exist rather @@ -302,7 +373,7 @@ async fn verify_endpoint( &http::StatusCode::OK, ))); Some( - NexusRequest::object_get(client, endpoint.url) + NexusRequest::object_get(client, uri.as_str()) .authn_as(AuthnMode::PrivilegedUser) .execute() .await @@ -319,7 +390,7 @@ async fn verify_endpoint( client, expected_status, http::Method::GET, - endpoint.url, + uri.as_str(), ) .authn_as(AuthnMode::PrivilegedUser) .execute() @@ -356,7 +427,7 @@ async fn verify_endpoint( None => StatusCode::METHOD_NOT_ALLOWED, }; let response = NexusRequest::new( - RequestBuilder::new(client, method.clone(), endpoint.url) + RequestBuilder::new(client, method.clone(), uri.as_str()) .body(body.as_ref()) .expect_status(Some(expected_status)), ) @@ -374,7 +445,7 @@ async fn verify_endpoint( None => StatusCode::METHOD_NOT_ALLOWED, }; let response = - RequestBuilder::new(client, method.clone(), endpoint.url) + RequestBuilder::new(client, method.clone(), uri.as_str()) .body(body.as_ref()) .expect_status(Some(expected_status)) .execute() @@ -402,7 +473,7 @@ async fn verify_endpoint( info!(log, "test: bogus creds: bad actor"; "method" => ?method); let bad_actor_authn_header = &spoof::SPOOF_HEADER_BAD_ACTOR; let response = - RequestBuilder::new(client, method.clone(), endpoint.url) + RequestBuilder::new(client, method.clone(), uri.as_str()) .body(body.as_ref()) .expect_status(Some(expected_status)) .header( @@ -419,7 +490,7 @@ async fn verify_endpoint( info!(log, "test: bogus creds: bad cred syntax"; "method" => ?method); let bad_creds_authn_header = &spoof::SPOOF_HEADER_BAD_CREDS; let response = - RequestBuilder::new(client, method.clone(), endpoint.url) + RequestBuilder::new(client, method.clone(), uri.as_str()) .body(body.as_ref()) .expect_status(Some(expected_status)) .header( @@ -450,7 +521,7 @@ async fn verify_endpoint( info!(log, "test: compare current resource content with earlier"); if let Some(resource_before) = resource_before { let resource_after: serde_json::Value = - NexusRequest::object_get(client, endpoint.url) + NexusRequest::object_get(client, uri.as_str()) .authn_as(AuthnMode::PrivilegedUser) .execute() .await diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 8961f214ca..c3ffb50c95 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -4,6 +4,7 @@ disk_create /organizations/{organization_name}/proj disk_delete /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} disk_list /organizations/{organization_name}/projects/{project_name}/disks disk_view /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} +disk_view_by_id /by-id/disks/{id} API operations found with tag "hardware" OPERATION ID URL PATH @@ -27,6 +28,7 @@ image_create /organizations/{organization_name}/proj image_delete /organizations/{organization_name}/projects/{project_name}/images/{image_name} image_list /organizations/{organization_name}/projects/{project_name}/images image_view /organizations/{organization_name}/projects/{project_name}/images/{image_name} +image_view_by_id /by-id/images/{id} API operations found with tag "images:global" OPERATION ID URL PATH @@ -34,6 +36,7 @@ image_global_create /images image_global_delete /images/{image_name} image_global_list /images image_global_view /images/{image_name} +image_global_view_by_id /by-id/global-images/{id} API operations found with tag "instances" OPERATION ID URL PATH @@ -49,11 +52,13 @@ instance_network_interface_delete /organizations/{organization_name}/proj instance_network_interface_list /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces instance_network_interface_update /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name} 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_serial_console /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console instance_start /organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/start 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} API operations found with tag "ip-pools" OPERATION ID URL PATH @@ -84,6 +89,7 @@ organization_policy_update /organizations/{organization_name}/poli organization_policy_view /organizations/{organization_name}/policy organization_update /organizations/{organization_name} organization_view /organizations/{organization_name} +organization_view_by_id /by-id/organizations/{id} API operations found with tag "policy" OPERATION ID URL PATH @@ -99,6 +105,7 @@ project_policy_update /organizations/{organization_name}/proj project_policy_view /organizations/{organization_name}/projects/{project_name}/policy project_update /organizations/{organization_name}/projects/{project_name} project_view /organizations/{organization_name}/projects/{project_name} +project_view_by_id /by-id/projects/{id} API operations found with tag "roles" OPERATION ID URL PATH @@ -136,6 +143,7 @@ snapshot_create /organizations/{organization_name}/proj snapshot_delete /organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name} snapshot_list /organizations/{organization_name}/projects/{project_name}/snapshots snapshot_view /organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name} +snapshot_view_by_id /by-id/snapshots/{id} API operations found with tag "system" OPERATION ID URL PATH @@ -161,14 +169,18 @@ vpc_router_route_delete /organizations/{organization_name}/proj vpc_router_route_list /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes vpc_router_route_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name} vpc_router_route_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name} +vpc_router_route_view_by_id /by-id/vpc-router-routes/{id} vpc_router_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name} vpc_router_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name} +vpc_router_view_by_id /by-id/vpc-routers/{id} vpc_subnet_create /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets vpc_subnet_delete /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} vpc_subnet_list /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets vpc_subnet_list_network_interfaces /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/network-interfaces vpc_subnet_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} vpc_subnet_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name} +vpc_subnet_view_by_id /by-id/vpc-subnets/{id} vpc_update /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name} vpc_view /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name} +vpc_view_by_id /by-id/vpcs/{id} diff --git a/openapi/nexus.json b/openapi/nexus.json index 10f926b221..44381f7a05 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -10,6 +10,474 @@ "version": "0.0.1" }, "paths": { + "/by-id/disks/{id}": { + "get": { + "tags": [ + "disks" + ], + "summary": "Get a disk by id", + "operationId": "disk_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Disk" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/by-id/global-images/{id}": { + "get": { + "tags": [ + "images:global" + ], + "summary": "Get a global image by id.", + "operationId": "image_global_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalImage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/by-id/images/{id}": { + "get": { + "tags": [ + "images" + ], + "summary": "Fetch an image by id", + "operationId": "image_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/by-id/instances/{id}": { + "get": { + "tags": [ + "instances" + ], + "summary": "Get an instance by id.", + "operationId": "instance_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "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" + } + } + } + }, + "/by-id/network-interfaces/{id}": { + "get": { + "tags": [ + "instances" + ], + "summary": "Get an instance's network interface by id.", + "operationId": "instance_network_interface_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkInterface" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/by-id/organizations/{id}": { + "get": { + "tags": [ + "organizations" + ], + "summary": "Get an organization by id", + "operationId": "organization_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "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" + } + } + } + }, + "/by-id/projects/{id}": { + "get": { + "tags": [ + "projects" + ], + "summary": "Get a project by id", + "operationId": "project_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/by-id/snapshots/{id}": { + "get": { + "tags": [ + "snapshots" + ], + "summary": "Get a snapshot by id.", + "operationId": "snapshot_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Snapshot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/by-id/vpc-router-routes/{id}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Get a vpc router route by id", + "operationId": "vpc_router_route_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouterRoute" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/by-id/vpc-routers/{id}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Get a VPC Router by id", + "operationId": "vpc_router_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcRouter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/by-id/vpc-subnets/{id}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Get a VPC subnet by id.", + "operationId": "vpc_subnet_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcSubnet" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/by-id/vpcs/{id}": { + "get": { + "tags": [ + "vpcs" + ], + "summary": "Get a VPC by id.", + "operationId": "vpc_view_by_id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vpc" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/device/auth": { "post": { "tags": [ @@ -1674,7 +2142,7 @@ "tags": [ "disks" ], - "summary": "Fetch a single disk in a project.", + "summary": "Get a single disk in a project.", "operationId": "disk_view", "parameters": [ {