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).