Skip to content
Open
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
2 changes: 1 addition & 1 deletion crates/admin-cli/src/operating_system/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ impl From<OperatingSystem> for SerializableOs {
id: os.id.map(|u| u.to_string()).unwrap_or_default(),
name: os.name,
description: os.description,
org: os.tenant_organization_id,
org: os.tenant_organization_id.unwrap_or_default(),
os_type: forgerpc::OperatingSystemType::try_from(os.r#type)
.map(|t| t.as_str_name().to_string())
.unwrap_or_else(|_| os.r#type.to_string()),
Expand Down
8 changes: 6 additions & 2 deletions crates/admin-cli/src/operating_system/create/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ pub struct Args {
#[clap(short, long, help = "Name of the operating system definition.")]
pub name: String,

#[clap(short, long, help = "Organization identifier for this OS definition.")]
pub org: String,
#[clap(
short,
long,
help = "Optional tenant organization identifier for this OS definition. Omit for OS definitions owned by provider."
)]
pub org: Option<String>,

#[clap(
long,
Expand Down
7 changes: 5 additions & 2 deletions crates/admin-cli/src/operating_system/show/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async fn list_all(
table.add_row(Row::new(vec![
Cell::new(&id_str),
Cell::new(&os.name),
Cell::new(&os.tenant_organization_id),
Cell::new(os.tenant_organization_id.as_deref().unwrap_or_default()),
Cell::new(
OperatingSystemType::try_from(os.r#type)
.map(|t| t.as_str_name())
Expand Down Expand Up @@ -167,7 +167,10 @@ async fn show_one(
os.id.map(|u| u.to_string()).as_deref().unwrap_or("")
);
println!("Name: {}", os.name);
println!("Org: {}", os.tenant_organization_id);
println!(
"Org: {}",
os.tenant_organization_id.as_deref().unwrap_or_default()
);
println!(
"Type: {}",
OperatingSystemType::try_from(os.r#type)
Expand Down
9 changes: 7 additions & 2 deletions crates/api-core/src/handlers/operating_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,14 @@ pub async fn create_operating_system(
if req.name.is_empty() {
return Err(Status::invalid_argument("name is required"));
}
if req.tenant_organization_id.is_empty() {
// if tenant_organization_id is provided it must not be empty:
if req
.tenant_organization_id
.as_deref()
.is_some_and(|org| org.is_empty())
{
return Err(Status::invalid_argument(
"tenant_organization_id is required",
"tenant_organization_id must not be empty when provided",
));
}

Expand Down
2 changes: 1 addition & 1 deletion crates/api-core/src/ipxe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ fn operating_system_row_to_ipxe_script(
name: row.name.clone(),
description: row.description.clone(),
hash: row.ipxe_definition_hash.clone().unwrap_or_default(),
tenant_id: Some(row.org.clone()),
tenant_id: row.org.clone(),
ipxe_template_id,
parameters,
artifacts,
Expand Down
10 changes: 5 additions & 5 deletions crates/api-core/src/tests/instance_os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ async fn test_create_instance_with_ipxe_template_os(_: PgPoolOptions, options: P
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "template-os".to_string(),
tenant_organization_id: "test-org".to_string(),
tenant_organization_id: Some("test-org".to_string()),
description: Some("iPXE template based OS".to_string()),
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -298,7 +298,7 @@ async fn create_os_definition(
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: name.to_string(),
tenant_organization_id: "test-org".to_string(),
tenant_organization_id: Some("test-org".to_string()),
description: None,
is_active,
allow_override: false,
Expand Down Expand Up @@ -386,7 +386,7 @@ async fn test_allocate_instance_rejects_not_ready_os(_: PgPoolOptions, options:
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "not-ready-os".to_string(),
tenant_organization_id: "test-org".to_string(),
tenant_organization_id: Some("test-org".to_string()),
description: None,
is_active: true,
allow_override: false,
Expand Down Expand Up @@ -650,7 +650,7 @@ async fn test_create_instance_with_raw_ipxe_os_and_verify_pxe_rendering(
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "raw-ipxe-os".to_string(),
tenant_organization_id: "test-org".to_string(),
tenant_organization_id: Some("test-org".to_string()),
description: Some("raw iPXE OS for instance test".to_string()),
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -735,7 +735,7 @@ async fn test_create_instance_with_templated_ipxe_os_with_artifacts_and_verify_p
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "templated-os-with-artifacts".to_string(),
tenant_organization_id: "test-org".to_string(),
tenant_organization_id: Some("test-org".to_string()),
description: Some("templated iPXE OS with artifacts".to_string()),
is_active: true,
allow_override: true,
Expand Down
92 changes: 76 additions & 16 deletions crates/api-core/tests/integration/operating_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async fn test_create_operating_system_ipxe(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "test-ipxe-os".to_string(),
tenant_organization_id: "test-org".to_string(),
tenant_organization_id: Some("test-org".to_string()),
description: Some("inline iPXE OS".to_string()),
is_active: true,
allow_override: true,
Expand All @@ -51,7 +51,7 @@ async fn test_create_operating_system_ipxe(pool: PgPool) {

let os = resp.into_inner();
assert_eq!(os.name, "test-ipxe-os");
assert_eq!(os.tenant_organization_id, "test-org");
assert_eq!(os.tenant_organization_id.as_deref(), Some("test-org"));
assert_eq!(os.r#type, OperatingSystemType::OsTypeIpxe as i32);
assert_eq!(os.description.as_deref(), Some("inline iPXE OS"));
assert!(os.is_active);
Expand All @@ -76,7 +76,7 @@ async fn test_create_operating_system_requires_name(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "".to_string(),
tenant_organization_id: "test-org".to_string(),
tenant_organization_id: Some("test-org".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -104,7 +104,7 @@ async fn test_create_operating_system_requires_variant(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "test-os".to_string(),
tenant_organization_id: "test-org".to_string(),
tenant_organization_id: Some("test-org".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand All @@ -122,6 +122,66 @@ async fn test_create_operating_system_requires_variant(pool: PgPool) {
assert_eq!(resp.unwrap_err().code(), Code::InvalidArgument);
}

#[sqlx_test]
async fn test_create_operating_system_allows_omitted_org(pool: PgPool) {
let env = TestHarness::builder(pool).build().await;

let resp = env
.api()
.create_operating_system(tonic::Request::new(
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "no-org-os".to_string(),
tenant_organization_id: None,
description: None,
is_active: true,
allow_override: true,
phone_home_enabled: false,
user_data: None,
ipxe_script: Some("chain http://example.com".to_string()),
ipxe_template_id: None,
ipxe_template_parameters: vec![],
ipxe_template_artifacts: vec![],
},
))
.await
.unwrap();

let os = resp.into_inner();
assert_eq!(os.name, "no-org-os");
// An omitted org round-trips as an absent (None) wire field, not "".
assert!(os.tenant_organization_id.is_none());
assert!(os.id.is_some());
}

#[sqlx_test]
async fn test_create_operating_system_rejects_empty_org(pool: PgPool) {
let env = TestHarness::builder(pool).build().await;

let resp = env
.api()
.create_operating_system(tonic::Request::new(
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "empty-org-os".to_string(),
tenant_organization_id: Some("".to_string()),
description: None,
is_active: true,
allow_override: true,
phone_home_enabled: false,
user_data: None,
ipxe_script: Some("chain http://example.com".to_string()),
ipxe_template_id: None,
ipxe_template_parameters: vec![],
ipxe_template_artifacts: vec![],
},
))
.await;

assert!(resp.is_err());
assert_eq!(resp.unwrap_err().code(), Code::InvalidArgument);
}

#[sqlx_test]
async fn test_get_operating_system(pool: PgPool) {
let env = TestHarness::builder(pool).build().await;
Expand All @@ -132,7 +192,7 @@ async fn test_get_operating_system(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "get-test-os".to_string(),
tenant_organization_id: "org1".to_string(),
tenant_organization_id: Some("org1".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -186,7 +246,7 @@ async fn test_update_operating_system(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "original-name".to_string(),
tenant_organization_id: "org1".to_string(),
tenant_organization_id: Some("org1".to_string()),
description: Some("original desc".to_string()),
is_active: true,
allow_override: false,
Expand Down Expand Up @@ -248,7 +308,7 @@ async fn test_delete_operating_system(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "delete-test-os".to_string(),
tenant_organization_id: "org1".to_string(),
tenant_organization_id: Some("org1".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -290,7 +350,7 @@ async fn test_find_operating_system_ids(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "find-os-1".to_string(),
tenant_organization_id: "find-org".to_string(),
tenant_organization_id: Some("find-org".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand All @@ -312,7 +372,7 @@ async fn test_find_operating_system_ids(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "find-os-2".to_string(),
tenant_organization_id: "find-org".to_string(),
tenant_organization_id: Some("find-org".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -356,7 +416,7 @@ async fn test_find_operating_systems_by_ids(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "by-id-os-1".to_string(),
tenant_organization_id: "org1".to_string(),
tenant_organization_id: Some("org1".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -450,7 +510,7 @@ async fn create_os_with_artifacts(env: &TestHarness) -> OperatingSystemId {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "artifact-test-os".to_string(),
tenant_organization_id: "org1".to_string(),
tenant_organization_id: Some("org1".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -586,7 +646,7 @@ async fn test_set_artifacts_cached_url_ordered_duplicate_names(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "dup-kernel-os".to_string(),
tenant_organization_id: "org1".to_string(),
tenant_organization_id: Some("org1".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -888,7 +948,7 @@ async fn test_create_strips_cached_url(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "strip-test".to_string(),
tenant_organization_id: "org1".to_string(),
tenant_organization_id: Some("org1".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -941,7 +1001,7 @@ async fn test_create_with_cached_only_sets_provisioning(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "provision-test".to_string(),
tenant_organization_id: "org1".to_string(),
tenant_organization_id: Some("org1".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -1251,7 +1311,7 @@ async fn test_create_operating_system_with_explicit_id(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: Some(id_proto),
name: "explicit-id-os".to_string(),
tenant_organization_id: "org1".to_string(),
tenant_organization_id: Some("org1".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down Expand Up @@ -1280,7 +1340,7 @@ async fn test_deleted_os_not_returned_by_find_ids(pool: PgPool) {
rpc::forge::CreateOperatingSystemRequest {
id: None,
name: "soon-deleted-os".to_string(),
tenant_organization_id: "del-org".to_string(),
tenant_organization_id: Some("del-org".to_string()),
description: None,
is_active: true,
allow_override: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Allow an operating_system definition to exist without an owning tenant
-- organization (e.g. provider-owned OSes). Absence of an org is now represented
-- as NULL.
ALTER TABLE operating_systems
ALTER COLUMN org DROP NOT NULL;
2 changes: 1 addition & 1 deletion crates/api-db/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1286,7 +1286,7 @@ mod tests {
id: None,
name: "soft-deleted-os".to_string(),
description: None,
org: "test-org".to_string(),
org: Some("test-org".to_string()),
type_: model::operating_system_definition::OS_TYPE_IPXE.to_string(),
status: operating_system::OS_STATUS_READY.to_string(),
is_active: true,
Expand Down
Loading
Loading