diff --git a/crates/admin-cli/src/vpc/show/cmd.rs b/crates/admin-cli/src/vpc/show/cmd.rs index 18230b5736..2b173aa15e 100644 --- a/crates/admin-cli/src/vpc/show/cmd.rs +++ b/crates/admin-cli/src/vpc/show/cmd.rs @@ -98,6 +98,40 @@ async fn show_vpc_details( Ok(()) } +#[allow(deprecated)] +fn vpc_config(vpc: &forgerpc::Vpc) -> forgerpc::VpcConfig { + if let Some(config) = vpc.config.clone() { + config + } else { + forgerpc::VpcConfig { + tenant_organization_id: vpc.tenant_organization_id.clone(), + tenant_keyset_id: vpc.tenant_keyset_id.clone(), + network_virtualization_type: vpc.network_virtualization_type, + network_security_group_id: vpc.network_security_group_id.clone(), + default_nvlink_logical_partition_id: vpc.default_nvlink_logical_partition_id, + vni: vpc.vni, + routing_profile_type: vpc.routing_profile_type.clone(), + } + } +} + +#[allow(deprecated)] +fn vpc_allocated_vni(vpc: &forgerpc::Vpc) -> u32 { + vpc.status + .as_ref() + .and_then(|status| status.vni) + .or(vpc.deprecated_vni) + .unwrap_or_default() +} + +#[allow(deprecated)] +fn vpc_virt_type(vpc: &forgerpc::Vpc) -> i32 { + vpc_config(vpc) + .network_virtualization_type + .or(vpc.network_virtualization_type) + .unwrap_or_default() +} + fn convert_vpcs_to_nice_table(vpcs: forgerpc::VpcList) -> Box { let mut table = Table::new(); @@ -115,18 +149,17 @@ fn convert_vpcs_to_nice_table(vpcs: forgerpc::VpcList) -> Box
{ for vpc in vpcs.vpcs { let metadata = vpc.metadata.as_ref().unwrap_or(&default_metadata); - let virt_type = forgerpc::VpcVirtualizationType::try_from( - vpc.network_virtualization_type.unwrap_or_default(), - ) - .unwrap_or_default() - .as_str_name() - .to_string(); + let config = vpc_config(&vpc); + let virt_type = forgerpc::VpcVirtualizationType::try_from(vpc_virt_type(&vpc)) + .unwrap_or_default() + .as_str_name() + .to_string(); table.add_row(row![ vpc.id.unwrap_or_default(), metadata.name, - vpc.tenant_organization_id, - vpc.network_security_group_id.unwrap_or_default(), + config.tenant_organization_id, + config.network_security_group_id.unwrap_or_default(), vpc.version, vpc.created.unwrap_or_default(), virt_type, @@ -146,9 +179,11 @@ fn convert_vpcs_to_nice_table(vpcs: forgerpc::VpcList) -> Box
{ table.into() } +#[allow(deprecated)] fn convert_vpc_to_nice_format(vpc: &forgerpc::Vpc) -> CarbideCliResult { let width = 25; let mut lines = String::new(); + let config = vpc_config(vpc); let vpc_name = vpc .metadata @@ -159,10 +194,10 @@ fn convert_vpc_to_nice_format(vpc: &forgerpc::Vpc) -> CarbideCliResult { let data: Vec<(&'static str, Cow)> = vec![ ("ID", vpc.id.unwrap_or_default().to_string().into()), ("NAME", vpc_name), - ("TENANT ORG", vpc.tenant_organization_id.as_str().into()), + ("TENANT ORG", config.tenant_organization_id.as_str().into()), ( "NETWORK SECURITY GROUP", - vpc.network_security_group_id().into(), + config.network_security_group_id.unwrap_or_default().into(), ), ("VERSION", vpc.version.as_str().into()), ( @@ -180,19 +215,17 @@ fn convert_vpc_to_nice_format(vpc: &forgerpc::Vpc) -> CarbideCliResult { None => "".into(), }, ), - ("TENANT KEYSET", vpc.tenant_keyset_id().into()), ( - "VNI", - format!("{}", vpc.status.and_then(|s| s.vni).unwrap_or_default()).into(), + "TENANT KEYSET", + config.tenant_keyset_id.unwrap_or_default().into(), ), + ("VNI", format!("{}", vpc_allocated_vni(vpc)).into()), ( "NW VIRTUALIZATION", - forgerpc::VpcVirtualizationType::try_from( - vpc.network_virtualization_type.unwrap_or_default(), - ) - .unwrap_or_default() - .as_str_name() - .into(), + forgerpc::VpcVirtualizationType::try_from(vpc_virt_type(vpc)) + .unwrap_or_default() + .as_str_name() + .into(), ), ]; diff --git a/crates/api-core/src/db_init.rs b/crates/api-core/src/db_init.rs index 157a74b3c7..bd77f329e1 100644 --- a/crates/api-core/src/db_init.rs +++ b/crates/api-core/src/db_init.rs @@ -105,7 +105,8 @@ pub async fn create_initial_networks( ns.vpc_id = if let Some(vpc_name) = &def.vpc_name { match db::vpc::find_by_name(&mut txn, vpc_name).await?.as_slice() { [vpc] => { - vpc.network_virtualization_type + vpc.config + .network_virtualization_type .ensure_supports_segment(&ns)?; Some(vpc.id) } @@ -283,7 +284,7 @@ pub async fn update_network_segments_svi_ip(db_pool: &Pool) -> Result< }; // SVI IP is needed only for FNN. - if vpc.network_virtualization_type != VpcVirtualizationType::Fnn { + if vpc.config.network_virtualization_type != VpcVirtualizationType::Fnn { continue; } @@ -400,8 +401,8 @@ pub(crate) async fn create_admin_vpc( }; if let Some(mut existing_vpc) = existing_vpc { - let existing_vni = existing_vpc.status.as_ref().and_then(|status| status.vni); - if existing_vni != Some(configured_vni) || existing_vpc.vni != Some(configured_vni) { + let existing_vni = existing_vpc.status.vni; + if existing_vni != Some(configured_vni) || existing_vpc.config.vni != Some(configured_vni) { if let Some(conflicting_vpc) = db::vpc::find_by_vni(&mut txn, configured_vni) .await? .into_iter() diff --git a/crates/api-core/src/ethernet_virtualization.rs b/crates/api-core/src/ethernet_virtualization.rs index 0c17dadc06..257f218b3b 100644 --- a/crates/api-core/src/ethernet_virtualization.rs +++ b/crates/api-core/src/ethernet_virtualization.rs @@ -152,7 +152,7 @@ pub(crate) async fn validate_instance_interface_routing_profiles( let vpc = db::vpc::find_by_segment(&mut *txn, segment_id).await?; // Interface routing profiles are only valid on FNN VPC interfaces. - if vpc.network_virtualization_type != VpcVirtualizationType::Fnn { + if vpc.config.network_virtualization_type != VpcVirtualizationType::Fnn { return Err(CarbideError::InvalidArgument( "instance interface routing_profile is only supported for FNN VPC interfaces" .to_string(), @@ -165,7 +165,8 @@ pub(crate) async fn validate_instance_interface_routing_profiles( ) })?; let profile_type = - vpc.routing_profile_type + vpc.config + .routing_profile_type .as_ref() .ok_or_else(|| CarbideError::Internal { message: "tenant routing profile type not found in VPC record".to_string(), @@ -440,7 +441,7 @@ pub async fn admin_network( return Err(CarbideError::FindOneReturnedNoResultsError(vpc_id.into()).into()); } let vpc = vpcs.remove(0); - match vpc.status.and_then(|v| v.vni) { + match vpc.status.vni { Some(vpc_vni) => { let tenant_loopback_ip = if use_vpc_vrf_loopback { Some( @@ -643,50 +644,51 @@ pub async fn tenant_network( None => None, }; - let vpc_vni = vpc - .as_ref() - .and_then(|v| v.status.as_ref().and_then(|vs| vs.vni)) - .unwrap_or_default() as u32; + let vpc_vni = vpc.as_ref().and_then(|v| v.status.vni).unwrap_or_default() as u32; // Resolve the routing profile from the VPC attached to this interface. - let (vpc_routing_profile, interface_routing_profile) = match (vpc.as_ref(), fnn_config) { - (Some(vpc), Some(fnn)) if vpc.network_virtualization_type == VpcVirtualizationType::Fnn => { - let profile_type = - vpc.routing_profile_type - .as_ref() - .ok_or_else(|| CarbideError::Internal { + let (vpc_routing_profile, interface_routing_profile) = + match (vpc.as_ref(), fnn_config) { + (Some(vpc), Some(fnn)) + if vpc.config.network_virtualization_type == VpcVirtualizationType::Fnn => + { + let profile_type = vpc.config.routing_profile_type.as_ref().ok_or_else(|| { + CarbideError::Internal { message: "tenant routing profile type not found in VPC record".to_string(), - })?; - let profile = fnn.routing_profiles.get(profile_type).ok_or_else(|| { - CarbideError::NotFoundError { - kind: "routing_profile_type", - id: profile_type.to_string(), - } - })?; + } + })?; + let profile = fnn.routing_profiles.get(profile_type).ok_or_else(|| { + CarbideError::NotFoundError { + kind: "routing_profile_type", + id: profile_type.to_string(), + } + })?; - ( - Some(rpc::RoutingProfile::from(profile)), - iface - .routing_profile - .as_ref() - .map(rpc::FlatInterfaceRoutingProfile::from), - ) - } - (Some(vpc), None) if vpc.network_virtualization_type == VpcVirtualizationType::Fnn => { - return Err(CarbideError::Internal { - message: "FNN VPC found but no FNN config found".to_string(), + ( + Some(rpc::RoutingProfile::from(profile)), + iface + .routing_profile + .as_ref() + .map(rpc::FlatInterfaceRoutingProfile::from), + ) } - .into()); - } - _ if iface.routing_profile.is_some() => { - return Err(CarbideError::InvalidArgument( - "instance interface routing_profile is only supported for FNN VPC interfaces" - .to_string(), - ) - .into()); - } - _ => (None, None), - }; + (Some(vpc), None) + if vpc.config.network_virtualization_type == VpcVirtualizationType::Fnn => + { + return Err(CarbideError::Internal { + message: "FNN VPC found but no FNN config found".to_string(), + } + .into()); + } + _ if iface.routing_profile.is_some() => { + return Err(CarbideError::InvalidArgument( + "instance interface routing_profile is only supported for FNN VPC interfaces" + .to_string(), + ) + .into()); + } + _ => (None, None), + }; let rpc_ft: rpc::InterfaceFunctionType = iface.function_id.function_type().into(); let (svi_ip, svi_ip_v6) = ds.svi_ips(network_virtualization_type, is_l2_segment)?; @@ -701,14 +703,14 @@ pub async fn tenant_network( // see if there's an associated VPC (there should be), // and see if the VPC has an NSG attached. (false, None, Some(v)) => { - match v.network_security_group_id.as_ref() { + match v.config.network_security_group_id.as_ref() { None => None, Some(vpc_nsg_id) => { // Make our DB query for the IDs to get our NetworkSecurityGroup let network_security_group = network_security_group::find_by_ids( txn, &[vpc_nsg_id.to_owned()], - Some(&v.tenant_organization_id.parse().map_err(|_| { + Some(&v.config.tenant_organization_id.parse().map_err(|_| { CarbideError::Internal { message: "invalid tenant org in VPC data".to_string(), } @@ -719,7 +721,7 @@ pub async fn tenant_network( .pop() .ok_or(CarbideError::NotFoundError { kind: "NetworkSecurityGroup", - id: v.tenant_organization_id.clone(), + id: vpc_nsg_id.to_string(), })?; Some(( diff --git a/crates/api-core/src/handlers/dpu.rs b/crates/api-core/src/handlers/dpu.rs index 25550e3ec7..40a98efecf 100644 --- a/crates/api-core/src/handlers/dpu.rs +++ b/crates/api-core/src/handlers/dpu.rs @@ -258,9 +258,9 @@ pub(crate) async fn get_managed_host_network_config_inner( let vpc = db::vpc::find_by_segment(&mut txn, network_segment_id) .await?; - network_virtualization_type = vpc.network_virtualization_type; + network_virtualization_type = vpc.config.network_virtualization_type; - vpc_vni = vpc.status.as_ref().and_then(|v| v.vni.map(|x|x as u32)); + vpc_vni = vpc.status.vni.map(|x| x as u32); let suppress_tenant_security_groups = match &snapshot.managed_state { ManagedHostState::Assigned { instance_state } => { diff --git a/crates/api-core/src/handlers/network_segment.rs b/crates/api-core/src/handlers/network_segment.rs index 0db2dd0d48..52758d3505 100644 --- a/crates/api-core/src/handlers/network_segment.rs +++ b/crates/api-core/src/handlers/network_segment.rs @@ -143,7 +143,7 @@ pub(crate) async fn create( .first() .ok_or_else(|| CarbideError::internal(format!("VPC ID: {vpc_id} not found.")))?; - let virtualization_type = vpc.network_virtualization_type; + let virtualization_type = vpc.config.network_virtualization_type; // Segment compatibility (segment-type binding + IPv6 support) // and SVI allocation are both expressed as capability checks @@ -214,7 +214,8 @@ pub(crate) async fn attach_to_vpc( .into()); } - vpc.network_virtualization_type + vpc.config + .network_virtualization_type .ensure_supports_segment(&segment) .map_err(CarbideError::from)?; diff --git a/crates/api-core/src/handlers/vpc.rs b/crates/api-core/src/handlers/vpc.rs index 0b8a6191e9..ad053d2447 100644 --- a/crates/api-core/src/handlers/vpc.rs +++ b/crates/api-core/src/handlers/vpc.rs @@ -189,7 +189,8 @@ pub(crate) async fn update( &mut txn, std::slice::from_ref(&id), Some( - &vpc.tenant_organization_id + &vpc.config + .tenant_organization_id .parse() .map_err(|e: InvalidTenantOrg| { CarbideError::from(RpcDataConversionError::InvalidTenantOrg(e.to_string())) @@ -203,7 +204,7 @@ pub(crate) async fn update( { return Err(CarbideError::FailedPrecondition(format!( "NetworkSecurityGroup `{}` does not exist or is not owned by Tenant `{}`", - id, vpc.tenant_organization_id + id, vpc.config.tenant_organization_id )) .into()); } @@ -299,13 +300,16 @@ pub(crate) async fn delete( } }; - if let Some(vni) = vpc.status.as_ref().and_then(|s| s.vni) { + if let Some(vni) = vpc.status.vni { // We can just keep deriving int/ext from the routing profile // because a VPC is not allowed to change its profile after // creation. VPC types that don't carry a routing profile // (ETV, Flat) land in the internal pool on create -- mirror // that here so the VNI is released back to the same pool. - let internal = match (api.runtime_config.fnn.as_ref(), vpc.routing_profile_type) { + let internal = match ( + api.runtime_config.fnn.as_ref(), + vpc.config.routing_profile_type, + ) { (None, _) | (Some(_), None) => true, (Some(f), Some(profile_type)) => { let Some(profile) = f.routing_profiles.get(&profile_type) else { diff --git a/crates/api-core/src/handlers/vpc_peering.rs b/crates/api-core/src/handlers/vpc_peering.rs index 36fbb9f35f..be3558c861 100644 --- a/crates/api-core/src/handlers/vpc_peering.rs +++ b/crates/api-core/src/handlers/vpc_peering.rs @@ -75,8 +75,9 @@ pub async fn create( // Make sure the VPCs are allowed to peer based on their // virtualization types. Their capabilities will determine // if they are allowed or not. - vpc1.network_virtualization_type - .ensure_can_peer_with(vpc2.network_virtualization_type) + vpc1.config + .network_virtualization_type + .ensure_can_peer_with(vpc2.config.network_virtualization_type) .map_err(CarbideError::from)?; } Some(VpcPeeringPolicy::Mixed) => { diff --git a/crates/api-core/src/handlers/vpc_prefix.rs b/crates/api-core/src/handlers/vpc_prefix.rs index 03ac44c091..a5267241f8 100644 --- a/crates/api-core/src/handlers/vpc_prefix.rs +++ b/crates/api-core/src/handlers/vpc_prefix.rs @@ -76,7 +76,8 @@ pub async fn create( })?; if new_prefix.config.prefix.is_ipv6() { - vpc.network_virtualization_type + vpc.config + .network_virtualization_type .ensure_supports_ipv6_prefix() .map_err(CarbideError::from)?; } diff --git a/crates/api-core/src/instance/mod.rs b/crates/api-core/src/instance/mod.rs index 8c358f6bbb..a0d1c630ce 100644 --- a/crates/api-core/src/instance/mod.rs +++ b/crates/api-core/src/instance/mod.rs @@ -242,7 +242,7 @@ pub async fn allocate_network( if vpcs.len() != vpc_ids.len() || vpcs .iter() - .any(|x| x.network_virtualization_type != VpcVirtualizationType::Fnn) + .any(|x| x.config.network_virtualization_type != VpcVirtualizationType::Fnn) { return Err(CarbideError::InvalidConfiguration( ConfigValidationError::InvalidValue(format!( @@ -252,7 +252,7 @@ pub async fn allocate_network( .map(|x| (x.id, x.vpc_id)) .collect_vec(), vpcs.iter() - .map(|x| (x.id, x.network_virtualization_type)) + .map(|x| (x.id, x.config.network_virtualization_type)) .collect_vec() )), )); @@ -955,14 +955,17 @@ pub async fn batch_allocate_instances( CarbideError::from(e) } })?; - let vpc_iface = vpc.network_virtualization_type.fabric_interface_type(); + let vpc_iface = vpc + .config + .network_virtualization_type + .fabric_interface_type(); if vpc_iface != FabricInterfaceType::Nic { return Err(CarbideError::FailedPrecondition(format!( "zero-DPU host {} has HostInband segment {} bound to VPC {} ({}); zero-DPU hosts can only allocate into VPCs whose fabric_interface_type is `nic` (got `{vpc_iface}`)", mh_snapshot.host_snapshot.id, segment_id, vpc.id, - vpc.network_virtualization_type, + vpc.config.network_virtualization_type, ))); } } @@ -998,13 +1001,16 @@ pub async fn batch_allocate_instances( let vpc = db::vpc::find_by_segment(&mut txn, ns_id) .await .map_err(CarbideError::from)?; - let vpc_iface = vpc.network_virtualization_type.fabric_interface_type(); + let vpc_iface = vpc + .config + .network_virtualization_type + .fabric_interface_type(); if vpc_iface != FabricInterfaceType::Dpu { return Err(CarbideError::FailedPrecondition(format!( "DPU-managed host {} cannot allocate an instance into VPC {} ({}, via segment {}); DPU hosts can only allocate into VPCs whose fabric_interface_type is `dpu` (got `{vpc_iface}`)", mh_snapshot.host_snapshot.id, vpc.id, - vpc.network_virtualization_type, + vpc.config.network_virtualization_type, ns_id, ))); } diff --git a/crates/api-core/src/tests/machine_network.rs b/crates/api-core/src/tests/machine_network.rs index c9afcb8139..87fabed307 100644 --- a/crates/api-core/src/tests/machine_network.rs +++ b/crates/api-core/src/tests/machine_network.rs @@ -514,8 +514,8 @@ async fn test_managed_host_network_config_includes_per_vpc_routing_profiles(pool let external_vpc = db::vpc::find_by_segment(txn.as_mut(), external_segment_id) .await .unwrap(); - let internal_vni = internal_vpc.status.unwrap().vni.unwrap() as u32; - let external_vni = external_vpc.status.unwrap().vni.unwrap() as u32; + let internal_vni = internal_vpc.status.vni.unwrap() as u32; + let external_vni = external_vpc.status.vni.unwrap() as u32; let profiles_by_vni = response .tenant_interfaces .into_iter() diff --git a/crates/api-core/src/tests/network_segment.rs b/crates/api-core/src/tests/network_segment.rs index 258bc00c60..8c4ca47a90 100644 --- a/crates/api-core/src/tests/network_segment.rs +++ b/crates/api-core/src/tests/network_segment.rs @@ -624,11 +624,11 @@ pub async fn test_create_initial_vpc_and_attached_network( assert_eq!(seeded_vpcs.len(), 1); let seeded_vpc = &seeded_vpcs[0]; assert_eq!( - seeded_vpc.tenant_organization_id, + seeded_vpc.config.tenant_organization_id, "2829bbe3-c169-4cd9-8b2a-19a8b1618a93" ); assert_eq!( - seeded_vpc.network_virtualization_type, + seeded_vpc.config.network_virtualization_type, VpcVirtualizationType::Flat ); @@ -1164,7 +1164,7 @@ async fn test_update_svi_ip_admin_segment( ) .await?; assert_eq!( - admin_vpc[0].network_virtualization_type, + admin_vpc[0].config.network_virtualization_type, VpcVirtualizationType::Fnn ); } diff --git a/crates/api-core/src/tests/vpc.rs b/crates/api-core/src/tests/vpc.rs index 395327beb6..703ce443e8 100644 --- a/crates/api-core/src/tests/vpc.rs +++ b/crates/api-core/src/tests/vpc.rs @@ -32,6 +32,39 @@ use crate::tests::common::api_fixtures::{TestEnvOverrides, create_test_env_with_ use crate::tests::common::rpc_builder::{VpcCreationRequest, VpcDeletionRequest, VpcUpdateRequest}; use crate::{DatabaseError, db_init}; +#[allow(deprecated)] +fn forge_vpc_config(vpc: &rpc::forge::Vpc) -> &rpc::forge::VpcConfig { + vpc.config + .as_ref() + .expect("structured config must be populated") +} + +/// Backware compatibility: deprecated fields mirror structured config/status. +/// TODO Remove after rest component migrates to config/status +#[allow(deprecated)] +fn assert_vpc_config_status_compat(vpc: &rpc::forge::Vpc) { + let config = forge_vpc_config(vpc); + assert_eq!(vpc.tenant_organization_id, config.tenant_organization_id); + assert_eq!(vpc.tenant_keyset_id, config.tenant_keyset_id); + assert_eq!(vpc.vni, config.vni); + assert_eq!( + vpc.network_virtualization_type, + config.network_virtualization_type + ); + assert_eq!( + vpc.network_security_group_id, + config.network_security_group_id + ); + assert_eq!( + vpc.default_nvlink_logical_partition_id, + config.default_nvlink_logical_partition_id + ); + assert_eq!(vpc.routing_profile_type, config.routing_profile_type); + + let status = vpc.status.as_ref().expect("status must be populated"); + assert_eq!(vpc.deprecated_vni, status.vni); +} + #[crate::sqlx_test] async fn create_vpc_for_tenant_without_profile( pool: sqlx::PgPool, @@ -100,6 +133,7 @@ async fn create_vpc_for_tenant_without_profile( } #[crate::sqlx_test] +#[allow(deprecated)] async fn create_vpc(pool: sqlx::PgPool) -> Result<(), Box> { // Build an FNN config with distinct access tiers so the create path // covers the new routing-profile validation. @@ -261,9 +295,10 @@ async fn create_vpc(pool: sqlx::PgPool) -> Result<(), Box .into_inner(); // A VNI is allocated - assert!(forge_vpc.status.and_then(|s| s.vni).is_some()); + assert!(forge_vpc.status.as_ref().and_then(|s| s.vni).is_some()); // The 'config' VNI and the status VNI match - assert_eq!(forge_vpc.vni, forge_vpc.status.and_then(|s| s.vni)); + assert_eq!(forge_vpc.vni, forge_vpc.status.as_ref().and_then(|s| s.vni)); + assert_vpc_config_status_compat(&forge_vpc); // Create another VPC by explicitly selecting a VNI from // the allowed pool, but use the same VNI, so it should fail. @@ -310,11 +345,12 @@ async fn create_vpc(pool: sqlx::PgPool) -> Result<(), Box let version: ConfigVersion = forge_vpc.version.parse()?; assert_eq!(version.version_nr(), 1); // A VNI is allocated - assert!(forge_vpc.status.and_then(|s| s.vni).is_some()); + assert!(forge_vpc.status.as_ref().and_then(|s| s.vni).is_some()); // The 'config' VNI is still None because this was an auto-allocated VNI assert!(forge_vpc.vni.is_none()); // We default to EthernetVirtualizer (proto value 0). assert_eq!(forge_vpc.network_virtualization_type, Some(0)); + assert_vpc_config_status_compat(&forge_vpc); let no_org_vpc = env .api @@ -394,12 +430,12 @@ async fn create_vpc(pool: sqlx::PgPool) -> Result<(), Box // DB value "etv" decodes as EthernetVirtualizer. assert_eq!( - updated_vpc.network_virtualization_type, + updated_vpc.config.network_virtualization_type, VpcVirtualizationType::EthernetVirtualizer ); // Update virtualization type. - let orig_virtualization_type = updated_vpc.network_virtualization_type; + let orig_virtualization_type = updated_vpc.config.network_virtualization_type; let _updated_vpc_virtualization = db::vpc::update_virtualization( &UpdateVpcVirtualization { id: no_org_vpc_id, @@ -417,7 +453,7 @@ async fn create_vpc(pool: sqlx::PgPool) -> Result<(), Box .await?; let first = vpcs.swap_remove(0); assert_eq!( - first.network_virtualization_type, + first.config.network_virtualization_type, VpcVirtualizationType::Fnn ); @@ -440,7 +476,7 @@ async fn create_vpc(pool: sqlx::PgPool) -> Result<(), Box .await?; let first = vpcs.swap_remove(0); assert_eq!( - first.network_virtualization_type, + first.config.network_virtualization_type, VpcVirtualizationType::EthernetVirtualizer ); @@ -602,6 +638,7 @@ async fn create_vpc_without_fnn_rejects_explicit_routing_profile( } #[crate::sqlx_test] +#[allow(deprecated)] async fn create_vpc_with_labels(pool: sqlx::PgPool) -> Result<(), Box> { let env = create_test_env(pool).await; @@ -677,7 +714,22 @@ async fn create_vpc_with_labels(pool: sqlx::PgPool) -> Result<(), Box Result<(), eyre::Report> { let admin_vpc = admin_vpc.remove(0); assert_eq!( - admin_vpc.network_virtualization_type, + admin_vpc.config.network_virtualization_type, VpcVirtualizationType::Fnn ); @@ -916,14 +968,8 @@ async fn create_admin_vpc_updates_existing_admin_vpc_vni( assert_eq!(updated_admin_vpcs.len(), 1); let updated_admin_vpc = updated_admin_vpcs.remove(0); assert_eq!(updated_admin_vpc.id, initial_admin_vpc.id); - assert_eq!(updated_admin_vpc.vni, Some(updated_vni as i32)); - assert_eq!( - updated_admin_vpc - .status - .as_ref() - .and_then(|status| status.vni), - Some(updated_vni as i32) - ); + assert_eq!(updated_admin_vpc.config.vni, Some(updated_vni as i32)); + assert_eq!(updated_admin_vpc.status.vni, Some(updated_vni as i32)); assert!( db::vpc::find_by_vni(&mut txn, initial_vni as i32) .await? @@ -1031,9 +1077,10 @@ async fn create_update_network_security_group_for_vpc( // Make sure the VPC has the security group we expect assert_eq!( - vpc.network_security_group_id.as_deref(), + forge_vpc_config(&vpc).network_security_group_id.as_deref(), Some(good_network_security_group_id) ); + assert_vpc_config_status_compat(&vpc); let vpc_id = vpc.id; @@ -1069,9 +1116,10 @@ async fn create_update_network_security_group_for_vpc( // Make sure the VPC has the security group we expect assert_eq!( - vpc.network_security_group_id.as_deref(), + forge_vpc_config(&vpc).network_security_group_id.as_deref(), Some(good_network_security_group_id) ); + assert_vpc_config_status_compat(&vpc); // Update again to clear the the NSG attachment. let vpc = env @@ -1089,7 +1137,8 @@ async fn create_update_network_security_group_for_vpc( .unwrap(); // Make sure the VPC has no NSG ID - assert!(vpc.network_security_group_id.is_none()); + assert!(forge_vpc_config(&vpc).network_security_group_id.is_none()); + assert_vpc_config_status_compat(&vpc); Ok(()) } @@ -1227,14 +1276,15 @@ async fn create_flat_vpc_succeeds_without_routing_profile( .into_inner(); assert_eq!( - vpc.network_virtualization_type, + forge_vpc_config(&vpc).network_virtualization_type, Some(rpc::forge::VpcVirtualizationType::Flat as i32), ); - assert!(vpc.routing_profile_type.is_none()); + assert!(forge_vpc_config(&vpc).routing_profile_type.is_none()); assert!( vpc.status.as_ref().and_then(|s| s.vni).is_some(), "Flat VPCs still allocate a VNI for pluggable SDN hooks (e.g. switch-side VTEPs)", ); + assert_vpc_config_status_compat(&vpc); Ok(()) } diff --git a/crates/api-core/src/tests/vpc_peering.rs b/crates/api-core/src/tests/vpc_peering.rs index 559f34578d..2a5184077d 100644 --- a/crates/api-core/src/tests/vpc_peering.rs +++ b/crates/api-core/src/tests/vpc_peering.rs @@ -593,19 +593,19 @@ async fn test_vpc_peering_network_config_ordered_peerings( .await? .into_iter() .next() - .and_then(|vpc| vpc.status.and_then(|status| status.vni)) + .and_then(|vpc| vpc.status.vni) .expect("Expected peer vpc 2 vni to be present") as u32; let peer_vpc_vni_3 = db::vpc::find_by_name(&env.pool, "test vpc 3") .await? .into_iter() .next() - .and_then(|vpc| vpc.status.and_then(|status| status.vni)) + .and_then(|vpc| vpc.status.vni) .expect("Expected peer vpc 3 vni to be present") as u32; let peer_vpc_vni_4 = db::vpc::find_by_name(&env.pool, "test vpc 4") .await? .into_iter() .next() - .and_then(|vpc| vpc.status.and_then(|status| status.vni)) + .and_then(|vpc| vpc.status.vni) .expect("Expected peer vpc 4 vni to be present") as u32; // Create VPC Peering between VPC 1 and VPC 2 diff --git a/crates/api-db/src/dpa_interface.rs b/crates/api-db/src/dpa_interface.rs index 5c5221c557..a766e1f2b2 100644 --- a/crates/api-db/src/dpa_interface.rs +++ b/crates/api-db/src/dpa_interface.rs @@ -465,7 +465,7 @@ where let vpc = crate::vpc::find_by_segment(txn, network_segment_id).await?; - match vpc.status.as_ref().and_then(|s| s.vni) { + match vpc.status.vni { Some(vni) => { if vni == 0 { tracing::warn!("Did not expect DPA VNI to be zero"); diff --git a/crates/api-db/src/instance_address.rs b/crates/api-db/src/instance_address.rs index 983e03d183..f2462de7d3 100644 --- a/crates/api-db/src/instance_address.rs +++ b/crates/api-db/src/instance_address.rs @@ -260,7 +260,7 @@ pub async fn allocate( vpcs.len() == vpc_ids.len() && vpcs .iter() - .all(|vpc| vpc.network_virtualization_type == VpcVirtualizationType::Fnn) + .all(|vpc| vpc.config.network_virtualization_type == VpcVirtualizationType::Fnn) } else { false }; diff --git a/crates/api-model/src/vpc/mod.rs b/crates/api-model/src/vpc/mod.rs index ae90262117..4dbb3b754b 100644 --- a/crates/api-model/src/vpc/mod.rs +++ b/crates/api-model/src/vpc/mod.rs @@ -26,6 +26,7 @@ pub use capability::{ use carbide_network::virtualization::VpcVirtualizationType; use carbide_uuid::machine::MachineId; use carbide_uuid::network_security_group::NetworkSecurityGroupId; +use carbide_uuid::nvlink::NvLinkLogicalPartitionId; use carbide_uuid::vpc::VpcId; use carbide_uuid::vpc_peering::VpcPeeringId; use chrono::{DateTime, Utc}; @@ -36,29 +37,33 @@ use sqlx::{FromRow, Row}; use crate::metadata::{LabelFilter, Metadata}; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VpcConfig { + pub tenant_organization_id: String, + pub tenant_keyset_id: Option, + pub network_virtualization_type: VpcVirtualizationType, + pub network_security_group_id: Option, + pub default_nvlink_logical_partition_id: Option, + pub vni: Option, + pub routing_profile_type: Option, +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct VpcStatus { + /// Allocated VNI. pub vni: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Vpc { pub id: VpcId, - pub tenant_organization_id: String, - pub network_security_group_id: Option, pub version: ConfigVersion, + pub config: VpcConfig, + pub status: VpcStatus, + pub metadata: Metadata, pub created: DateTime, pub updated: DateTime, pub deleted: Option>, - pub tenant_keyset_id: Option, - pub network_virtualization_type: VpcVirtualizationType, - pub routing_profile_type: Option, - // Option because we can't allocate it until DB generates an id for us - // TODO: Update - Seems this isn't true since we generate a UUID if not found - // in the original creation request. - pub vni: Option, - pub metadata: Metadata, - pub status: Option, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] @@ -115,26 +120,24 @@ impl<'r> sqlx::FromRow<'r, PgRow> for Vpc { labels: vpc_labels.0, }; - let routing_profile_type: Option = row.try_get("routing_profile_type")?; - let status: Option> = row.try_get("status")?; + let status: sqlx::types::Json = row.try_get("status")?; - // TODO(chet): Once `tenant_keyset_id` is taken care of, - // this entire FromRow implementation can go away with a - // rename of `tenant_organization_id` to match (or just - // a rename of the `organization_id` column). Ok(Vpc { id: row.try_get("id")?, version: row.try_get("version")?, - tenant_organization_id: row.try_get("organization_id")?, - network_security_group_id: row.try_get("network_security_group_id")?, + config: VpcConfig { + tenant_organization_id: row.try_get("organization_id")?, + tenant_keyset_id: None, // TODO: fix this once DB gets updated + network_security_group_id: row.try_get("network_security_group_id")?, + network_virtualization_type: row.try_get("network_virtualization_type")?, + routing_profile_type: row.try_get("routing_profile_type")?, + vni: row.try_get("vni")?, + default_nvlink_logical_partition_id: None, + }, + status: status.0, created: row.try_get("created")?, updated: row.try_get("updated")?, deleted: row.try_get("deleted")?, - tenant_keyset_id: None, //TODO: fix this once DB gets updated - status: status.map(|s| s.0), - network_virtualization_type: row.try_get("network_virtualization_type")?, - routing_profile_type, - vni: row.try_get("vni")?, metadata, }) } diff --git a/crates/api-web/src/ipam.rs b/crates/api-web/src/ipam.rs index 3ee40165f5..61b75fff76 100644 --- a/crates/api-web/src/ipam.rs +++ b/crates/api-web/src/ipam.rs @@ -567,6 +567,12 @@ pub async fn overlay_html(AxumState(state): AxumState>) -> Response { .map(|vpc| { let id = vpc.id.map(|id| id.to_string()).unwrap_or_default(); let prefixes = prefixes_by_vpc.remove(&id).unwrap_or_default(); + #[allow(deprecated)] + let tenant = vpc + .config + .as_ref() + .map(|config| config.tenant_organization_id.clone()) + .unwrap_or_else(|| vpc.tenant_organization_id.clone()); OverlayVpcDisplay { id, name: vpc @@ -580,7 +586,7 @@ pub async fn overlay_html(AxumState(state): AxumState>) -> Response { .and_then(|status| status.vni) .map(|vni| vni.to_string()) .unwrap_or_default(), - tenant: vpc.tenant_organization_id, + tenant, prefixes, } }) diff --git a/crates/api-web/src/tests/vpc.rs b/crates/api-web/src/tests/vpc.rs index f204c13585..3c8f6a702d 100644 --- a/crates/api-web/src/tests/vpc.rs +++ b/crates/api-web/src/tests/vpc.rs @@ -40,6 +40,7 @@ async fn response_body(response: Response) -> String { } #[crate::sqlx_test] +#[allow(deprecated)] async fn vpc_pages_show_status_vni(pool: sqlx::PgPool) { let env = TestEnv::new(pool).await; let app = make_test_app(&env.test_harness); diff --git a/crates/api-web/src/vpc.rs b/crates/api-web/src/vpc.rs index 34b59e6ada..fc8b6b0e4b 100644 --- a/crates/api-web/src/vpc.rs +++ b/crates/api-web/src/vpc.rs @@ -44,19 +44,53 @@ struct VpcRowDisplay { vni: String, } +#[allow(deprecated)] +fn vpc_config(vpc: &forgerpc::Vpc) -> forgerpc::VpcConfig { + if let Some(config) = vpc.config.clone() { + config + } else { + forgerpc::VpcConfig { + tenant_organization_id: vpc.tenant_organization_id.clone(), + tenant_keyset_id: vpc.tenant_keyset_id.clone(), + network_virtualization_type: vpc.network_virtualization_type, + network_security_group_id: vpc.network_security_group_id.clone(), + default_nvlink_logical_partition_id: vpc.default_nvlink_logical_partition_id, + vni: vpc.vni, + routing_profile_type: vpc.routing_profile_type.clone(), + } + } +} + +#[allow(deprecated)] +fn vpc_allocated_vni(vpc: &forgerpc::Vpc) -> Option { + vpc.status + .as_ref() + .and_then(|status| status.vni) + .or(vpc.deprecated_vni) +} + +#[allow(deprecated)] +fn vpc_virt_type(vpc: &forgerpc::Vpc) -> i32 { + vpc_config(vpc) + .network_virtualization_type + .or(vpc.network_virtualization_type) + .unwrap_or_default() +} + impl From for VpcRowDisplay { fn from(vpc: forgerpc::Vpc) -> Self { + let config = vpc_config(&vpc); Self { - network_virtualization_type: format!("{:?}", vpc.network_virtualization_type()), + network_virtualization_type: format!( + "{:?}", + forgerpc::VpcVirtualizationType::try_from(vpc_virt_type(&vpc)).unwrap_or_default() + ), id: vpc.id.unwrap_or_default().to_string(), - metadata: vpc.metadata.unwrap_or_default(), - tenant_organization_id: vpc.tenant_organization_id, - tenant_keyset_id: vpc.tenant_keyset_id.unwrap_or_default(), - routing_profile_type: vpc.routing_profile_type.unwrap_or("None".to_string()), - vni: vpc - .status - .as_ref() - .and_then(|status| status.vni) + metadata: vpc.metadata.clone().unwrap_or_default(), + tenant_organization_id: config.tenant_organization_id, + tenant_keyset_id: config.tenant_keyset_id.unwrap_or_default(), + routing_profile_type: config.routing_profile_type.unwrap_or("None".to_string()), + vni: vpc_allocated_vni(&vpc) .map(|vni| vni.to_string()) .unwrap_or_default(), } @@ -154,20 +188,21 @@ struct VpcDetail { impl From for VpcDetail { fn from(vpc: forgerpc::Vpc) -> Self { + let config = vpc_config(&vpc); Self { - network_virtualization_type: format!("{:?}", vpc.network_virtualization_type()), + network_virtualization_type: format!( + "{:?}", + forgerpc::VpcVirtualizationType::try_from(vpc_virt_type(&vpc)).unwrap_or_default() + ), id: vpc.id.unwrap_or_default().to_string(), - tenant_organization_id: vpc.tenant_organization_id, - tenant_keyset_id: vpc.tenant_keyset_id.unwrap_or_default(), - routing_profile_type: vpc.routing_profile_type.unwrap_or("None".to_string()), - vni: vpc - .status - .as_ref() - .and_then(|status| status.vni) + tenant_organization_id: config.tenant_organization_id, + tenant_keyset_id: config.tenant_keyset_id.unwrap_or_default(), + routing_profile_type: config.routing_profile_type.unwrap_or("None".to_string()), + vni: vpc_allocated_vni(&vpc) .map(|vni| vni.to_string()) .unwrap_or_default(), metadata_detail: super::MetadataDetail { - metadata: vpc.metadata.unwrap_or_default(), + metadata: vpc.metadata.clone().unwrap_or_default(), metadata_version: vpc.version, }, } diff --git a/crates/rpc/build.rs b/crates/rpc/build.rs index aad53fb881..ef260d8fd7 100644 --- a/crates/rpc/build.rs +++ b/crates/rpc/build.rs @@ -411,6 +411,7 @@ fn main() -> Result<(), Box> { .type_attribute("forge.DpaInterface", "#[derive(serde::Serialize)]") .type_attribute("forge.DpaInterfaceList", "#[derive(serde::Serialize)]") .type_attribute("forge.Vpc", "#[derive(serde::Serialize)]") + .type_attribute("forge.VpcConfig", "#[derive(serde::Serialize)]") .type_attribute("forge.VpcStatus", "#[derive(serde::Serialize)]") .type_attribute("forge.VpcList", "#[derive(serde::Serialize)]") .type_attribute( diff --git a/crates/rpc/proto/forge.proto b/crates/rpc/proto/forge.proto index 6c33dd7235..83aba06360 100644 --- a/crates/rpc/proto/forge.proto +++ b/crates/rpc/proto/forge.proto @@ -1488,19 +1488,30 @@ message TenantSearchQuery { optional string tenantOrganizationId = 1; // protolint:disable:this FIELD_NAMES_LOWER_SNAKE_CASE } -// +message VpcConfig { + string tenant_organization_id = 1; + optional string tenant_keyset_id = 2; + optional VpcVirtualizationType network_virtualization_type = 3; + optional string network_security_group_id = 4; + optional common.NVLinkLogicalPartitionId default_nvlink_logical_partition_id = 5; + // Desired VNI for this VPC. Only populated when explicitly requested during creation. + optional uint32 vni = 6; + optional string routing_profile_type = 7; +} + message VpcStatus { // The actual VNI allocated to the VPC. If a VNI had been - // explicitly requested, we'd expect this to match the VNI - // of the Vpc message. If not explicitly requested, we'd - // expect the VNI of the Vpc message to be unset. + // explicitly requested, we'd expect this to match config.vni. + // If not explicitly requested, we'd expect config.vni to be unset. optional uint32 vni = 1; } message Vpc { common.VpcId id = 1; reserved 2; // was: string name, replaced with metadata.name - string tenantOrganizationId = 3; // protolint:disable:this FIELD_NAMES_LOWER_SNAKE_CASE + // Deprecated: use config.tenant_organization_id + // TODO change to reserved once rest component uses `VpcConfig` + string tenantOrganizationId = 3 [deprecated = true]; // protolint:disable:this FIELD_NAMES_LOWER_SNAKE_CASE string version = 99; @@ -1509,34 +1520,41 @@ message Vpc { google.protobuf.Timestamp updated = 5; google.protobuf.Timestamp deleted = 6; - optional string tenantKeysetId = 7; // protolint:disable:this FIELD_NAMES_LOWER_SNAKE_CASE + // Deprecated: use config.tenant_keyset_id + // TODO Change to reserved once rest component uses `VpcConfig` + optional string tenantKeysetId = 7 [deprecated = true]; // protolint:disable:this FIELD_NAMES_LOWER_SNAKE_CASE - // We'll keep populating this field but it will come from - // status.vni - // We'll be able to trim it out later after some deprecation window. - optional uint32 deprecated_vni = 8; // Deprecated + // Deprecated: use status.vni + // TODO change to reserved once rest component uses `VpcStatus` + optional uint32 deprecated_vni = 8 [deprecated = true]; - optional VpcVirtualizationType network_virtualization_type = 9; + // Deprecated: use config.network_virtualization_type + // TODO change to reserved once rest component uses `VpcConfig` + optional VpcVirtualizationType network_virtualization_type = 9 [deprecated = true]; Metadata metadata = 10; - // Sets the desired NSG ID for a VPC - optional string network_security_group_id = 11; + // Deprecated: use config.network_security_group_id + // TODO Change to reserved once rest component uses `VpcConfig` + optional string network_security_group_id = 11 [deprecated = true]; // dpa_vni reserved 12; - // The ID of the default NVLink Logical Partition for a VPC - // This is the NVLink Logical Partition that will be used by default for all instances in the VPC. - optional common.NVLinkLogicalPartitionId default_nvlink_logical_partition_id = 13; + // Deprecated: use config.default_nvlink_logical_partition_id + // TODO change to reserved once rest component uses `VpcConfig` + optional common.NVLinkLogicalPartitionId default_nvlink_logical_partition_id = 13 [deprecated = true]; optional VpcStatus status = 14; - // This will only be populated if the VNI was explicitly - // requested during creation of the VPC. - optional uint32 vni = 15; + // Deprecated: use config.vni + // TODO change to reserved once rest component uses `VpcConfig` + optional uint32 vni = 15 [deprecated = true]; + + // Deprecated: use config.routing_profile_type + // TODO change to reserved once rest-component uses `VpcConfig` + optional string routing_profile_type = 16 [deprecated = true]; - // The resolved routing profile type applied to this VPC. - optional string routing_profile_type = 16; + VpcConfig config = 17; } message VpcCreationRequest { diff --git a/crates/rpc/src/model/vpc.rs b/crates/rpc/src/model/vpc.rs index 3d47e8a357..cfb7909676 100644 --- a/crates/rpc/src/model/vpc.rs +++ b/crates/rpc/src/model/vpc.rs @@ -36,46 +36,64 @@ impl From for VpcSearchFilter { } } +#[allow(deprecated)] impl From for rpc::forge::Vpc { fn from(src: Vpc) -> Self { + let allocated_vni = src.status.vni.map(|v| v as u32); + let desired_vni = src.config.vni.map(|v| v as u32); + let virt_type = + rpc::forge::VpcVirtualizationType::from(src.config.network_virtualization_type) as i32; + let nsg_id = src + .config + .network_security_group_id + .map(|nsg_id| nsg_id.to_string()); + let metadata = Some(rpc::Metadata { + name: src.metadata.name, + description: src.metadata.description, + labels: src + .metadata + .labels + .iter() + .map(|(key, value)| rpc::forge::Label { + key: key.clone(), + value: if value.clone().is_empty() { + None + } else { + Some(value.clone()) + }, + }) + .collect(), + }); + rpc::forge::Vpc { id: Some(src.id), version: src.version.version_string(), - tenant_organization_id: src.tenant_organization_id, - network_security_group_id: src - .network_security_group_id - .map(|nsg_id| nsg_id.to_string()), created: Some(src.created.into()), updated: Some(src.updated.into()), deleted: src.deleted.map(|t| t.into()), - tenant_keyset_id: src.tenant_keyset_id, - deprecated_vni: src.status.as_ref().and_then(|x| x.vni.map(|v| v as u32)), - vni: src.vni.map(|x| x as u32), - network_virtualization_type: Some( - rpc::forge::VpcVirtualizationType::from(src.network_virtualization_type).into(), - ), - status: src.status.map(rpc::forge::VpcStatus::from), - routing_profile_type: src.routing_profile_type, - metadata: { - Some(rpc::Metadata { - name: src.metadata.name, - description: src.metadata.description, - labels: src - .metadata - .labels - .iter() - .map(|(key, value)| rpc::forge::Label { - key: key.clone(), - value: if value.clone().is_empty() { - None - } else { - Some(value.clone()) - }, - }) - .collect(), - }) - }, - default_nvlink_logical_partition_id: None, + metadata, + + config: Some(rpc::forge::VpcConfig { + tenant_organization_id: src.config.tenant_organization_id.clone(), + tenant_keyset_id: src.config.tenant_keyset_id.clone(), + network_virtualization_type: Some(virt_type), + network_security_group_id: nsg_id.clone(), + default_nvlink_logical_partition_id: src.config.default_nvlink_logical_partition_id, + vni: desired_vni, + routing_profile_type: src.config.routing_profile_type.clone(), + }), + status: Some(rpc::forge::VpcStatus::from(src.status)), + + // Deprecated flat fields - populated for external client compatibility. + // Remove after rest component use VpcConfig/VpcStatus + tenant_organization_id: src.config.tenant_organization_id, + tenant_keyset_id: src.config.tenant_keyset_id, + deprecated_vni: allocated_vni, + vni: desired_vni, + network_virtualization_type: Some(virt_type), + network_security_group_id: nsg_id, + default_nvlink_logical_partition_id: src.config.default_nvlink_logical_partition_id, + routing_profile_type: src.config.routing_profile_type, } } } @@ -233,10 +251,65 @@ impl From for rpc::forge::VpcPeering { #[cfg(test)] mod tests { + use carbide_network::virtualization::VpcVirtualizationType; use carbide_test_support::value_scenarios; + use carbide_uuid::vpc::VpcId; + use model::vpc::VpcConfig; use super::*; + fn sample_vpc() -> Vpc { + Vpc { + id: VpcId::from(uuid::Uuid::new_v4()), + version: ConfigVersion::initial(), + config: VpcConfig { + tenant_organization_id: "tenant-1".to_string(), + tenant_keyset_id: Some("keyset-1".to_string()), + network_virtualization_type: VpcVirtualizationType::Fnn, + network_security_group_id: None, + default_nvlink_logical_partition_id: None, + vni: Some(42), + routing_profile_type: Some("EXTERNAL".to_string()), + }, + status: VpcStatus { vni: Some(100) }, + metadata: Metadata::new_with_default_name(), + created: chrono::Utc::now(), + updated: chrono::Utc::now(), + deleted: None, + } + } + + #[test] + #[allow(deprecated)] + fn vpc_to_rpc_populates_structured_and_deprecated_flat_fields() { + let vpc = sample_vpc(); + let rpc_vpc = rpc::forge::Vpc::from(vpc); + + let config = rpc_vpc.config.as_ref().expect("config must be set"); + assert_eq!(config.tenant_organization_id, "tenant-1"); + assert_eq!(config.tenant_keyset_id.as_deref(), Some("keyset-1")); + assert_eq!(config.vni, Some(42)); + assert_eq!(config.routing_profile_type.as_deref(), Some("EXTERNAL")); + assert_eq!( + config.network_virtualization_type, + Some(rpc::forge::VpcVirtualizationType::Fnn as i32) + ); + + let status = rpc_vpc.status.as_ref().expect("status must be set"); + assert_eq!(status.vni, Some(100)); + + assert_eq!(rpc_vpc.tenant_organization_id, "tenant-1"); + assert_eq!(rpc_vpc.tenant_keyset_id.as_deref(), Some("keyset-1")); + assert_eq!(rpc_vpc.vni, Some(42)); + assert_eq!(rpc_vpc.deprecated_vni, Some(100)); + assert_eq!(rpc_vpc.routing_profile_type.as_deref(), Some("EXTERNAL")); + assert_eq!( + rpc_vpc.network_virtualization_type, + Some(rpc::forge::VpcVirtualizationType::Fnn as i32) + ); + assert_eq!(status.vni, rpc_vpc.deprecated_vni); + } + // `VpcSearchFilter::from` is a total conversion, so we project its output to // the fields the originals asserted: name, tenant_org_id, and the label as its // (key, value) pair (None when no label is present).