Skip to content

[nexus] Safer project deletion #2024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
19 changes: 18 additions & 1 deletion end-to-end-tests/src/helpers/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use crate::helpers::generate_name;
use anyhow::{Context as _, Result};
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 @@ -49,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
24 changes: 22 additions & 2 deletions 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 All @@ -89,7 +90,9 @@ async fn instance_launch() -> Result<()> {
hostname: "localshark".into(), // 🦈
memory: ByteCount(1024 * 1024 * 1024),
ncpus: InstanceCpuCount(2),
disks: vec![InstanceDiskAttachment::Attach { name: disk_name }],
disks: vec![InstanceDiskAttachment::Attach {
name: disk_name.clone(),
}],
network_interfaces: InstanceNetworkInterfaceAttachment::Default,
external_ips: vec![ExternalIpCreate::Ephemeral { pool_name: None }],
user_data: String::new(),
Expand Down Expand Up @@ -245,6 +248,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.clone())
.send()
.await
.map_err(|_| CondCheckError::<oxide_client::Error>::NotYet)
},
&Duration::from_secs(1),
&Duration::from_secs(60),
)
.await?;

ctx.cleanup().await
}

Expand Down
1 change: 1 addition & 0 deletions nexus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ oso = "0.26"
oximeter-client = { path = "../oximeter-client" }
oximeter-db = { path = "../oximeter/db/" }
parse-display = "0.7.0"
paste = "1.0"
# See omicron-rpaths for more about the "pq-sys" dependency.
pq-sys = "*"
propolis-client = { git = "https://github.com/oxidecomputer/propolis", rev = "b596e72b6b93bc412d675358f782ae7d53f8bf7a", features = [ "generated" ] }
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 @@ -197,12 +197,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_for(authz::Action::Delete)
.await?;
self.db_datastore
.project_delete(opctx, &authz_project, &db_project)
.await
}

// Role assignments
Expand Down
8 changes: 7 additions & 1 deletion nexus/src/app/sagas/disk_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,15 @@ async fn sdc_create_disk_record(
ActionError::action_failed(Error::invalid_request(&e.to_string()))
})?;

let (.., authz_project) = LookupPath::new(&opctx, &osagactx.datastore())
.project_id(params.project_id)
.lookup_for(authz::Action::CreateChild)
.await
.map_err(ActionError::action_failed)?;

let disk_created = osagactx
.datastore()
.project_create_disk(disk)
.project_create_disk(&opctx, &authz_project, disk)
.await
.map_err(ActionError::action_failed)?;

Expand Down
9 changes: 8 additions & 1 deletion nexus/src/app/sagas/instance_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,7 @@ async fn sic_create_instance_record(
) -> Result<db::model::Name, ActionError> {
let osagactx = sagactx.user_data();
let params = sagactx.saga_params::<Params>()?;
let opctx = OpContext::for_saga_action(&sagactx, &params.serialized_authn);
let sled_uuid = sagactx.lookup::<Uuid>("server_id")?;
let instance_id = sagactx.lookup::<Uuid>("instance_id")?;
let propolis_uuid = sagactx.lookup::<Uuid>("propolis_id")?;
Expand Down Expand Up @@ -848,9 +849,15 @@ async fn sic_create_instance_record(
runtime.into(),
);

let (.., authz_project) = LookupPath::new(&opctx, &osagactx.datastore())
.project_id(params.project_id)
.lookup_for(authz::Action::CreateChild)
.await
.map_err(ActionError::action_failed)?;

let instance = osagactx
.datastore()
.project_create_instance(new_instance)
.project_create_instance(&opctx, &authz_project, new_instance)
.await
.map_err(ActionError::action_failed)?;

Expand Down
8 changes: 4 additions & 4 deletions nexus/src/app/sagas/snapshot_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,15 +465,15 @@ async fn ssc_create_snapshot_record(
size: disk.size,
};

let (authz_silo, ..) = LookupPath::new(&opctx, &osagactx.datastore())
.silo_id(params.silo_id)
.fetch()
let (.., authz_project) = LookupPath::new(&opctx, &osagactx.datastore())
.project_id(params.project_id)
.lookup_for(authz::Action::CreateChild)
.await
.map_err(ActionError::action_failed)?;

let snapshot_created = osagactx
.datastore()
.project_ensure_snapshot(&opctx, &authz_silo, snapshot)
.project_ensure_snapshot(&opctx, &authz_project, snapshot)
.await
.map_err(ActionError::action_failed)?;

Expand Down
37 changes: 27 additions & 10 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 @@ -59,24 +62,38 @@ impl DataStore {
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

pub async fn project_create_disk(&self, disk: Disk) -> CreateResult<Disk> {
pub async fn project_create_disk(
&self,
opctx: &OpContext,
authz_project: &authz::Project,
disk: Disk,
) -> CreateResult<Disk> {
use db::schema::disk::dsl;

opctx.authorize(authz::Action::CreateChild, authz_project).await?;

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_authorized(opctx).await?)
.await
.map_err(|e| match e {
AsyncInsertError::CollectionNotFound => authz_project.not_found(),
AsyncInsertError::DatabaseError(e) => {
public_error_from_diesel_pool(
e,
ErrorHandler::Conflict(ResourceType::Disk, name.as_str()),
)
})?;
}
})?;

let runtime = disk.runtime();
bail_unless!(
Expand Down
Loading