Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,9 @@ CREATE TABLE omicron.public.project (
/* Indicates that the object has been deleted */
time_deleted TIMESTAMPTZ,

/* child resource generation number, per RFD 192 */
rcgen INT NOT NULL,

/* Which organization this project belongs to */
organization_id UUID NOT NULL /* foreign key into "Organization" table */
);
Expand Down
20 changes: 18 additions & 2 deletions end-to-end-tests/src/helpers/ctx.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::helpers::generate_name;
use anyhow::{Context as _, Result};
use omicron_sled_agent::params::ServiceType;
use omicron_sled_agent::rack_setup::config::SetupServiceConfig;
use oxide_client::types::{Name, OrganizationCreate, ProjectCreate};
use oxide_client::{Client, ClientOrganizationsExt, ClientProjectsExt};
use oxide_client::{
Client, ClientOrganizationsExt, ClientProjectsExt, ClientVpcsExt,
};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use reqwest::Url;
use std::net::SocketAddr;
Expand Down Expand Up @@ -50,6 +51,21 @@ impl Context {
}

pub async fn cleanup(self) -> Result<()> {
self.client
.vpc_subnet_delete()
.organization_name(self.org_name.clone())
.project_name(self.project_name.clone())
.vpc_name("default")
.subnet_name("default")
.send()
.await?;
self.client
.vpc_delete()
.organization_name(self.org_name.clone())
.project_name(self.project_name.clone())
.vpc_name("default")
.send()
.await?;
self.client
.project_delete()
.organization_name(self.org_name.clone())
Expand Down
20 changes: 19 additions & 1 deletion end-to-end-tests/src/instance_launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ async fn instance_launch() -> Result<()> {
.id;

eprintln!("create disk");
let disk_name = generate_name("disk")?;
let disk_name = ctx
.client
.disk_create()
.organization_name(ctx.org_name.clone())
.project_name(ctx.project_name.clone())
.body(DiskCreate {
name: generate_name("disk")?,
name: disk_name.clone(),
description: String::new(),
disk_source: DiskSource::GlobalImage { image_id },
size: ByteCount(2048 * 1024 * 1024),
Expand Down Expand Up @@ -245,6 +246,23 @@ async fn instance_launch() -> Result<()> {
)
.await?;

eprintln!("deleting disk");
wait_for_condition(
|| async {
ctx.client
.disk_delete()
.organization_name(ctx.org_name.clone())
.project_name(ctx.project_name.clone())
.disk_name(disk_name)
.send()
.await
.map_err(|_| CondCheckError::<oxide_client::Error>::NotYet)
},
&Duration::from_secs(1),
&Duration::from_secs(60),
)
.await?;

ctx.cleanup().await
}

Expand Down
67 changes: 65 additions & 2 deletions nexus/db-model/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use super::Name;
use crate::schema::project;
use super::{
Disk, ExternalIp, Generation, Image, Instance, IpPool, Name, Snapshot, Vpc,
};
use crate::collection::DatastoreCollectionConfig;
use crate::schema::{
disk, external_ip, image, instance, ip_pool, project, snapshot, vpc,
};
use chrono::{DateTime, Utc};
use db_macros::Resource;
use nexus_types::external_api::params;
Expand All @@ -18,6 +23,8 @@ pub struct Project {
#[diesel(embed)]
identity: ProjectIdentity,

/// child resource generation number, per RFD 192
pub rcgen: Generation,
pub organization_id: Uuid,
}

Expand All @@ -26,6 +33,7 @@ impl Project {
pub fn new(organization_id: Uuid, params: params::ProjectCreate) -> Self {
Self {
identity: ProjectIdentity::new(Uuid::new_v4(), params.identity),
rcgen: Generation::new(),
organization_id,
}
}
Expand All @@ -40,6 +48,61 @@ impl From<Project> for views::Project {
}
}

impl DatastoreCollectionConfig<Instance> for Project {
type CollectionId = Uuid;
type GenerationNumberColumn = project::dsl::rcgen;
type CollectionTimeDeletedColumn = project::dsl::time_deleted;
type CollectionIdColumn = instance::dsl::project_id;
}

impl DatastoreCollectionConfig<Disk> for Project {
type CollectionId = Uuid;
type GenerationNumberColumn = project::dsl::rcgen;
type CollectionTimeDeletedColumn = project::dsl::time_deleted;
type CollectionIdColumn = disk::dsl::project_id;
}

impl DatastoreCollectionConfig<Image> for Project {
type CollectionId = Uuid;
type GenerationNumberColumn = project::dsl::rcgen;
type CollectionTimeDeletedColumn = project::dsl::time_deleted;
type CollectionIdColumn = image::dsl::project_id;
}

impl DatastoreCollectionConfig<Snapshot> for Project {
type CollectionId = Uuid;
type GenerationNumberColumn = project::dsl::rcgen;
type CollectionTimeDeletedColumn = project::dsl::time_deleted;
type CollectionIdColumn = snapshot::dsl::project_id;
}

impl DatastoreCollectionConfig<Vpc> for Project {
type CollectionId = Uuid;
type GenerationNumberColumn = project::dsl::rcgen;
type CollectionTimeDeletedColumn = project::dsl::time_deleted;
type CollectionIdColumn = vpc::dsl::project_id;
}

// NOTE: "IpPoolRange" also contains a reference to "project_id", but
// ranges should only exist within IP Pools.
impl DatastoreCollectionConfig<IpPool> for Project {
type CollectionId = Uuid;
type GenerationNumberColumn = project::dsl::rcgen;
type CollectionTimeDeletedColumn = project::dsl::time_deleted;
type CollectionIdColumn = ip_pool::dsl::project_id;
}

// TODO(https://github.com/oxidecomputer/omicron/issues/1482): Not yet utilized,
// but needed for project deletion safety.
// TODO(https://github.com/oxidecomputer/omicron/issues/1334): Cannot be
// utilized until floating IPs are implemented.
impl DatastoreCollectionConfig<ExternalIp> for Project {
type CollectionId = Uuid;
type GenerationNumberColumn = project::dsl::rcgen;
type CollectionTimeDeletedColumn = project::dsl::time_deleted;
type CollectionIdColumn = external_ip::dsl::project_id;
}

/// Describes a set of updates for the [`Project`] model.
#[derive(AsChangeset)]
#[diesel(table_name = project)]
Expand Down
1 change: 1 addition & 0 deletions nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ table! {
time_created -> Timestamptz,
time_modified -> Timestamptz,
time_deleted -> Nullable<Timestamptz>,
rcgen -> Int8,
organization_id -> Uuid,
}
}
Expand Down
4 changes: 4 additions & 0 deletions nexus/src/app/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ impl super::Nexus {
.project_name(project_name)
.lookup_for(authz::Action::CreateChild)
.await?;

// TODO(https://github.com/oxidecomputer/omicron/issues/1482): When
// we implement this, remember to insert the image within a Project
// using "insert_resource".
Err(self.unimplemented_todo(opctx, Unimpl::Public).await)
}

Expand Down
15 changes: 9 additions & 6 deletions nexus/src/app/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,15 @@ impl super::Nexus {
organization_name: &Name,
project_name: &Name,
) -> DeleteResult {
let (.., authz_project) = LookupPath::new(opctx, &self.db_datastore)
.organization_name(organization_name)
.project_name(project_name)
.lookup_for(authz::Action::Delete)
.await?;
self.db_datastore.project_delete(opctx, &authz_project).await
let (.., authz_project, db_project) =
LookupPath::new(opctx, &self.db_datastore)
.organization_name(organization_name)
.project_name(project_name)
.fetch()
.await?;
self.db_datastore
.project_delete(opctx, &authz_project, &db_project)
.await
}

// Role assignments
Expand Down
31 changes: 22 additions & 9 deletions nexus/src/db/datastore/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use crate::db::collection_attach::AttachError;
use crate::db::collection_attach::DatastoreAttachTarget;
use crate::db::collection_detach::DatastoreDetachTarget;
use crate::db::collection_detach::DetachError;
use crate::db::collection_insert::AsyncInsertError;
use crate::db::collection_insert::DatastoreCollection;
use crate::db::error::public_error_from_diesel_pool;
use crate::db::error::ErrorHandler;
use crate::db::identity::Resource;
Expand All @@ -21,6 +23,7 @@ use crate::db::model::Disk;
use crate::db::model::DiskRuntimeState;
use crate::db::model::Instance;
use crate::db::model::Name;
use crate::db::model::Project;
use crate::db::pagination::paginated;
use crate::db::update_and_check::UpdateAndCheck;
use crate::db::update_and_check::UpdateStatus;
Expand Down Expand Up @@ -64,19 +67,29 @@ impl DataStore {

let gen = disk.runtime().gen;
let name = disk.name().clone();
let disk: Disk = diesel::insert_into(dsl::disk)
.values(disk)
.on_conflict(dsl::id)
.do_nothing()
.returning(Disk::as_returning())
.get_result_async(self.pool())
.await
.map_err(|e| {
let project_id = disk.project_id;

let disk: Disk = Project::insert_resource(
project_id,
diesel::insert_into(dsl::disk)
.values(disk)
.on_conflict(dsl::id)
.do_nothing(),
)
.insert_and_get_result_async(self.pool())
.await
.map_err(|e| match e {
AsyncInsertError::CollectionNotFound => Error::ObjectNotFound {
type_name: ResourceType::Project,
lookup_type: LookupType::ById(project_id),
},
AsyncInsertError::DatabaseError(e) => {
public_error_from_diesel_pool(
e,
ErrorHandler::Conflict(ResourceType::Disk, name.as_str()),
)
})?;
}
})?;

let runtime = disk.runtime();
bail_unless!(
Expand Down
31 changes: 22 additions & 9 deletions nexus/src/db/datastore/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ use crate::context::OpContext;
use crate::db;
use crate::db::collection_detach_many::DatastoreDetachManyTarget;
use crate::db::collection_detach_many::DetachManyError;
use crate::db::collection_insert::AsyncInsertError;
use crate::db::collection_insert::DatastoreCollection;
use crate::db::error::public_error_from_diesel_pool;
use crate::db::error::ErrorHandler;
use crate::db::identity::Resource;
use crate::db::lookup::LookupPath;
use crate::db::model::Instance;
use crate::db::model::InstanceRuntimeState;
use crate::db::model::Name;
use crate::db::model::Project;
use crate::db::pagination::paginated;
use crate::db::update_and_check::UpdateAndCheck;
use crate::db::update_and_check::UpdateStatus;
Expand Down Expand Up @@ -66,22 +69,32 @@ impl DataStore {

let gen = instance.runtime().gen;
let name = instance.name().clone();
let instance: Instance = diesel::insert_into(dsl::instance)
.values(instance)
.on_conflict(dsl::id)
.do_nothing()
.returning(Instance::as_returning())
.get_result_async(self.pool())
.await
.map_err(|e| {
let project_id = instance.project_id;

let instance: Instance = Project::insert_resource(
project_id,
diesel::insert_into(dsl::instance)
.values(instance)
.on_conflict(dsl::id)
.do_nothing(),
)
.insert_and_get_result_async(self.pool())
.await
.map_err(|e| match e {
AsyncInsertError::CollectionNotFound => Error::ObjectNotFound {
type_name: ResourceType::Project,
lookup_type: LookupType::ById(project_id),
},
AsyncInsertError::DatabaseError(e) => {
public_error_from_diesel_pool(
e,
ErrorHandler::Conflict(
ResourceType::Instance,
name.as_str(),
),
)
})?;
}
})?;

bail_unless!(
instance.runtime().state.state()
Expand Down
47 changes: 38 additions & 9 deletions nexus/src/db/datastore/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::db::model::IpPool;
use crate::db::model::IpPoolRange;
use crate::db::model::IpPoolUpdate;
use crate::db::model::Name;
use crate::db::model::Project;
use crate::db::pagination::paginated;
use crate::db::queries::ip_pool::FilterOverlappingIpRanges;
use crate::external_api::params;
Expand Down Expand Up @@ -158,17 +159,45 @@ impl DataStore {
};
let pool = IpPool::new(&new_pool.identity, project_id, rack_id);
let pool_name = pool.name().as_str().to_string();
diesel::insert_into(dsl::ip_pool)
.values(pool)
.returning(IpPool::as_returning())
.get_result_async(self.pool_authorized(opctx).await?)

if let Some(project_id) = project_id {
Project::insert_resource(
project_id,
diesel::insert_into(dsl::ip_pool).values(pool),
)
.insert_and_get_result_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
public_error_from_diesel_pool(
e,
ErrorHandler::Conflict(ResourceType::IpPool, &pool_name),
)
.map_err(|e| match e {
AsyncInsertError::CollectionNotFound => Error::ObjectNotFound {
type_name: ResourceType::Project,
lookup_type: LookupType::ById(project_id),
},
AsyncInsertError::DatabaseError(e) => {
public_error_from_diesel_pool(
e,
ErrorHandler::Conflict(
ResourceType::IpPool,
&pool_name,
),
)
}
})
} else {
diesel::insert_into(dsl::ip_pool)
.values(pool)
.returning(IpPool::as_returning())
.get_result_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
public_error_from_diesel_pool(
e,
ErrorHandler::Conflict(
ResourceType::IpPool,
&pool_name,
),
)
})
}
}

pub async fn ip_pool_delete(
Expand Down
Loading