Skip to content
Draft
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 nexus/db-fixed-data/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub static SERVICES_PROJECT: LazyLock<model::Project> = LazyLock::new(|| {
name: SERVICES_DB_NAME.parse().unwrap(),
description: "Built-in project for Oxide Services".to_string(),
},
skip_default_vpc: false,
},
)
});
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2093,6 +2093,7 @@ mod tests {
name: "testpost".parse().unwrap(),
description: "please ignore".to_string(),
},
skip_default_vpc: false,
},
),
)
Expand Down
2 changes: 2 additions & 0 deletions nexus/db-queries/src/db/datastore/external_subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,7 @@ mod tests {
name: "my-project".parse().unwrap(),
description: String::new(),
},
skip_default_vpc: false,
},
),
)
Expand Down Expand Up @@ -2842,6 +2843,7 @@ mod tests {
name: "my-project".parse().unwrap(),
description: String::new(),
},
skip_default_vpc: false,
},
),
)
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2337,6 +2337,7 @@ mod tests {
name: "stuff".parse().unwrap(),
description: "Where I keep my stuff".into(),
},
skip_default_vpc: false,
},
),
)
Expand Down
2 changes: 2 additions & 0 deletions nexus/db-queries/src/db/datastore/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2829,6 +2829,7 @@ mod test {
name: "my-project".parse().unwrap(),
description: "".to_string(),
},
skip_default_vpc: false,
},
);
let (.., project) =
Expand Down Expand Up @@ -2946,6 +2947,7 @@ mod test {
name: "my-project".parse().unwrap(),
description: "".to_string(),
},
skip_default_vpc: false,
},
);
let (.., project) =
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ mod tests {
name: "stuff".parse().unwrap(),
description: "Where I keep my stuff".into(),
},
skip_default_vpc: false,
},
),
)
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ mod test {
name: "project".parse().unwrap(),
description: "desc".to_string(),
},
skip_default_vpc: false,
},
);
datastore.project_create(&opctx, project).await.unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ mod test {
name: "myproject".parse().unwrap(),
description: "It's a project".into(),
},
skip_default_vpc: false,
},
),
)
Expand Down
3 changes: 3 additions & 0 deletions nexus/db-queries/src/db/datastore/vpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3026,6 +3026,7 @@ mod tests {
name: "project".parse().unwrap(),
description: String::from("test project"),
},
skip_default_vpc: false,
};
let project = Project::new(Uuid::new_v4(), project_params);
let (authz_project, _) = datastore
Expand Down Expand Up @@ -3131,6 +3132,7 @@ mod tests {
name: "project".parse().unwrap(),
description: String::from("test project"),
},
skip_default_vpc: false,
};
let project = Project::new(Uuid::new_v4(), project_params);
let (authz_project, _) = datastore
Expand Down Expand Up @@ -3546,6 +3548,7 @@ mod tests {
name: "project".parse().unwrap(),
description: String::from("test project"),
},
skip_default_vpc: false,
};
let project = Project::new(DEFAULT_SILO.id(), project_params);
let (authz_project, _) = datastore
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/pub_test_utils/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub async fn create_project(
name: name.parse().unwrap(),
description: "desc".to_string(),
},
skip_default_vpc: false,
},
);
datastore.project_create(&opctx, project).await.unwrap()
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/queries/network_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2143,6 +2143,7 @@ mod tests {
name: "project".parse().unwrap(),
description: "desc".to_string(),
},
skip_default_vpc: false,
},
);
let (.., project) =
Expand Down
18 changes: 18 additions & 0 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ mod v2026012200;
mod v2026012300;
mod v2026013000;
mod v2026013001;
mod v2026020100;

#[cfg(test)]
mod test_utils;
Expand Down Expand Up @@ -79,6 +80,7 @@ api_versions!([
// | date-based version should be at the top of the list.
// v
// (next_yyyymmddnn, IDENT),
(2026020100, SKIP_DEFAULT_VPC),
(2026013100, READ_ONLY_DISKS_NULLABLE),
(2026013001, READ_ONLY_DISKS),
(2026013000, INSTANCES_EXTERNAL_SUBNETS),
Expand Down Expand Up @@ -942,12 +944,28 @@ pub trait NexusExternalApi {
method = POST,
path = "/v1/projects",
tags = ["projects"],
versions = VERSION_SKIP_DEFAULT_VPC..,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if the operation_id is needed here.

}]
async fn project_create(
rqctx: RequestContext<Self::Context>,
new_project: TypedBody<params::ProjectCreate>,
) -> Result<HttpResponseCreated<views::Project>, HttpError>;

/// Create project
#[endpoint {
method = POST,
path = "/v1/projects",
tags = ["projects"],
operation_id = "project_create",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if the operation_id is needed here in the versioned function.

versions = ..VERSION_SKIP_DEFAULT_VPC,
}]
async fn v2026020100_project_create(
rqctx: RequestContext<Self::Context>,
new_project: TypedBody<v2026020100::ProjectCreate>,
) -> Result<HttpResponseCreated<views::Project>, HttpError> {
Self::project_create(rqctx, new_project.map(Into::into)).await
}

/// Fetch project
#[endpoint {
method = GET,
Expand Down
24 changes: 24 additions & 0 deletions nexus/external-api/src/v2026020100.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// 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/.

//! Types that changed in v2026020100.

use nexus_types::external_api::params;
use omicron_common::api::external::IdentityMetadataCreateParams;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;

/// Create-time parameters for a `Project`
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct ProjectCreate {
#[serde(flatten)]
pub identity: IdentityMetadataCreateParams,
}

impl From<ProjectCreate> for params::ProjectCreate {
fn from(ProjectCreate { identity }: ProjectCreate) -> Self {
Self { identity, skip_default_vpc: false }
}
}
91 changes: 79 additions & 12 deletions nexus/src/app/sagas/project_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,24 @@ impl NexusSaga for SagaProjectCreate {
}

fn make_saga_dag(
_params: &Self::Params,
params: &Self::Params,
mut builder: steno::DagBuilder,
) -> Result<steno::Dag, super::SagaInitError> {
builder.append(project_create_record_action());
builder.append(project_create_vpc_params_action());

let subsaga_builder = steno::DagBuilder::new(steno::SagaName::new(
sagas::vpc_create::SagaVpcCreate::NAME,
));
builder.append(steno::Node::subsaga(
"vpc",
sagas::vpc_create::create_dag(subsaga_builder)?,
"vpc_create_params",
));

if !params.project_create.skip_default_vpc {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if skipping parts of sagas like this is the preferred way to handle such conditional logic but it seemed to be the cleanest method here.

builder.append(project_create_vpc_params_action());

let subsaga_builder = steno::DagBuilder::new(steno::SagaName::new(
sagas::vpc_create::SagaVpcCreate::NAME,
));
builder.append(steno::Node::subsaga(
"vpc",
sagas::vpc_create::create_dag(subsaga_builder)?,
"vpc_create_params",
));
}

Ok(builder.build()?)
}
}
Expand Down Expand Up @@ -162,24 +166,34 @@ mod test {
ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper,
};
use nexus_db_queries::{
authn::saga::Serialized, authz, context::OpContext,
authn::saga::Serialized, authz, context::OpContext, db,
db::datastore::DataStore,
};
use nexus_test_utils_macros::nexus_test;
use nexus_types::identity::Resource;
use omicron_common::api::external::IdentityMetadataCreateParams;

type ControlPlaneTestContext =
nexus_test_utils::ControlPlaneTestContext<crate::Server>;

// Helper for creating project create parameters
fn new_test_params(opctx: &OpContext, authz_silo: authz::Silo) -> Params {
new_test_params_with_options(opctx, authz_silo, false)
}

fn new_test_params_with_options(
opctx: &OpContext,
authz_silo: authz::Silo,
skip_default_vpc: bool,
) -> Params {
Params {
serialized_authn: Serialized::for_opctx(opctx),
project_create: params::ProjectCreate {
identity: IdentityMetadataCreateParams {
name: "my-project".parse().unwrap(),
description: "My Project".to_string(),
},
skip_default_vpc,
},
authz_silo,
}
Expand Down Expand Up @@ -304,4 +318,57 @@ mod test {
)
.await;
}

#[nexus_test(server = crate::Server)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still figuring out tests for this. It's not pretty currently.

async fn test_skip_default_vpc_creates_no_vpc(
cptestctx: &ControlPlaneTestContext,
) {
let nexus = &cptestctx.server.server_context().nexus;
let datastore = nexus.datastore();

// Before running the test, confirm we have no records of any projects.
verify_clean_slate(datastore).await;

// Build the saga DAG with skip_default_vpc = true.
let opctx = test_opctx(&cptestctx);
let authz_silo = opctx.authn.silo_required().unwrap();
let params =
new_test_params_with_options(&opctx, authz_silo.clone(), true);
let saga_output = nexus
.sagas
.saga_execute::<SagaProjectCreate>(params)
.await
.unwrap();

// Verify that a project was created.
let (authz_project, db_project) = saga_output
.lookup_node_output::<(authz::Project, db::model::Project)>(
"project",
)
.unwrap();
assert_eq!(db_project.name().as_str(), "my-project");

// Verify that no VPCs were created for this project.
use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
use nexus_db_queries::db::model::Vpc;
use nexus_db_schema::schema::vpc::dsl;

let vpcs = dsl::vpc
.filter(dsl::project_id.eq(authz_project.id()))
.filter(dsl::time_deleted.is_null())
.select(Vpc::as_select())
.load_async::<Vpc>(
&*datastore.pool_connection_for_tests().await.unwrap(),
)
.await
.unwrap();

assert!(
vpcs.is_empty(),
"expected no VPCs for project with skip_default_vpc=true, \
found: {:?}",
vpcs.iter().map(|v| v.name()).collect::<Vec<_>>()
);
}
}
1 change: 1 addition & 0 deletions nexus/test-utils/src/resource_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,7 @@ pub async fn create_project(
name: project_name.parse().unwrap(),
description: "a pier".to_string(),
},
skip_default_vpc: false,
},
)
.await
Expand Down
2 changes: 2 additions & 0 deletions nexus/tests/integration_tests/audit_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ async fn test_audit_log_list(ctx: &ControlPlaneTestContext) {
name: "test-proj2".parse().unwrap(),
description: "a pier".to_string(),
},
skip_default_vpc: false,
};
let long_user_agent = "A".repeat(300);
let long_query_value = "B".repeat(600);
Expand Down Expand Up @@ -677,6 +678,7 @@ async fn test_audit_log_access_token_auth(ctx: &ControlPlaneTestContext) {
name: "token-project".parse().unwrap(),
description: "created with access token".to_string(),
},
skip_default_vpc: false,
};
RequestBuilder::new(client, Method::POST, "/v1/projects")
.body(Some(&body))
Expand Down
3 changes: 3 additions & 0 deletions nexus/tests/integration_tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ async fn test_projects_basic(cptestctx: &ControlPlaneTestContext) {
"<auto-generated by test suite>",
),
},
skip_default_vpc: false,
},
)
.authn_as(AuthnMode::PrivilegedUser)
Expand Down Expand Up @@ -367,6 +368,7 @@ async fn test_projects_basic(cptestctx: &ControlPlaneTestContext) {
name: "simproject1".parse().unwrap(),
description: "a duplicate of simproject1".to_string(),
},
skip_default_vpc: false,
};
let error = NexusRequest::new(
RequestBuilder::new(client, Method::POST, &projects_url)
Expand Down Expand Up @@ -415,6 +417,7 @@ async fn test_projects_basic(cptestctx: &ControlPlaneTestContext) {
name: "honor-roller".parse().unwrap(),
description: "a soapbox racer".to_string(),
},
skip_default_vpc: false,
};
let project: Project =
NexusRequest::objects_post(client, projects_url, &project_create)
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/integration_tests/console_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ async fn test_sessions(cptestctx: &ControlPlaneTestContext) {
name: "my-proj".parse().unwrap(),
description: "a project".to_string(),
},
skip_default_vpc: false,
};

// hitting auth-gated API endpoint without session cookie 401s
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ pub static DEMO_PROJECT_CREATE: LazyLock<params::ProjectCreate> =
name: DEMO_PROJECT_NAME.clone(),
description: String::from(""),
},
skip_default_vpc: false,
});

// VPC used for testing
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/integration_tests/external_ips.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ async fn test_floating_ip_create_non_admin(
name: PROJECT_NAME.parse().unwrap(),
description: "floating ip project".to_string(),
},
skip_default_vpc: false,
},
)
.authn_as(AuthnMode::SiloUser(user.id))
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/integration_tests/instances.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8078,6 +8078,7 @@ async fn test_instance_create_in_silo(cptestctx: &ControlPlaneTestContext) {
name: PROJECT_NAME.parse().unwrap(),
description: String::new(),
},
skip_default_vpc: false,
},
)
.authn_as(AuthnMode::SiloUser(user_id))
Expand Down
Loading
Loading