diff --git a/rest-api/Makefile b/rest-api/Makefile index 791bc07bc0..8870b23b61 100644 --- a/rest-api/Makefile +++ b/rest-api/Makefile @@ -340,7 +340,8 @@ core-protogen: .tools-stamp echo "Generating protobuf for Core" cd proto/core && PATH="$(TOOLS_DIR):$$PATH" $(BUF) generate go fmt ./... - python3 scripts/check_source_headers.py --fix + python3 scripts/check_source_headers.py --fix \ + --include-untracked 'proto/core/gen/v1/*.pb.go' flow-proto: $(MAKE) flow-proto-clean @@ -893,7 +894,9 @@ generate-sdk: $(OPENAPI_GENERATOR_JAR) --additional-properties=isGoSubmodule=true,enumClassPrefix=true \ --global-property=apis,models,supportingFiles rm -rf sdk/standard/docs sdk/standard/api sdk/standard/README.md sdk/standard/test sdk/standard/.openapi-generator - python3 scripts/check_source_headers.py --fix + python3 scripts/check_source_headers.py --fix \ + --include-untracked 'sdk/standard/api_*.go' \ + --include-untracked 'sdk/standard/model_*.go' @echo "Client generated in sdk/standard/" cd sdk/standard && go build ./... @echo "Client compiles successfully" diff --git a/rest-api/api/pkg/api/handler/vpc.go b/rest-api/api/pkg/api/handler/vpc.go index 5571e1def2..f53305ff84 100644 --- a/rest-api/api/pkg/api/handler/vpc.go +++ b/rest-api/api/pkg/api/handler/vpc.go @@ -234,16 +234,34 @@ func (cvh CreateVPCHandler) Handle(c echo.Context) error { } } - var routingProfile *string - if apiRequest.RoutingProfile != nil { - // For now, we gate on TargetedInstanceCreation permission, - // which must be effective for the VPC's Site. - enabledForSite, derr := common.TenantHasTargetedInstanceCreation(ctx, nil, cvh.dbSession, tenant, &common.TenantPrivilegeScope{SiteID: &site.ID}) - if derr != nil { - logger.Error().Err(derr).Msg("error checking effective targeted instance creation for Site") + // TargetedInstanceCreation supplies both policies, but keep write authorization + // separate from effective-profile response visibility. + tenantCanSetRoutingProfile := false + tenantCanViewEffectiveRoutingProfile := false + if cdbm.VpcTypeSupportsRoutingProfile(networkVirtualizationType) || apiRequest.RoutingProfile != nil { + tenantHasTargetedInstanceCreation, err := common.TenantHasTargetedInstanceCreation(ctx, nil, cvh.dbSession, tenant, &common.TenantPrivilegeScope{SiteID: &site.ID}) + if err != nil { + logger.Error().Err(err).Msg("error resolving TargetedInstanceCreation for Tenant/Site") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to verify privileges for Site", nil) } - if !enabledForSite { + tenantCanSetRoutingProfile = tenantHasTargetedInstanceCreation + tenantCanViewEffectiveRoutingProfile = tenantHasTargetedInstanceCreation + } + + if apiRequest.RoutingProfileOverrides != nil && !cdbm.VpcTypeSupportsRoutingProfile(networkVirtualizationType) { + logger.Warn().Str("networkVirtualizationType", *networkVirtualizationType).Msg("routing profile overrides are not supported for network virtualization type") + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Routing profile overrides are not supported for network virtualization type: %s", *networkVirtualizationType), nil) + } + + if apiRequest.RoutingProfileOverrides != nil && !tenantCanSetRoutingProfile { + logger.Warn().Msg("tenant does not have sufficient privileges to set `routingProfileOverrides`") + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Tenant does not have sufficient privileges to set `routingProfileOverrides`", nil) + } + + var routingProfile *string + if apiRequest.RoutingProfile != nil { + // Routing-profile writes require TargetedInstanceCreation for the VPC's Site. + if !tenantCanSetRoutingProfile { logger.Warn().Msg("tenant does not have sufficient privileges to set `routingProfile`") return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Tenant does not have sufficient privileges to set `routingProfile`", nil) } @@ -332,6 +350,7 @@ func (cvh CreateVPCHandler) Handle(c echo.Context) error { SiteID: site.ID, NetworkVirtualizationType: networkVirtualizationType, RoutingProfile: routingProfile, + RoutingProfileOverrides: apiRequest.RoutingProfileOverrides.ToDB(), NVLinkLogicalPartitionID: defaultNvllPartitionId, Labels: labels, Status: cdbm.VpcStatusProvisioning, @@ -438,15 +457,20 @@ func (cvh CreateVPCHandler) Handle(c echo.Context) error { statusDetails := []cdbm.StatusDetail{*ssd} - // Make a best-effort attempt to return a response with the allocated VNI. + // Make a best-effort attempt to cache the controller-reported VNI and + // effective routing profile for the response. controllerVpcModel := &cdbm.Vpc{} controllerVpcModel.FromProto(controllerVpc) activeVni := controllerVpcModel.ActiveVni - if activeVni != nil { + effectiveRoutingProfile := controllerVpcModel.EffectiveRoutingProfile + if activeVni != nil || effectiveRoutingProfile != nil { uvpcInput := cdbm.VpcUpdateInput{ - VpcID: vpc.ID, - ActiveVni: activeVni, - Status: cutil.GetPtr(cdbm.VpcStatusReady), + VpcID: vpc.ID, + ActiveVni: activeVni, + EffectiveRoutingProfile: effectiveRoutingProfile, + } + if activeVni != nil { + uvpcInput.Status = cutil.GetPtr(cdbm.VpcStatusReady) } updatedVpc, err := vpcDAO.Update(ctx, nil, uvpcInput) if err != nil { @@ -455,20 +479,22 @@ func (cvh CreateVPCHandler) Handle(c echo.Context) error { // Update the vpc being returned if all went well. vpc = updatedVpc - // Best effort create status detail - ssd, err = sdDAO.Create(ctx, nil, cdbm.StatusDetailCreateInput{EntityID: vpc.ID.String(), Status: cdbm.VpcStatusReady, Message: cutil.GetPtr("VPC is ready for use")}) - if err != nil { - logger.Error().Err(err).Msg("error creating Status Detail DB entry") - } else if ssd == nil { - logger.Error().Err(err).Msg("unexpected nil Status Detail returned from DB") - } else { - statusDetails = append(statusDetails, *ssd) + if activeVni != nil { + // Best effort create status detail + ssd, err = sdDAO.Create(ctx, nil, cdbm.StatusDetailCreateInput{EntityID: vpc.ID.String(), Status: cdbm.VpcStatusReady, Message: cutil.GetPtr("VPC is ready for use")}) + if err != nil { + logger.Error().Err(err).Msg("error creating Status Detail DB entry") + } else if ssd == nil { + logger.Error().Err(err).Msg("unexpected nil Status Detail returned from DB") + } else { + statusDetails = append(statusDetails, *ssd) + } } } } // Create response - apiVpc := model.NewAPIVpc(*vpc, statusDetails) + apiVpc := model.NewAPIVpc(*vpc, statusDetails, tenantCanViewEffectiveRoutingProfile) logger.Info().Msg("finishing API handler") @@ -588,6 +614,34 @@ func (uvh UpdateVPCHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "VPC does not belong to current Tenant", nil) } + // TargetedInstanceCreation supplies both policies, but keep write authorization + // separate from effective-profile response visibility. + tenantCanSetRoutingProfile := false + tenantCanViewEffectiveRoutingProfile := false + if cdbm.VpcTypeSupportsRoutingProfile(vpc.NetworkVirtualizationType) { + tenantHasTargetedInstanceCreation, err := common.TenantHasTargetedInstanceCreation(ctx, nil, uvh.dbSession, tenant, &common.TenantPrivilegeScope{SiteID: &vpc.SiteID}) + if err != nil { + logger.Error().Err(err).Msg("error resolving TargetedInstanceCreation for Tenant/Site") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to resolve Tenant capability, DB error", nil) + } + tenantCanSetRoutingProfile = tenantHasTargetedInstanceCreation + tenantCanViewEffectiveRoutingProfile = tenantHasTargetedInstanceCreation + } + + if apiRequest.RoutingProfileOverrides != nil && !cdbm.VpcTypeSupportsRoutingProfile(vpc.NetworkVirtualizationType) { + networkVirtualizationType := "" + if vpc.NetworkVirtualizationType != nil { + networkVirtualizationType = *vpc.NetworkVirtualizationType + } + logger.Warn().Str("networkVirtualizationType", networkVirtualizationType).Msg("routing profile overrides are not supported for network virtualization type") + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Routing profile overrides are not supported for network virtualization type: %s", networkVirtualizationType), nil) + } + + if apiRequest.RoutingProfileOverrides != nil && !tenantCanSetRoutingProfile { + logger.Warn().Msg("tenant does not have sufficient privileges to set `routingProfileOverrides`") + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Tenant does not have sufficient privileges to set `routingProfileOverrides`", nil) + } + // Ensure that Tenant has an Allocation with specified Site aDAO := cdbm.NewAllocationDAO(uvh.dbSession) allocationFilter := cdbm.AllocationFilterInput{TenantIDs: []uuid.UUID{tenant.ID}, SiteIDs: []uuid.UUID{vpc.SiteID}} @@ -739,11 +793,12 @@ func (uvh UpdateVPCHandler) Handle(c echo.Context) error { err = cdb.WithTx(ctx, uvh.dbSession, func(tx *cdb.Tx) error { // Update VPC uvpcInput := cdbm.VpcUpdateInput{ - VpcID: vpc.ID, - Name: apiRequest.Name, - Description: apiRequest.Description, - Labels: labels, - NetworkSecurityGroupID: nsgID, + VpcID: vpc.ID, + Name: apiRequest.Name, + Description: apiRequest.Description, + Labels: labels, + NetworkSecurityGroupID: nsgID, + RoutingProfileOverrides: apiRequest.RoutingProfileOverrides.ToDB(), } if defaultNvllPartitionId != nil { @@ -779,6 +834,12 @@ func (uvh UpdateVPCHandler) Handle(c echo.Context) error { shouldClear = true } + // A changed desired definition invalidates the last controller-resolved value. + if apiRequest.RoutingProfileOverrides != nil { + clearInput.EffectiveRoutingProfile = true + shouldClear = true + } + // Clear it in the db if something should be cleared. if shouldClear { clearedVpc, derr := vpcDAO.Clear(ctx, tx, clearInput) @@ -863,7 +924,7 @@ func (uvh UpdateVPCHandler) Handle(c echo.Context) error { } // Create response - apiVpc := model.NewAPIVpc(*vpc, ssds) + apiVpc := model.NewAPIVpc(*vpc, ssds, tenantCanViewEffectiveRoutingProfile) logger.Info().Msg("finishing API handler") return c.JSON(http.StatusOK, apiVpc) @@ -982,6 +1043,12 @@ func (uvvh UpdateVPCVirtualizationHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "VPC does not belong to current Tenant", nil) } + tenantCanViewEffectiveRoutingProfile, err := common.TenantHasTargetedInstanceCreation(ctx, nil, uvvh.dbSession, tenant, &common.TenantPrivilegeScope{SiteID: &vpc.SiteID}) + if err != nil { + logger.Error().Err(err).Msg("error resolving TargetedInstanceCreation for Tenant/Site") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to resolve Tenant capability, DB error", nil) + } + // Ensure that Tenant has access to Site tsDAO := cdbm.NewTenantSiteDAO(uvvh.dbSession) _, err = tsDAO.GetByTenantIDAndSiteID(ctx, nil, tenant.ID, vpc.SiteID, nil) @@ -1132,7 +1199,7 @@ func (uvvh UpdateVPCVirtualizationHandler) Handle(c echo.Context) error { } // Create response - apiVpc := model.NewAPIVpc(*uv, ssds) + apiVpc := model.NewAPIVpc(*uv, ssds, tenantCanViewEffectiveRoutingProfile) logger.Info().Msg("finishing API handler") return c.JSON(http.StatusOK, apiVpc) @@ -1242,6 +1309,17 @@ func (gvh GetVPCHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "VPC does not belong to current Tenant", nil) } + tenantCanViewEffectiveRoutingProfile := false + // Existing non-FNN rows can carry cached effective state, so gate that state + // instead of hiding it solely based on VPC type. + if cdbm.VpcTypeSupportsRoutingProfile(vpc.NetworkVirtualizationType) || vpc.EffectiveRoutingProfile != nil { + tenantCanViewEffectiveRoutingProfile, err = common.TenantHasTargetedInstanceCreation(ctx, nil, gvh.dbSession, tenant, &common.TenantPrivilegeScope{SiteID: &vpc.SiteID}) + if err != nil { + logger.Error().Err(err).Msg("error resolving TargetedInstanceCreation for Tenant/Site") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to resolve Tenant capability, DB error", nil) + } + } + // Get status details sdDAO := cdbm.NewStatusDetailDAO(gvh.dbSession) @@ -1252,7 +1330,7 @@ func (gvh GetVPCHandler) Handle(c echo.Context) error { } // Create response - vc := model.NewAPIVpc(*vpc, ssds) + vc := model.NewAPIVpc(*vpc, ssds, tenantCanViewEffectiveRoutingProfile) logger.Info().Msg("finishing API handler") @@ -1415,6 +1493,16 @@ func (gavh GetAllVPCHandler) Handle(c echo.Context) error { } tenant := tenants[0] + privilegedSiteIDs, err := common.GetPrivilegedAccessSiteIDsForTenant(ctx, nil, gavh.dbSession, &tenant) + if err != nil { + logger.Error().Err(err).Msg("error resolving privileged Site access for Tenant") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to resolve Tenant capability, DB error", nil) + } + privilegedSites := make(map[uuid.UUID]bool, len(privilegedSiteIDs)) + for _, siteID := range privilegedSiteIDs { + privilegedSites[siteID] = true + } + // Get all VPCs by Tenant, and Site, if specified vpcDAO := cdbm.NewVpcDAO(gavh.dbSession) @@ -1531,7 +1619,7 @@ func (gavh GetAllVPCHandler) Handle(c echo.Context) error { apiVpcs := []model.APIVpc{} for _, vpc := range vpcs { - apiVpc := model.NewAPIVpc(vpc, ssdMap[vpc.ID.String()]) + apiVpc := model.NewAPIVpc(vpc, ssdMap[vpc.ID.String()], privilegedSites[vpc.SiteID]) apiVpcs = append(apiVpcs, apiVpc) } diff --git a/rest-api/api/pkg/api/handler/vpc_test.go b/rest-api/api/pkg/api/handler/vpc_test.go index 3217b1d1b9..2ecb0d6eeb 100644 --- a/rest-api/api/pkg/api/handler/vpc_test.go +++ b/rest-api/api/pkg/api/handler/vpc_test.go @@ -39,6 +39,7 @@ import ( temporalClient "go.temporal.io/sdk/client" tmocks "go.temporal.io/sdk/mocks" tp "go.temporal.io/sdk/temporal" + "google.golang.org/protobuf/proto" authz "github.com/NVIDIA/infra-controller/rest-api/auth/pkg/authorization" oteltrace "go.opentelemetry.io/otel/trace" @@ -53,7 +54,8 @@ func testVPCInitDB(t *testing.T) *cdb.Session { return dbSession } -// reset the tables needed for Allocation tests +// testVPCSetupSchema resets the tables required by VPC handler and +// privilege-resolution tests. func testVPCSetupSchema(t *testing.T, dbSession *cdb.Session) { // create Infrastructure Provider table err := dbSession.DB.ResetModel(context.Background(), (*cdbm.InfrastructureProvider)(nil)) @@ -67,6 +69,12 @@ func testVPCSetupSchema(t *testing.T, dbSession *cdb.Session) { // create User table err = dbSession.DB.ResetModel(context.Background(), (*cdbm.User)(nil)) assert.Nil(t, err) + // create Tenant Account table used for effective per-Site privilege resolution + err = dbSession.DB.ResetModel(context.Background(), (*cdbm.TenantAccount)(nil)) + assert.Nil(t, err) + // create Tenant Site table used for per-Site privilege overrides + err = dbSession.DB.ResetModel(context.Background(), (*cdbm.TenantSite)(nil)) + assert.Nil(t, err) // create Allocation table err = dbSession.DB.ResetModel(context.Background(), (*cdbm.Allocation)(nil)) assert.Nil(t, err) @@ -318,9 +326,9 @@ func TestCreateVPCHandler_Handle(t *testing.T) { tnu := testVPCBuildUser(t, dbSession, "test-starfleet-id-2", tnOrg, tnOrgRoles) tn := testVPCBuildTenant(t, dbSession, "test-tenant", tnOrg, tnu) - // Privilege for `routingProfile` is resolved site-scoped. TenantSite - // associations without an explicit override inherit this Ready - // TenantAccount default. + // Routing-profile write privilege is resolved site-scoped. TenantSite + // associations without an explicit override inherit this Ready TenantAccount + // default. _ = common.TestBuildTenantAccountWithTargetedInstanceCreation(t, dbSession, ip, &tn.ID, tnOrg, cdbm.TenantAccountStatusReady, tnu) tnu2 := testVPCBuildUser(t, dbSession, "test-starfleet-id-3", tnOrg, tnOrgRoles) @@ -406,6 +414,8 @@ func TestCreateVPCHandler_Handle(t *testing.T) { scp.IDClientMap[st3.ID.String()] = tst3 vpcWithAllocatedVniName := "Test VPC with allocated VNI" + vpcWithRoutingProfileName := "Test VPC routing profile" + vpcWithRoutingProfileOverridesName := "Test VPC routing profile overrides" allocatedVni := uint32(7301) expectedAllocatedVni := int(allocatedVni) @@ -426,6 +436,21 @@ func TestCreateVPCHandler_Handle(t *testing.T) { } }).Return(nil) + wrunWithEffectiveProfile := &tmocks.WorkflowRun{} + wrunWithEffectiveProfile.On("GetID").Return(wid) + wrunWithEffectiveProfile.Mock.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + controllerVpc, ok := args.Get(1).(*corev1.Vpc) + if ok { + controllerVpc.Status = &corev1.VpcStatus{ + EffectiveRoutingProfile: &corev1.VpcEffectiveRoutingProfile{ + LeakDefaultRouteFromUnderlay: true, + Internal: true, + AccessTier: 6, + }, + } + } + }).Return(nil) + tc.Mock.On("ExecuteWorkflow", mock.Anything, mock.AnythingOfType("internal.StartWorkflowOptions"), mock.AnythingOfType("func(internal.Context, uuid.UUID, uuid.UUID) error"), mock.AnythingOfType("uuid.UUID"), mock.AnythingOfType("uuid.UUID")).Return(wrun, nil) @@ -437,7 +462,12 @@ func TestCreateVPCHandler_Handle(t *testing.T) { tsc.Mock.On("ExecuteWorkflow", mock.Anything, mock.AnythingOfType("internal.StartWorkflowOptions"), "CreateVPCV2", mock.MatchedBy(func(req *corev1.VpcCreationRequest) bool { - return req == nil || req.Name != vpcWithAllocatedVniName + return req != nil && (req.Name == vpcWithRoutingProfileName || req.Name == vpcWithRoutingProfileOverridesName) + })).Return(wrunWithEffectiveProfile, nil) + + tsc.Mock.On("ExecuteWorkflow", mock.Anything, mock.AnythingOfType("internal.StartWorkflowOptions"), + "CreateVPCV2", mock.MatchedBy(func(req *corev1.VpcCreationRequest) bool { + return req == nil || (req.Name != vpcWithAllocatedVniName && req.Name != vpcWithRoutingProfileName && req.Name != vpcWithRoutingProfileOverridesName) })).Return(wrun, nil) // Mock timeout error @@ -453,6 +483,11 @@ func TestCreateVPCHandler_Handle(t *testing.T) { // OTEL Spanner configuration tracer, _, ctx := common.TestCommonTraceProviderSetup(t, ctx) + routingProfileOverrides := &model.APIVpcRoutingProfileOverrides{ + RouteTargetImports: &model.APIVpcRouteTargets{{ASN: 64512, VNI: 559}}, + LeakDefaultRouteFromUnderlay: cutil.GetPtr(false), + AllowedAnycastPrefixes: &[]string{"192.0.2.1/24"}, + } tests := []struct { name string @@ -460,6 +495,7 @@ func TestCreateVPCHandler_Handle(t *testing.T) { args args wantErr bool verifyChildSpanner bool + expectNoMutation bool }{ { name: "test VPC create API endpoint success", @@ -496,6 +532,36 @@ func TestCreateVPCHandler_Handle(t *testing.T) { wantErr: false, verifyChildSpanner: true, }, + // Override-only writes use the same site-scoped privilege as named profiles. + { + name: "test VPC create API endpoint with routing profile overrides success", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIVpcCreateRequest{ + Name: vpcWithRoutingProfileOverridesName, + Description: cutil.GetPtr("Test VPC Description"), + SiteID: st1.ID.String(), + NetworkVirtualizationType: cutil.GetPtr(cdbm.VpcFNN), + RoutingProfileOverrides: routingProfileOverrides, + }, + reqOrg: tnOrg, + reqUser: tnu, + respCode: http.StatusCreated, + expectedStatus: cdbm.VpcStatusProvisioning, + expectedStatusDetails: []expectedStatusDetail{ + { + status: cdbm.VpcStatusProvisioning, + message: "VPC provisioning has been initiated on Site", + }, + }, + }, + wantErr: false, + verifyChildSpanner: true, + }, { name: "test VPC create API endpoint returns allocated VNI from Site workflow response", fields: fields{ @@ -535,6 +601,30 @@ func TestCreateVPCHandler_Handle(t *testing.T) { wantErr: false, verifyChildSpanner: true, }, + // Override-only writes must not bypass the named-profile privilege gate. + { + name: "test VPC create API endpoint rejects routing profile overrides without targeted instance creation", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIVpcCreateRequest{ + Name: "Test VPC restricted routing profile overrides", + SiteID: st1.ID.String(), + NetworkVirtualizationType: cutil.GetPtr(cdbm.VpcFNN), + RoutingProfileOverrides: routingProfileOverrides, + }, + reqOrg: tnOrg3, + reqUser: tnu3, + respCode: http.StatusForbidden, + respMessage: "Tenant does not have sufficient privileges to set `routingProfileOverrides`", + }, + wantErr: false, + expectNoMutation: true, + }, + // A privileged FNN create forwards and persists every supplied override. { name: "test VPC create API endpoint with routing profile success", fields: fields{ @@ -544,13 +634,14 @@ func TestCreateVPCHandler_Handle(t *testing.T) { }, args: args{ reqData: &model.APIVpcCreateRequest{ - Name: "Test VPC routing profile", + Name: vpcWithRoutingProfileName, Description: cutil.GetPtr("Test VPC Description"), SiteID: st1.ID.String(), NetworkVirtualizationType: cutil.GetPtr(cdbm.VpcFNN), NetworkSecurityGroupID: &nsgTenant1Site1.ID, Vni: cutil.GetPtr(559), RoutingProfile: cutil.GetPtr(model.APIVpcRoutingProfileInternal), + RoutingProfileOverrides: routingProfileOverrides, Labels: map[string]string{ "vpc-dpu-zone": "east1", "vpc-gpu-zone": "west1", @@ -744,6 +835,28 @@ func TestCreateVPCHandler_Handle(t *testing.T) { wantErr: false, verifyChildSpanner: true, }, + // Handler-side defaulting reports the resolved unsupported type. + { + name: "test VPC create API endpoint rejects routing profile overrides when type defaults to ethernet", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIVpcCreateRequest{ + Name: "Test VPC default ethernet routing profile overrides", + SiteID: st3.ID.String(), + RoutingProfileOverrides: routingProfileOverrides, + }, + reqOrg: tnOrg, + reqUser: tnu, + respCode: http.StatusBadRequest, + respMessage: "Routing profile overrides are not supported for network virtualization type: ETHERNET_VIRTUALIZER", + }, + wantErr: false, + expectNoMutation: true, + }, { name: "test VPC create API endpoint with explicit VPC ID success", fields: fields{ @@ -1076,6 +1189,18 @@ func TestCreateVPCHandler_Handle(t *testing.T) { if tt.args.respMessage != "" { assert.Contains(t, rec.Body.String(), tt.args.respMessage) } + if tt.expectNoMutation { + // Authorization and compatibility failures must precede persistence and workflow dispatch. + persistedVpcs, total, gerr := cdbm.NewVpcDAO(tt.fields.dbSession).GetAll(ctx, nil, cdbm.VpcFilterInput{Name: &tt.args.reqData.Name}, paginator.PageInput{}, nil) + require.NoError(t, gerr) + assert.Zero(t, total) + assert.Empty(t, persistedVpcs) + requestMatcher := mock.MatchedBy(func(req *corev1.VpcCreationRequest) bool { + return req != nil && req.Name == tt.args.reqData.Name + }) + tsc.AssertNotCalled(t, "ExecuteWorkflow", mock.Anything, mock.AnythingOfType("internal.StartWorkflowOptions"), "CreateVPCV2", requestMatcher) + tst3.AssertNotCalled(t, "ExecuteWorkflow", mock.Anything, mock.AnythingOfType("internal.StartWorkflowOptions"), "CreateVPCV2", requestMatcher) + } if tt.args.respCode != http.StatusCreated { return } @@ -1100,6 +1225,7 @@ func TestCreateVPCHandler_Handle(t *testing.T) { assert.Nil(t, rst.Description) } assert.Equal(t, tt.args.reqData.RoutingProfile, rst.RoutingProfile) + assert.Equal(t, tt.args.reqData.RoutingProfileOverrides, rst.RoutingProfileOverrides) if tt.args.reqData.NetworkVirtualizationType != nil { assert.Equal(t, rst.NetworkVirtualizationType, tt.args.reqData.NetworkVirtualizationType) } else { @@ -1129,10 +1255,25 @@ func TestCreateVPCHandler_Handle(t *testing.T) { assert.Equal(t, len(rst.Labels), len(tt.args.reqData.Labels)) } + // Read the row independently so the create response cannot hide a persistence defect. + persistedVpc, gerr := cdbm.NewVpcDAO(tt.fields.dbSession).GetByID(ctx, nil, uuid.MustParse(rst.ID), nil) + require.NoError(t, gerr) + assert.Equal(t, tt.args.reqData.RoutingProfileOverrides.ToDB(), persistedVpc.RoutingProfileOverrides) + if tt.args.reqData.RoutingProfileOverrides != nil { + // Effective state returned without a VNI is cached and exposed to this privileged tenant. + require.NotNil(t, rst.EffectiveRoutingProfile) + assert.Equal(t, 6, rst.EffectiveRoutingProfile.AccessTier) + require.NotNil(t, persistedVpc.EffectiveRoutingProfile) + assert.Equal(t, uint32(6), persistedVpc.EffectiveRoutingProfile.AccessTier) + } + assert.True(t, tsc.AssertCalled(t, "ExecuteWorkflow", mock.Anything, mock.AnythingOfType("internal.StartWorkflowOptions"), "CreateVPCV2", mock.MatchedBy(func(req *corev1.VpcCreationRequest) bool { if req == nil { return false } + if !proto.Equal(req.RoutingProfileOverrides, tt.args.reqData.RoutingProfileOverrides.ToDB().ToProto()) { + return false + } if tt.args.reqData.RoutingProfile == nil { return req.RoutingProfileType == nil } @@ -1183,10 +1324,15 @@ func TestUpdateVPCHandler_Handle(t *testing.T) { tnu := testVPCBuildUser(t, dbSession, "test-starfleet-id-2", tnOrg, tnOrgRoles) tn := testVPCBuildTenant(t, dbSession, "test-tenant", tnOrg, tnu) + _ = common.TestBuildTenantAccountWithTargetedInstanceCreation(t, dbSession, ip, &tn.ID, tnOrg, cdbm.TenantAccountStatusReady, tnu) tnu2 := testVPCBuildUser(t, dbSession, "test-starfleet-id-3", tnOrg, tnOrgRoles) tn2 := testVPCBuildTenant(t, dbSession, "test-tenant-2", tnOrg, tnu2) + tnOrg3 := "test-tenant-org-3" + tnu3 := testVPCBuildUser(t, dbSession, "test-starfleet-id-4", tnOrg3, tnOrgRoles) + tn3 := testVPCBuildTenant(t, dbSession, "test-tenant-3", tnOrg3, tnu3) + st := testVPCBuildSite(t, dbSession, ip, "test-site-1", false, true, cdbm.SiteStatusRegistered, ipu) assert.NotNil(t, st) @@ -1216,6 +1362,12 @@ func TestUpdateVPCHandler_Handle(t *testing.T) { vpc2 := testVPCBuildVPC(t, dbSession, "test-vpc-2", ip, tn, st, cutil.GetPtr(cdbm.VpcEthernetVirtualizer), nil, map[string]string{"zone": "wes2"}, cdbm.VpcStatusReady, tnu) assert.NotNil(t, vpc2) + vpcFNN := testVPCBuildVPC(t, dbSession, "test-vpc-fnn", ip, tn, st, cutil.GetPtr(cdbm.VpcFNN), nil, nil, cdbm.VpcStatusReady, tnu) + assert.NotNil(t, vpcFNN) + vpcFNN.EffectiveRoutingProfile = &cdbm.VpcEffectiveRoutingProfile{Internal: true, AccessTier: 2} + testUpdateVPC(t, dbSession, vpcFNN) + vpcFNNUnprivileged := testVPCBuildVPC(t, dbSession, "test-vpc-fnn-unprivileged", ip, tn3, st, cutil.GetPtr(cdbm.VpcFNN), nil, nil, cdbm.VpcStatusReady, tnu3) + assert.NotNil(t, vpcFNNUnprivileged) vpc3 := testVPCBuildVPC(t, dbSession, "test-vpc-3", ip, tn, st2, cutil.GetPtr(cdbm.VpcEthernetVirtualizer), nil, map[string]string{"zone": "west3"}, cdbm.VpcStatusReady, tnu) assert.NotNil(t, vpc2) @@ -1235,6 +1387,10 @@ func TestUpdateVPCHandler_Handle(t *testing.T) { ts1t2 := testBuildTenantSiteAssociation(t, dbSession, tnOrg, tn2.ID, st.ID, tnu2.ID) assert.NotNil(t, ts1t2) + // Associate the unprivileged tenant with Site 1 without granting targeted instance creation. + ts1t3 := testBuildTenantSiteAssociation(t, dbSession, tnOrg3, tn3.ID, st.ID, tnu3.ID) + assert.NotNil(t, ts1t3) + // Associate tenant 2 with site 2 ts2t2 := testBuildTenantSiteAssociation(t, dbSession, tnOrg, tn2.ID, st2.ID, tnu2.ID) assert.NotNil(t, ts2t2) @@ -1265,6 +1421,11 @@ func TestUpdateVPCHandler_Handle(t *testing.T) { // OTEL Spanner configuration tracer, _, ctx := common.TestCommonTraceProviderSetup(t, ctx) + updateRoutingProfileOverrides := &model.APIVpcRoutingProfileOverrides{ + RouteTargetsOnExports: &model.APIVpcRouteTargets{{ASN: 64513, VNI: 61}}, + TenantLeakCommunitiesAccepted: cutil.GetPtr(true), + AcceptedLeaksFromUnderlay: &[]string{}, + } // Mock per-Site client for st3 tsc := &tmocks.Client{} @@ -1311,7 +1472,70 @@ func TestUpdateVPCHandler_Handle(t *testing.T) { expectNVLinkPartitionNil bool expectedNetworkSecurityGroupValue *string expectNetworkSecurityGroupNil bool + expectedRoutingProfileOverrides *model.APIVpcRoutingProfileOverrides + expectNoRoutingProfileMutation bool }{ + // A present object replaces the FNN VPC's full inline definition. + { + name: "test VPC update replaces routing profile overrides", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIVpcUpdateRequest{ + RoutingProfileOverrides: updateRoutingProfileOverrides, + }, + reqVPCID: vpcFNN.ID.String(), + reqVPC: vpcFNN, + reqOrg: tnOrg, + reqUser: tnu, + respCode: http.StatusOK, + }, + expectedRoutingProfileOverrides: updateRoutingProfileOverrides, + }, + // Tenants without targeted instance creation cannot replace inline definitions. + { + name: "test VPC update rejects routing profile overrides without targeted instance creation", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIVpcUpdateRequest{ + RoutingProfileOverrides: updateRoutingProfileOverrides, + }, + reqVPCID: vpcFNNUnprivileged.ID.String(), + reqVPC: vpcFNNUnprivileged, + reqOrg: tnOrg3, + reqUser: tnu3, + respCode: http.StatusForbidden, + respMessage: "Tenant does not have sufficient privileges to set `routingProfileOverrides`", + }, + expectNoRoutingProfileMutation: true, + }, + // Inline definitions are rejected when the persisted VPC is not FNN. + { + name: "test VPC update rejects routing profile overrides for ethernet virtualization", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIVpcUpdateRequest{ + RoutingProfileOverrides: updateRoutingProfileOverrides, + }, + reqVPCID: vpc2.ID.String(), + reqVPC: vpc2, + reqOrg: tnOrg, + reqUser: tnu, + respCode: http.StatusBadRequest, + respMessage: "Routing profile overrides are not supported for network virtualization type: ETHERNET_VIRTUALIZER", + }, + }, { name: "test VPC update success", fields: fields{ @@ -1689,6 +1913,17 @@ func TestUpdateVPCHandler_Handle(t *testing.T) { } require.Equal(t, tt.args.respCode, rec.Code) + if tt.expectNoRoutingProfileMutation { + // Rejected writes must preserve storage and avoid dispatching the update workflow. + persistedVpc, gerr := cdbm.NewVpcDAO(tt.fields.dbSession).GetByID(ctx, nil, uuid.MustParse(tt.args.reqVPCID), nil) + require.NoError(t, gerr) + assert.Nil(t, persistedVpc.RoutingProfileOverrides) + workflowOptionsMatcher := mock.MatchedBy(func(options temporalClient.StartWorkflowOptions) bool { + return options.ID == "vpc-update-"+tt.args.reqVPCID + }) + tsc.AssertNotCalled(t, "ExecuteWorkflow", mock.Anything, workflowOptionsMatcher, "UpdateVPC", mock.Anything) + tst.AssertNotCalled(t, "ExecuteWorkflow", mock.Anything, workflowOptionsMatcher, "UpdateVPC", mock.Anything) + } if tt.args.respCode != http.StatusOK { return } @@ -1721,6 +1956,15 @@ func TestUpdateVPCHandler_Handle(t *testing.T) { assert.Equal(t, len(rst.Labels), len(tt.args.reqData.Labels)) } + if tt.expectedRoutingProfileOverrides != nil { + // Verify the response and a fresh DB lookup both hold the replacement object. + assert.Equal(t, tt.expectedRoutingProfileOverrides, rst.RoutingProfileOverrides) + persistedVpc, gerr := cdbm.NewVpcDAO(tt.fields.dbSession).GetByID(ctx, nil, uuid.MustParse(tt.args.reqVPCID), nil) + require.NoError(t, gerr) + assert.Equal(t, tt.expectedRoutingProfileOverrides.ToDB(), persistedVpc.RoutingProfileOverrides) + assert.Nil(t, persistedVpc.EffectiveRoutingProfile) + } + if tt.args.reqData.NetworkSecurityGroupID != nil { persistedVpc, err := cdbm.NewVpcDAO(tt.fields.dbSession).GetByID(ctx, nil, uuid.MustParse(tt.args.reqVPCID), nil) require.NoError(t, err) @@ -1736,7 +1980,7 @@ func TestUpdateVPCHandler_Handle(t *testing.T) { } var lastUpdateVPCReq *corev1.VpcUpdateRequest - if tt.expectedNVLinkPartitionValue != nil || tt.expectNVLinkPartitionNil || tt.expectedNetworkSecurityGroupValue != nil || tt.expectNetworkSecurityGroupNil { + if tt.expectedNVLinkPartitionValue != nil || tt.expectNVLinkPartitionNil || tt.expectedNetworkSecurityGroupValue != nil || tt.expectNetworkSecurityGroupNil || tt.expectedRoutingProfileOverrides != nil { for i := len(tsc.Mock.Calls) - 1; i >= 0; i-- { call := tsc.Mock.Calls[i] if call.Method == "ExecuteWorkflow" && len(call.Arguments) >= 4 { @@ -1761,6 +2005,9 @@ func TestUpdateVPCHandler_Handle(t *testing.T) { require.NotNil(t, lastUpdateVPCReq.NetworkSecurityGroupId, "NetworkSecurityGroupId should be set in workflow request") assert.Equal(t, *tt.expectedNetworkSecurityGroupValue, *lastUpdateVPCReq.NetworkSecurityGroupId) } + if tt.expectedRoutingProfileOverrides != nil { + assert.True(t, proto.Equal(tt.expectedRoutingProfileOverrides.ToDB().ToProto(), lastUpdateVPCReq.RoutingProfileOverrides)) + } if tt.verifyChildSpanner { span := oteltrace.SpanFromContext(ec.Request().Context()) @@ -2197,9 +2444,10 @@ func TestGetVPCHandler_Handle(t *testing.T) { tnu1 := testVPCBuildUser(t, dbSession, "test-starfleet-id-2", tnOrg1, tnOrgRoles) tn1 := testVPCBuildTenant(t, dbSession, "test-tenant", tnOrg1, tnu1) - tnu2 := testVPCBuildUser(t, dbSession, "test-starfleet-id-3", tnOrg1, tnOrgRoles) - tn2 := testVPCBuildTenant(t, dbSession, "test-tenant-1", tnOrg1, tnu2) + tnu2 := testVPCBuildUser(t, dbSession, "test-starfleet-id-3", tnOrg2, tnOrgRoles) + tn2 := testVPCBuildTenant(t, dbSession, "test-tenant-1", tnOrg2, tnu2) assert.NotNil(t, tn2) + _ = common.TestBuildTenantAccountWithTargetedInstanceCreation(t, dbSession, ip, &tn1.ID, tnOrg1, cdbm.TenantAccountStatusReady, tnu1) st := testVPCBuildSite(t, dbSession, ip, "test-site-1", false, true, cdbm.SiteStatusRegistered, ipu) assert.NotNil(t, st) @@ -2207,8 +2455,16 @@ func TestGetVPCHandler_Handle(t *testing.T) { al := testVPCSiteBuildAllocation(t, dbSession, st, tn1, "test-allocation", ipu) assert.NotNil(t, al) - vpc := testVPCBuildVPC(t, dbSession, "test-vpc", ip, tn1, st, cutil.GetPtr(cdbm.VpcEthernetVirtualizer), nil, map[string]string{"zone": "west1"}, cdbm.VpcStatusReady, tnu1) + vpc := testVPCBuildVPC(t, dbSession, "test-vpc", ip, tn1, st, cutil.GetPtr(cdbm.VpcFNN), nil, map[string]string{"zone": "west1"}, cdbm.VpcStatusReady, tnu1) assert.NotNil(t, vpc) + vpc.RoutingProfileOverrides = &cdbm.VpcRoutingProfileOverrides{LeakDefaultRouteFromUnderlay: cutil.GetPtr(false)} + vpc.EffectiveRoutingProfile = &cdbm.VpcEffectiveRoutingProfile{LeakDefaultRouteFromUnderlay: true, Internal: true, AccessTier: 5} + testUpdateVPC(t, dbSession, vpc) + + // The second tenant has no targeted-instance-creation account and must not see effective state. + unprivilegedVpc := testVPCBuildVPC(t, dbSession, "test-vpc-unprivileged", ip, tn2, st, cutil.GetPtr(cdbm.VpcFNN), nil, nil, cdbm.VpcStatusReady, tnu2) + unprivilegedVpc.EffectiveRoutingProfile = &cdbm.VpcEffectiveRoutingProfile{Internal: true, AccessTier: 5} + testUpdateVPC(t, dbSession, unprivilegedVpc) // Attach an NSG to this instance nsg1 := testBuildNetworkSecurityGroup(t, dbSession, "network-security-group-1-for-the-win", tn1, st, cdbm.NetworkSecurityGroupStatusReady) @@ -2234,6 +2490,7 @@ func TestGetVPCHandler_Handle(t *testing.T) { expectedTenantOrg *string expectedSiteName *string expectedNetworkSecurityGroupName *string + expectEffectiveRoutingProfile bool verifyChildSpanner bool }{ { @@ -2250,6 +2507,24 @@ func TestGetVPCHandler_Handle(t *testing.T) { reqUser: tnu1, respCode: http.StatusOK, }, + wantErr: false, + expectEffectiveRoutingProfile: true, + }, + // A tenant without effective site privilege receives no resolved profile field. + { + name: "test VPC get omits effective routing profile for unprivileged tenant", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqVPC: unprivilegedVpc, + reqVPCID: unprivilegedVpc.ID.String(), + reqOrg: tnOrg2, + reqUser: tnu2, + respCode: http.StatusOK, + }, wantErr: false, }, { @@ -2330,10 +2605,11 @@ func TestGetVPCHandler_Handle(t *testing.T) { reqUser: tnu1, respCode: http.StatusOK, }, - queryIncludeRelations1: cutil.GetPtr(cdbm.TenantRelationName), - expectedTenantOrg: &tn1.Org, - wantErr: false, - verifyChildSpanner: true, + queryIncludeRelations1: cutil.GetPtr(cdbm.TenantRelationName), + expectedTenantOrg: &tn1.Org, + wantErr: false, + expectEffectiveRoutingProfile: true, + verifyChildSpanner: true, }, { name: "test VPC get API endpoint success include NSG relation", @@ -2352,6 +2628,7 @@ func TestGetVPCHandler_Handle(t *testing.T) { queryIncludeRelations1: cutil.GetPtr(cdbm.NetworkSecurityGroupRelationName), expectedNetworkSecurityGroupName: &nsg1.Name, wantErr: false, + expectEffectiveRoutingProfile: true, verifyChildSpanner: true, }, { @@ -2368,11 +2645,12 @@ func TestGetVPCHandler_Handle(t *testing.T) { reqUser: tnu1, respCode: http.StatusOK, }, - queryIncludeRelations1: cutil.GetPtr(cdbm.TenantRelationName), - queryIncludeRelations2: cutil.GetPtr(cdbm.SiteRelationName), - expectedTenantOrg: &tn1.Org, - expectedSiteName: &st.Name, - wantErr: false, + queryIncludeRelations1: cutil.GetPtr(cdbm.TenantRelationName), + queryIncludeRelations2: cutil.GetPtr(cdbm.SiteRelationName), + expectedTenantOrg: &tn1.Org, + expectedSiteName: &st.Name, + wantErr: false, + expectEffectiveRoutingProfile: true, }, } for _, tt := range tests { @@ -2429,6 +2707,11 @@ func TestGetVPCHandler_Handle(t *testing.T) { assert.Equal(t, rst.Name, tt.args.reqVPC.Name) assert.Equal(t, rst.Description, tt.args.reqVPC.Description) + assert.Equal(t, tt.expectEffectiveRoutingProfile, rst.EffectiveRoutingProfile != nil) + assert.Equal(t, tt.expectEffectiveRoutingProfile, strings.Contains(rec.Body.String(), "effectiveRoutingProfile")) + if tt.args.reqVPC.RoutingProfileOverrides != nil { + assert.NotNil(t, rst.RoutingProfileOverrides) + } if tt.expectedTenantOrg != nil { assert.Equal(t, rst.Tenant.Org, *tt.expectedTenantOrg) @@ -2481,6 +2764,7 @@ func TestGetAllVPCHandler_Handle(t *testing.T) { tnu := testVPCBuildUser(t, dbSession, "test-starfleet-id-2", tnOrg, tnOrgRoles) tn := testVPCBuildTenant(t, dbSession, "test-tenant", tnOrg, tnu) + _ = common.TestBuildTenantAccountWithTargetedInstanceCreation(t, dbSession, ip, &tn.ID, tnOrg, cdbm.TenantAccountStatusReady, tnu) tnu2 := testVPCBuildUser(t, dbSession, "test-starfleet-id-3", tn2Org, tnOrgRoles) tn2 := testVPCBuildTenant(t, dbSession, "test-tenant-2", tn2Org, tnu2) @@ -2496,6 +2780,14 @@ func TestGetAllVPCHandler_Handle(t *testing.T) { al2 := testVPCSiteBuildAllocation(t, dbSession, st2, tn, "test-allocation-2", ipu) assert.NotNil(t, al2) + // A per-Site false override removes only Site 2 from the account-level privilege. + tenantSite2 := testBuildTenantSiteAssociation(t, dbSession, tnOrg, tn.ID, st2.ID, tnu.ID) + _, err := cdbm.NewTenantSiteDAO(dbSession).Update(ctx, nil, cdbm.TenantSiteUpdateInput{ + TenantSiteID: tenantSite2.ID, + Config: &cdbm.TenantSiteConfig{TargetedInstanceCreation: cutil.GetPtr(false)}, + }) + require.NoError(t, err) + // Site with no allocations for first tenant // We'll add VPCs to simulate a site where tenant had allocations // but they were deleted without deleting VPCs. @@ -2556,6 +2848,7 @@ func TestGetAllVPCHandler_Handle(t *testing.T) { // Add the NSG of the site to the VPC vpc.NetworkSecurityGroupID = cutil.GetPtr(curNsg.ID) + vpc.EffectiveRoutingProfile = &cdbm.VpcEffectiveRoutingProfile{Internal: true, AccessTier: uint32(i + 1)} testUpdateVPC(t, dbSession, vpc) vpcs = append(vpcs, *vpc) @@ -3105,6 +3398,16 @@ func TestGetAllVPCHandler_Handle(t *testing.T) { assert.Equal(t, tt.wantCount, len(resp)) + // Bulk responses apply effective privilege independently for each VPC Site. + for _, responseVpc := range resp { + require.NotNil(t, responseVpc.SiteID) + if *responseVpc.SiteID == st2.ID.String() { + assert.Nil(t, responseVpc.EffectiveRoutingProfile) + } else { + assert.NotNil(t, responseVpc.EffectiveRoutingProfile) + } + } + ph := rec.Header().Get(pagination.ResponseHeaderName) require.NotEmpty(t, ph) diff --git a/rest-api/api/pkg/api/model/vpc.go b/rest-api/api/pkg/api/model/vpc.go index b196aa259c..e11dcdf04f 100644 --- a/rest-api/api/pkg/api/model/vpc.go +++ b/rest-api/api/pkg/api/model/vpc.go @@ -7,7 +7,9 @@ import ( "errors" "fmt" "math" + "net/netip" "regexp" + "slices" "time" "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/model/util" @@ -61,6 +63,209 @@ func normalizeAPIVpcRoutingProfileFromSite(routingProfile string) string { return routingProfile } +// APIVpcRouteTarget identifies a BGP route target by ASN and VNI. +type APIVpcRouteTarget struct { + ASN int `json:"asn"` + VNI int `json:"vni"` +} + +// ToDBModel converts an API route target to its persisted representation. +func (target APIVpcRouteTarget) ToDBModel() cdbm.VpcRouteTarget { + return cdbm.VpcRouteTarget{ASN: uint32(target.ASN), VNI: uint32(target.VNI)} +} + +// FromDBModel populates an API route target from its persisted representation. +func (target *APIVpcRouteTarget) FromDBModel(dbTarget cdbm.VpcRouteTarget) { + *target = APIVpcRouteTarget{ASN: int(dbTarget.ASN), VNI: int(dbTarget.VNI)} +} + +// Validate ensures the route target fits the unsigned Core wire representation. +func (target APIVpcRouteTarget) Validate() error { + return validation.ValidateStruct(&target, + validation.Field(&target.ASN, + validation.Min(0).Error("must be non-negative"), + validation.Max(math.MaxUint32).Error(fmt.Sprintf("must fit in uint32 (0..%d)", uint32(math.MaxUint32)))), + validation.Field(&target.VNI, + validation.Min(0).Error("must be non-negative"), + validation.Max(math.MaxUint32).Error(fmt.Sprintf("must fit in uint32 (0..%d)", uint32(math.MaxUint32)))), + ) +} + +// APIVpcRouteTargets is a collection of API route targets with DB conversion behavior. +type APIVpcRouteTargets []APIVpcRouteTarget + +// ToDBModel converts route targets to their persisted representation. +// Nil input is normalized to an allocated empty slice. +func (targets APIVpcRouteTargets) ToDBModel() []cdbm.VpcRouteTarget { + dbTargets := make([]cdbm.VpcRouteTarget, 0, len(targets)) + for _, target := range targets { + dbTargets = append(dbTargets, target.ToDBModel()) + } + return dbTargets +} + +// FromDBModel populates route targets from their persisted representation. +// Nil input is normalized to an allocated empty slice. +func (targets *APIVpcRouteTargets) FromDBModel(dbTargets []cdbm.VpcRouteTarget) { + *targets = make(APIVpcRouteTargets, 0, len(dbTargets)) + for _, dbTarget := range dbTargets { + target := APIVpcRouteTarget{} + target.FromDBModel(dbTarget) + *targets = append(*targets, target) + } +} + +// APIVpcRoutingProfileOverrides contains presence-aware routing properties set on a VPC. +// Nil fields inherit from the named routing profile, while present empty lists +// explicitly replace the corresponding base-profile lists. +type APIVpcRoutingProfileOverrides struct { + RouteTargetImports *APIVpcRouteTargets `json:"routeTargetImports"` + RouteTargetsOnExports *APIVpcRouteTargets `json:"routeTargetsOnExports"` + LeakDefaultRouteFromUnderlay *bool `json:"leakDefaultRouteFromUnderlay"` + LeakTenantHostRoutesToUnderlay *bool `json:"leakTenantHostRoutesToUnderlay"` + TenantLeakCommunitiesAccepted *bool `json:"tenantLeakCommunitiesAccepted"` + AcceptedLeaksFromUnderlay *[]string `json:"acceptedLeaksFromUnderlay"` + AllowedAnycastPrefixes *[]string `json:"allowedAnycastPrefixes"` +} + +// validateVpcRoutingProfilePrefix ensures a routing-policy prefix is valid IPv4 or IPv6 CIDR. +func validateVpcRoutingProfilePrefix(value any) error { + prefix, ok := value.(string) + if !ok { + return nil + } + if _, err := netip.ParsePrefix(prefix); err != nil { + return fmt.Errorf("invalid prefix `%s`", prefix) + } + return nil +} + +// validateVpcRoutingProfilePrefixes validates every prefix while preserving empty-list support. +func validateVpcRoutingProfilePrefixes(value any) error { + prefixes, ok := value.(*[]string) + if !ok || prefixes == nil { + return nil + } + return validation.Validate(*prefixes, + validation.Each(validation.By(validateVpcRoutingProfilePrefix)), + ) +} + +// Validate ensures every supplied override can be represented by Core. +func (profile *APIVpcRoutingProfileOverrides) Validate() error { + if profile == nil { + return nil + } + return validation.ValidateStruct(profile, + validation.Field(&profile.RouteTargetImports), + validation.Field(&profile.RouteTargetsOnExports), + validation.Field(&profile.AcceptedLeaksFromUnderlay, + validation.By(validateVpcRoutingProfilePrefixes)), + validation.Field(&profile.AllowedAnycastPrefixes, + validation.By(validateVpcRoutingProfilePrefixes)), + ) +} + +// ToDB converts API routing-profile overrides to their persisted representation. +func (profile *APIVpcRoutingProfileOverrides) ToDB() *cdbm.VpcRoutingProfileOverrides { + if profile == nil { + return nil + } + + dbProfile := &cdbm.VpcRoutingProfileOverrides{ + LeakDefaultRouteFromUnderlay: profile.LeakDefaultRouteFromUnderlay, + LeakTenantHostRoutesToUnderlay: profile.LeakTenantHostRoutesToUnderlay, + TenantLeakCommunitiesAccepted: profile.TenantLeakCommunitiesAccepted, + } + if profile.RouteTargetImports != nil { + targets := profile.RouteTargetImports.ToDBModel() + dbProfile.RouteTargetImports = &targets + } + if profile.RouteTargetsOnExports != nil { + targets := profile.RouteTargetsOnExports.ToDBModel() + dbProfile.RouteTargetsOnExports = &targets + } + if profile.AcceptedLeaksFromUnderlay != nil { + prefixes := slices.Clone(*profile.AcceptedLeaksFromUnderlay) + dbProfile.AcceptedLeaksFromUnderlay = &prefixes + } + if profile.AllowedAnycastPrefixes != nil { + prefixes := slices.Clone(*profile.AllowedAnycastPrefixes) + dbProfile.AllowedAnycastPrefixes = &prefixes + } + + return dbProfile +} + +// FromDB populates API routing-profile overrides from their persisted representation. +func (profile *APIVpcRoutingProfileOverrides) FromDB(dbProfile *cdbm.VpcRoutingProfileOverrides) { + *profile = APIVpcRoutingProfileOverrides{} + if dbProfile == nil { + return + } + + profile.LeakDefaultRouteFromUnderlay = dbProfile.LeakDefaultRouteFromUnderlay + profile.LeakTenantHostRoutesToUnderlay = dbProfile.LeakTenantHostRoutesToUnderlay + profile.TenantLeakCommunitiesAccepted = dbProfile.TenantLeakCommunitiesAccepted + if dbProfile.RouteTargetImports != nil { + targets := APIVpcRouteTargets{} + targets.FromDBModel(*dbProfile.RouteTargetImports) + profile.RouteTargetImports = &targets + } + if dbProfile.RouteTargetsOnExports != nil { + targets := APIVpcRouteTargets{} + targets.FromDBModel(*dbProfile.RouteTargetsOnExports) + profile.RouteTargetsOnExports = &targets + } + if dbProfile.AcceptedLeaksFromUnderlay != nil { + prefixes := slices.Clone(*dbProfile.AcceptedLeaksFromUnderlay) + profile.AcceptedLeaksFromUnderlay = &prefixes + } + if dbProfile.AllowedAnycastPrefixes != nil { + prefixes := slices.Clone(*dbProfile.AllowedAnycastPrefixes) + profile.AllowedAnycastPrefixes = &prefixes + } +} + +// APIVpcEffectiveRoutingProfile is the fully resolved routing policy reported by Core. +// It does not preserve override presence semantics, and its list fields are +// exposed as non-nil arrays. +type APIVpcEffectiveRoutingProfile struct { + RouteTargetImports APIVpcRouteTargets `json:"routeTargetImports"` + RouteTargetsOnExports APIVpcRouteTargets `json:"routeTargetsOnExports"` + LeakDefaultRouteFromUnderlay bool `json:"leakDefaultRouteFromUnderlay"` + LeakTenantHostRoutesToUnderlay bool `json:"leakTenantHostRoutesToUnderlay"` + TenantLeakCommunitiesAccepted bool `json:"tenantLeakCommunitiesAccepted"` + AcceptedLeaksFromUnderlay []string `json:"acceptedLeaksFromUnderlay"` + AllowedAnycastPrefixes []string `json:"allowedAnycastPrefixes"` + Internal bool `json:"internal"` + AccessTier int `json:"accessTier"` +} + +// FromDB populates an API effective routing profile from the last Core-reported value. +func (profile *APIVpcEffectiveRoutingProfile) FromDB(dbProfile *cdbm.VpcEffectiveRoutingProfile) { + *profile = APIVpcEffectiveRoutingProfile{} + if dbProfile == nil { + return + } + + profile.RouteTargetImports.FromDBModel(dbProfile.RouteTargetImports) + profile.RouteTargetsOnExports.FromDBModel(dbProfile.RouteTargetsOnExports) + profile.LeakDefaultRouteFromUnderlay = dbProfile.LeakDefaultRouteFromUnderlay + profile.LeakTenantHostRoutesToUnderlay = dbProfile.LeakTenantHostRoutesToUnderlay + profile.TenantLeakCommunitiesAccepted = dbProfile.TenantLeakCommunitiesAccepted + profile.AcceptedLeaksFromUnderlay = slices.Clone(dbProfile.AcceptedLeaksFromUnderlay) + if profile.AcceptedLeaksFromUnderlay == nil { + profile.AcceptedLeaksFromUnderlay = []string{} + } + profile.AllowedAnycastPrefixes = slices.Clone(dbProfile.AllowedAnycastPrefixes) + if profile.AllowedAnycastPrefixes == nil { + profile.AllowedAnycastPrefixes = []string{} + } + profile.Internal = dbProfile.Internal + profile.AccessTier = int(dbProfile.AccessTier) +} + // APIVpcCreateRequest captures the request data for creating a new VPC type APIVpcCreateRequest struct { // ID is the user-specified UUID of the VPC. @@ -90,6 +295,8 @@ type APIVpcCreateRequest struct { // This requires the Tenant to have elevated privileges. Current accepted values // are `privileged-internal`, `internal`, and `external`. RoutingProfile *string `json:"routingProfile"` + // RoutingProfileOverrides replaces selected properties from the VPC's named routing profile. + RoutingProfileOverrides *APIVpcRoutingProfileOverrides `json:"routingProfileOverrides"` } // Validate ensure the values passed in create request are acceptable @@ -110,6 +317,7 @@ func (ascr APIVpcCreateRequest) Validate() error { validation.Match(vpcRoutingProfileAllowedCharsRegexp).Error("`routingProfile` may only contain letters, numbers, or dashes"), ), ), + validation.Field(&ascr.RoutingProfileOverrides), validation.Field(&ascr.SiteID, validation.Required.Error(validationErrorValueRequired), validationis.UUID.Error(validationErrorInvalidUUID)), @@ -144,6 +352,12 @@ func (ascr APIVpcCreateRequest) Validate() error { } } + if ascr.RoutingProfileOverrides != nil && ascr.NetworkVirtualizationType != nil && !cdbm.VpcTypeSupportsRoutingProfile(ascr.NetworkVirtualizationType) { + return validation.Errors{ + "routingProfileOverrides": fmt.Errorf("`routingProfileOverrides` is not supported when `networkVirtualizationType` is `%s`", *ascr.NetworkVirtualizationType), + } + } + if ascr.Vni != nil && (*ascr.Vni < 0 || *ascr.Vni > math.MaxUint16) { return validation.Errors{ "vni": fmt.Errorf("VNI must be an integer between 0 and %d", math.MaxUint16), @@ -187,6 +401,7 @@ func (ascr APIVpcCreateRequest) ToProto(vpc *cdbm.Vpc) *corev1.VpcCreationReques TenantOrganizationId: config.TenantOrganizationId, NetworkVirtualizationType: config.NetworkVirtualizationType, RoutingProfileType: routingProfile, + RoutingProfileOverrides: ascr.RoutingProfileOverrides.ToDB().ToProto(), NetworkSecurityGroupId: config.NetworkSecurityGroupId, Vni: vni, Metadata: vpcProto.Metadata, @@ -207,6 +422,8 @@ type APIVpcUpdateRequest struct { NetworkSecurityGroupID *string `json:"networkSecurityGroupId"` // NVLinkLogicalPartitionID is the ID of the NVLinkLogicalPartition NVLinkLogicalPartitionID *string `json:"nvLinkLogicalPartitionId"` + // RoutingProfileOverrides replaces the VPC's current inline routing-profile definition when present. + RoutingProfileOverrides *APIVpcRoutingProfileOverrides `json:"routingProfileOverrides"` } // Validate ensure the values passed in update request are acceptable @@ -219,6 +436,7 @@ func (asur APIVpcUpdateRequest) Validate() error { validation.Field(&asur.Description, validation.When(asur.Description != nil, validation.Length(0, 1024).Error(validationErrorDescriptionStringLength)), ), + validation.Field(&asur.RoutingProfileOverrides), ) if err != nil { @@ -251,6 +469,7 @@ func (asur APIVpcUpdateRequest) ToProto(vpc *cdbm.Vpc) *corev1.VpcUpdateRequest Id: vpcProto.Id, NetworkSecurityGroupId: config.NetworkSecurityGroupId, DefaultNvlinkLogicalPartitionId: config.DefaultNvlinkLogicalPartitionId, + RoutingProfileOverrides: asur.RoutingProfileOverrides.ToDB().ToProto(), Metadata: vpcProto.Metadata, } } @@ -329,6 +548,10 @@ type APIVpc struct { NetworkSecurityGroupPropagationDetails *APINetworkSecurityGroupPropagationDetails `json:"networkSecurityGroupPropagationDetails"` // RoutingProfile is the applied routing profile for the VPC, when known. RoutingProfile *string `json:"routingProfile"` + // RoutingProfileOverrides contains properties set directly on the VPC. + RoutingProfileOverrides *APIVpcRoutingProfileOverrides `json:"routingProfileOverrides"` + // EffectiveRoutingProfile is visible only to tenants with targeted instance creation permission for the Site. + EffectiveRoutingProfile *APIVpcEffectiveRoutingProfile `json:"effectiveRoutingProfile,omitempty"` // RequestedVni is the explicitly requested VPC VNI at creation time _if_ one was requested. RequestedVni *int `json:"requestedVni"` // Vni is the active/actual VNI of the VPC, regardless of whether it was @@ -344,8 +567,10 @@ type APIVpc struct { Updated time.Time `json:"updated"` } -// NewAPIVpc creates and returns a new APIVpc object -func NewAPIVpc(dbVpc cdbm.Vpc, dbsds []cdbm.StatusDetail) APIVpc { +// NewAPIVpc converts a persisted VPC to its REST representation. +// includeEffectiveRoutingProfile controls whether cached controller-resolved +// routing state is exposed. +func NewAPIVpc(dbVpc cdbm.Vpc, dbsds []cdbm.StatusDetail, includeEffectiveRoutingProfile bool) APIVpc { apivpc := APIVpc{ ID: dbVpc.ID.String(), Name: dbVpc.Name, @@ -373,6 +598,16 @@ func NewAPIVpc(dbVpc cdbm.Vpc, dbsds []cdbm.StatusDetail) APIVpc { apivpc.RoutingProfile = &routingProfile } + if dbVpc.RoutingProfileOverrides != nil { + apivpc.RoutingProfileOverrides = &APIVpcRoutingProfileOverrides{} + apivpc.RoutingProfileOverrides.FromDB(dbVpc.RoutingProfileOverrides) + } + + if includeEffectiveRoutingProfile && dbVpc.EffectiveRoutingProfile != nil { + apivpc.EffectiveRoutingProfile = &APIVpcEffectiveRoutingProfile{} + apivpc.EffectiveRoutingProfile.FromDB(dbVpc.EffectiveRoutingProfile) + } + if dbVpc.ControllerVpcID != nil { apivpc.ControllerVpcID = util.GetUUIDPtrToStrPtr(dbVpc.ControllerVpcID) } diff --git a/rest-api/api/pkg/api/model/vpc_test.go b/rest-api/api/pkg/api/model/vpc_test.go index d90daf09c5..aca26b5f1b 100644 --- a/rest-api/api/pkg/api/model/vpc_test.go +++ b/rest-api/api/pkg/api/model/vpc_test.go @@ -5,6 +5,7 @@ package model import ( "encoding/json" + "math" "testing" "time" @@ -27,11 +28,13 @@ func TestAPIVpcCreateRequest_Validate(t *testing.T) { Labels map[string]string Vni *int RoutingProfile *string + RoutingProfileOverrides *APIVpcRoutingProfileOverrides } tests := []struct { - name string - fields fields - wantErr bool + name string + fields fields + wantErr bool + wantErrContains string }{ { name: "test valid VPC create request", @@ -91,6 +94,29 @@ func TestAPIVpcCreateRequest_Validate(t *testing.T) { }, wantErr: false, }, + // Inline overrides are accepted when the request explicitly selects FNN. + { + name: "test valid VPC create request - routing profile overrides for FNN", + fields: fields{ + Name: "test-name", + SiteID: uuid.NewString(), + NetworkVirtualizationType: cutil.GetPtr(cdbm.VpcFNN), + RoutingProfileOverrides: &APIVpcRoutingProfileOverrides{LeakDefaultRouteFromUnderlay: cutil.GetPtr(true)}, + }, + wantErr: false, + }, + // Explicit non-FNN requests identify the rejected virtualization type. + { + name: "test invalid VPC create request - routing profile overrides on non-FNN VPC", + fields: fields{ + Name: "test-name", + SiteID: uuid.NewString(), + NetworkVirtualizationType: cutil.GetPtr(cdbm.VpcEthernetVirtualizer), + RoutingProfileOverrides: &APIVpcRoutingProfileOverrides{LeakDefaultRouteFromUnderlay: cutil.GetPtr(true)}, + }, + wantErr: true, + wantErrContains: "`networkVirtualizationType` is `ETHERNET_VIRTUALIZER`", + }, { name: "test invalid VPC create request - routing profile on non-FNN VPC", fields: fields{ @@ -224,21 +250,28 @@ func TestAPIVpcCreateRequest_Validate(t *testing.T) { Labels: tt.fields.Labels, Vni: tt.fields.Vni, RoutingProfile: tt.fields.RoutingProfile, + RoutingProfileOverrides: tt.fields.RoutingProfileOverrides, } - if err := vcr.Validate(); (err != nil) != tt.wantErr { + err := vcr.Validate() + if (err != nil) != tt.wantErr { marshalledErr, _ := json.Marshal(err) t.Errorf("APIVpcCreateRequest.Validate() error = %v, wantErr %v", string(marshalledErr), tt.wantErr) } + if tt.wantErrContains != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErrContains) + } }) } } func TestAPIVpcUpdateRequest_Validate(t *testing.T) { type fields struct { - Name string - Description *string - Labels map[string]string + Name string + Description *string + Labels map[string]string + RoutingProfileOverrides *APIVpcRoutingProfileOverrides } tests := []struct { name string @@ -253,6 +286,15 @@ func TestAPIVpcUpdateRequest_Validate(t *testing.T) { }, wantErr: false, }, + // Update validates the inline definition while the handler checks the persisted VPC type. + { + name: "test valid VPC update request - routing profile overrides", + fields: fields{ + Name: "test-name", + RoutingProfileOverrides: &APIVpcRoutingProfileOverrides{LeakDefaultRouteFromUnderlay: cutil.GetPtr(true)}, + }, + wantErr: false, + }, { name: "test valid VPC update request - invalid names are specified names exceeded 256 char", fields: fields{ @@ -309,9 +351,10 @@ func TestAPIVpcUpdateRequest_Validate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { vur := APIVpcUpdateRequest{ - Name: &tt.fields.Name, - Description: tt.fields.Description, - Labels: tt.fields.Labels, + Name: &tt.fields.Name, + Description: tt.fields.Description, + Labels: tt.fields.Labels, + RoutingProfileOverrides: tt.fields.RoutingProfileOverrides, } if err := vur.Validate(); (err != nil) != tt.wantErr { @@ -322,6 +365,119 @@ func TestAPIVpcUpdateRequest_Validate(t *testing.T) { } } +// TestAPIVpcRoutingProfileOverrides_Validate verifies the API rejects values +// that cannot be represented by Core while preserving valid presence semantics. +func TestAPIVpcRoutingProfileOverrides_Validate(t *testing.T) { + tests := []struct { + name string + profile *APIVpcRoutingProfileOverrides + wantErr bool + }{ + // Empty lists, duplicate prefixes, host bits, and both IP families are valid Core inputs. + { + name: "accepts Core-compatible values", + profile: &APIVpcRoutingProfileOverrides{ + RouteTargetImports: &APIVpcRouteTargets{ + {ASN: 0, VNI: 0}, + {ASN: int(math.MaxUint32), VNI: int(math.MaxUint32)}, + }, + RouteTargetsOnExports: &APIVpcRouteTargets{}, + AcceptedLeaksFromUnderlay: &[]string{"10.0.0.1/24", "10.0.0.1/24", "2001:db8::1/64"}, + AllowedAnycastPrefixes: &[]string{}, + LeakDefaultRouteFromUnderlay: cutil.GetPtr(false), + }, + }, + // Negative values cannot be represented by the unsigned protobuf fields. + { + name: "rejects negative route targets", + profile: &APIVpcRoutingProfileOverrides{ + RouteTargetImports: &APIVpcRouteTargets{{ASN: -1, VNI: 1}}, + }, + wantErr: true, + }, + // Values above uint32 would otherwise be truncated during conversion. + { + name: "rejects overflowing route targets", + profile: &APIVpcRoutingProfileOverrides{ + RouteTargetsOnExports: &APIVpcRouteTargets{{ASN: 1, VNI: int(math.MaxUint32) + 1}}, + }, + wantErr: true, + }, + // Malformed prefixes must not reach Core's IpNetwork parser. + { + name: "rejects malformed prefixes", + profile: &APIVpcRoutingProfileOverrides{ + AllowedAnycastPrefixes: &[]string{"not-a-prefix"}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.profile.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +// TestAPIVpcRoutingProfileOverrides_ToDB verifies optional values retain their +// presence and explicit-empty semantics in the persisted representation. +func TestAPIVpcRoutingProfileOverrides_ToDB(t *testing.T) { + imports := APIVpcRouteTargets{{ASN: 64512, VNI: 17}} + emptyTargets := APIVpcRouteTargets{} + emptyPrefixes := []string{} + profile := &APIVpcRoutingProfileOverrides{ + RouteTargetImports: &imports, + RouteTargetsOnExports: &emptyTargets, + LeakDefaultRouteFromUnderlay: cutil.GetPtr(false), + TenantLeakCommunitiesAccepted: cutil.GetPtr(true), + AcceptedLeaksFromUnderlay: &emptyPrefixes, + AllowedAnycastPrefixes: &[]string{"192.0.2.1/24"}, + } + + dbProfile := profile.ToDB() + require.NotNil(t, dbProfile) + require.NotNil(t, dbProfile.RouteTargetImports) + assert.Equal(t, []cdbm.VpcRouteTarget{{ASN: 64512, VNI: 17}}, *dbProfile.RouteTargetImports) + require.NotNil(t, dbProfile.RouteTargetsOnExports) + assert.Empty(t, *dbProfile.RouteTargetsOnExports) + require.NotNil(t, dbProfile.LeakDefaultRouteFromUnderlay) + assert.False(t, *dbProfile.LeakDefaultRouteFromUnderlay) + require.NotNil(t, dbProfile.AcceptedLeaksFromUnderlay) + assert.Empty(t, *dbProfile.AcceptedLeaksFromUnderlay) +} + +// TestAPIVpcRoutingProfileOverrides_FromDB verifies persisted optional values +// retain their presence and explicit-empty semantics in the API representation. +func TestAPIVpcRoutingProfileOverrides_FromDB(t *testing.T) { + emptyTargets := []cdbm.VpcRouteTarget{} + emptyPrefixes := []string{} + dbProfile := &cdbm.VpcRoutingProfileOverrides{ + RouteTargetImports: &[]cdbm.VpcRouteTarget{{ASN: 64512, VNI: 17}}, + RouteTargetsOnExports: &emptyTargets, + LeakDefaultRouteFromUnderlay: cutil.GetPtr(false), + TenantLeakCommunitiesAccepted: cutil.GetPtr(true), + AcceptedLeaksFromUnderlay: &emptyPrefixes, + AllowedAnycastPrefixes: &[]string{"192.0.2.1/24"}, + } + + profile := &APIVpcRoutingProfileOverrides{} + profile.FromDB(dbProfile) + require.NotNil(t, profile.RouteTargetImports) + assert.Equal(t, APIVpcRouteTargets{{ASN: 64512, VNI: 17}}, *profile.RouteTargetImports) + require.NotNil(t, profile.RouteTargetsOnExports) + assert.Empty(t, *profile.RouteTargetsOnExports) + require.NotNil(t, profile.LeakDefaultRouteFromUnderlay) + assert.False(t, *profile.LeakDefaultRouteFromUnderlay) + require.NotNil(t, profile.AcceptedLeaksFromUnderlay) + assert.Empty(t, *profile.AcceptedLeaksFromUnderlay) +} + func TestAPIVpcVirtualizationUpdateRequest_Validate(t *testing.T) { vpcObj1 := &cdbm.Vpc{ ID: uuid.New(), @@ -509,7 +665,7 @@ func TestNewAPIVpc(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := NewAPIVpc(tt.args.dbVpc, tt.args.dbsds) + got := NewAPIVpc(tt.args.dbVpc, tt.args.dbsds, false) assert.Equal(t, tt.want.ID, got.ID) assert.Equal(t, tt.want.Name, got.Name) @@ -536,6 +692,40 @@ func TestNewAPIVpc(t *testing.T) { assert.Equal(t, tt.want.Updated, got.Updated) }) } + + profileVpc := cdbm.Vpc{ + ID: uuid.New(), + RoutingProfileOverrides: &cdbm.VpcRoutingProfileOverrides{ + LeakDefaultRouteFromUnderlay: cutil.GetPtr(false), + }, + EffectiveRoutingProfile: &cdbm.VpcEffectiveRoutingProfile{ + LeakDefaultRouteFromUnderlay: true, + Internal: true, + AccessTier: 7, + }, + } + + t.Run("omits effective profile without targeted instance creation permission", func(t *testing.T) { + // Desired configuration remains visible even when the resolved state is gated. + unprivileged := NewAPIVpc(profileVpc, nil, false) + require.NotNil(t, unprivileged.RoutingProfileOverrides) + assert.Nil(t, unprivileged.EffectiveRoutingProfile) + unprivilegedJSON, err := json.Marshal(unprivileged) + require.NoError(t, err) + assert.NotContains(t, string(unprivilegedJSON), "effectiveRoutingProfile") + }) + + t.Run("includes effective profile with targeted instance creation permission", func(t *testing.T) { + // Authorized responses expose resolved values and canonical empty lists. + privileged := NewAPIVpc(profileVpc, nil, true) + require.NotNil(t, privileged.EffectiveRoutingProfile) + assert.True(t, privileged.EffectiveRoutingProfile.Internal) + assert.Equal(t, 7, privileged.EffectiveRoutingProfile.AccessTier) + assert.NotNil(t, privileged.EffectiveRoutingProfile.AcceptedLeaksFromUnderlay) + assert.Empty(t, privileged.EffectiveRoutingProfile.AcceptedLeaksFromUnderlay) + assert.NotNil(t, privileged.EffectiveRoutingProfile.AllowedAnycastPrefixes) + assert.Empty(t, privileged.EffectiveRoutingProfile.AllowedAnycastPrefixes) + }) } func TestAPIVpcCreateRequest_ToProto(t *testing.T) { @@ -611,6 +801,20 @@ func TestAPIVpcCreateRequest_ToProto(t *testing.T) { got := APIVpcCreateRequest{}.ToProto(vpc) assert.Nil(t, got.RoutingProfileType) }) + + t.Run("forwards supplied routing profile overrides", func(t *testing.T) { + // Create forwards the complete inline definition supplied by the caller. + vpc := &cdbm.Vpc{ID: id, Org: "org-1", Name: "vpc-a", NetworkVirtualizationType: &fnn} + profile := &APIVpcRoutingProfileOverrides{ + LeakTenantHostRoutesToUnderlay: cutil.GetPtr(false), + AllowedAnycastPrefixes: &[]string{}, + } + got := (APIVpcCreateRequest{RoutingProfileOverrides: profile}).ToProto(vpc) + require.NotNil(t, got.RoutingProfileOverrides) + require.NotNil(t, got.RoutingProfileOverrides.LeakTenantHostRoutesToUnderlay) + assert.False(t, *got.RoutingProfileOverrides.LeakTenantHostRoutesToUnderlay) + require.NotNil(t, got.RoutingProfileOverrides.AllowedAnycastPrefixes) + }) } func TestAPIVpcUpdateRequest_ToProto(t *testing.T) { @@ -692,4 +896,18 @@ func TestAPIVpcUpdateRequest_ToProto(t *testing.T) { require.NotNil(t, got.Id) assert.Equal(t, ctrlID.String(), got.Id.Value) }) + + t.Run("preserves omitted routing profile overrides", func(t *testing.T) { + // Omitted input must not replace the current Core definition. + vpc := &cdbm.Vpc{ID: id, Name: "vpc-a"} + got := (APIVpcUpdateRequest{}).ToProto(vpc) + assert.Nil(t, got.RoutingProfileOverrides) + }) + + t.Run("forwards explicit empty routing profile overrides", func(t *testing.T) { + // An empty object restores inheritance for every property. + vpc := &cdbm.Vpc{ID: id, Name: "vpc-a"} + got := (APIVpcUpdateRequest{RoutingProfileOverrides: &APIVpcRoutingProfileOverrides{}}).ToProto(vpc) + require.NotNil(t, got.RoutingProfileOverrides) + }) } diff --git a/rest-api/db/pkg/db/model/vpc.go b/rest-api/db/pkg/db/model/vpc.go index 24491f27e1..31da4d0898 100644 --- a/rest-api/db/pkg/db/model/vpc.go +++ b/rest-api/db/pkg/db/model/vpc.go @@ -83,8 +83,8 @@ var ( // REST-specific" default. Callers should use the helper functions // below rather than this map directly. vpcTypeCapabilities = map[string]struct { - // supportsRoutingProfile is true for VPC types that accept a - // `routingProfile` field on create. FNN-only today. + // supportsRoutingProfile is true for VPC types that accept named + // or inline routing-profile configuration. FNN-only. supportsRoutingProfile bool // supportsAutoInterface is true for VPC types that allow @@ -99,8 +99,8 @@ var ( } ) -// VpcTypeSupportsRoutingProfile reports whether VPCs of the given -// network-virtualization type accept a `routingProfile` on create. +// VpcTypeSupportsRoutingProfile reports whether the given network-virtualization +// type supports named or inline VPC routing-profile configuration. // A nil pointer (no type specified) returns false; the caller is // expected to have resolved any defaulting beforehand. func VpcTypeSupportsRoutingProfile(virtType *string) bool { @@ -121,6 +121,183 @@ func VpcTypeSupportsAutoInterface(virtType *string) bool { return vpcTypeCapabilities[*virtType].supportsAutoInterface } +// VpcRouteTarget is the persisted representation of a routing-profile route target. +type VpcRouteTarget struct { + ASN uint32 `json:"asn"` + VNI uint32 `json:"vni"` +} + +// ToProto converts a persisted route target to its Core representation. +func (target VpcRouteTarget) ToProto() *corev1.RouteTarget { + return &corev1.RouteTarget{Asn: target.ASN, Vni: target.VNI} +} + +// FromProto populates a persisted route target from its Core representation. +func (target *VpcRouteTarget) FromProto(protoTarget *corev1.RouteTarget) { + *target = VpcRouteTarget{} + if protoTarget == nil { + return + } + target.ASN = protoTarget.Asn + target.VNI = protoTarget.Vni +} + +// VpcRoutingProfileOverrides contains presence-aware properties set directly on a VPC. +// Nil properties inherit from the VPC's named routing profile, while present empty +// lists explicitly replace the corresponding base-profile list with an empty list. +type VpcRoutingProfileOverrides struct { + RouteTargetImports *[]VpcRouteTarget `json:"routeTargetImports"` + RouteTargetsOnExports *[]VpcRouteTarget `json:"routeTargetsOnExports"` + LeakDefaultRouteFromUnderlay *bool `json:"leakDefaultRouteFromUnderlay"` + LeakTenantHostRoutesToUnderlay *bool `json:"leakTenantHostRoutesToUnderlay"` + TenantLeakCommunitiesAccepted *bool `json:"tenantLeakCommunitiesAccepted"` + AcceptedLeaksFromUnderlay *[]string `json:"acceptedLeaksFromUnderlay"` + AllowedAnycastPrefixes *[]string `json:"allowedAnycastPrefixes"` +} + +// VpcEffectiveRoutingProfile is the fully resolved routing profile reported by Core. +type VpcEffectiveRoutingProfile struct { + RouteTargetImports []VpcRouteTarget `json:"routeTargetImports"` + RouteTargetsOnExports []VpcRouteTarget `json:"routeTargetsOnExports"` + LeakDefaultRouteFromUnderlay bool `json:"leakDefaultRouteFromUnderlay"` + LeakTenantHostRoutesToUnderlay bool `json:"leakTenantHostRoutesToUnderlay"` + TenantLeakCommunitiesAccepted bool `json:"tenantLeakCommunitiesAccepted"` + AcceptedLeaksFromUnderlay []string `json:"acceptedLeaksFromUnderlay"` + AllowedAnycastPrefixes []string `json:"allowedAnycastPrefixes"` + Internal bool `json:"internal"` + AccessTier uint32 `json:"accessTier"` +} + +// vpcRouteTargetsToProto converts persisted route targets to their Core wire representation. +func vpcRouteTargetsToProto(targets []VpcRouteTarget) []*corev1.RouteTarget { + protoTargets := make([]*corev1.RouteTarget, 0, len(targets)) + for _, target := range targets { + protoTargets = append(protoTargets, target.ToProto()) + } + return protoTargets +} + +// vpcRouteTargetsFromProto converts Core route targets to their persisted representation. +func vpcRouteTargetsFromProto(targets []*corev1.RouteTarget) []VpcRouteTarget { + dbTargets := make([]VpcRouteTarget, 0, len(targets)) + for _, protoTarget := range targets { + target := VpcRouteTarget{} + target.FromProto(protoTarget) + dbTargets = append(dbTargets, target) + } + return dbTargets +} + +// vpcPrefixesToProto converts stored CIDR strings to Core prefix-filter entries. +func vpcPrefixesToProto(prefixes []string) []*corev1.PrefixFilterPolicyEntry { + entries := make([]*corev1.PrefixFilterPolicyEntry, 0, len(prefixes)) + for _, prefix := range prefixes { + entries = append(entries, &corev1.PrefixFilterPolicyEntry{Prefix: prefix}) + } + return entries +} + +// vpcPrefixesFromProto converts Core prefix-filter entries to stored CIDR strings. +func vpcPrefixesFromProto(entries []*corev1.PrefixFilterPolicyEntry) []string { + prefixes := make([]string, 0, len(entries)) + for _, entry := range entries { + prefixes = append(prefixes, entry.GetPrefix()) + } + return prefixes +} + +// ToProto converts VPC routing-profile overrides to their presence-aware Core representation. +func (profile *VpcRoutingProfileOverrides) ToProto() *corev1.VpcRoutingProfileOverrides { + if profile == nil { + return nil + } + + protoProfile := &corev1.VpcRoutingProfileOverrides{ + LeakDefaultRouteFromUnderlay: profile.LeakDefaultRouteFromUnderlay, + LeakTenantHostRoutesToUnderlay: profile.LeakTenantHostRoutesToUnderlay, + TenantLeakCommunitiesAccepted: profile.TenantLeakCommunitiesAccepted, + } + if profile.RouteTargetImports != nil { + protoProfile.RouteTargetImports = &corev1.RouteTargets{Values: vpcRouteTargetsToProto(*profile.RouteTargetImports)} + } + if profile.RouteTargetsOnExports != nil { + protoProfile.RouteTargetsOnExports = &corev1.RouteTargets{Values: vpcRouteTargetsToProto(*profile.RouteTargetsOnExports)} + } + if profile.AcceptedLeaksFromUnderlay != nil { + protoProfile.AcceptedLeaksFromUnderlay = &corev1.PrefixFilterPolicyEntries{Values: vpcPrefixesToProto(*profile.AcceptedLeaksFromUnderlay)} + } + if profile.AllowedAnycastPrefixes != nil { + protoProfile.AllowedAnycastPrefixes = &corev1.PrefixFilterPolicyEntries{Values: vpcPrefixesToProto(*profile.AllowedAnycastPrefixes)} + } + + return protoProfile +} + +// FromProto populates VPC routing-profile overrides from Core while preserving field presence. +func (profile *VpcRoutingProfileOverrides) FromProto(protoProfile *corev1.VpcRoutingProfileOverrides) { + *profile = VpcRoutingProfileOverrides{} + if protoProfile == nil { + return + } + + profile.LeakDefaultRouteFromUnderlay = protoProfile.LeakDefaultRouteFromUnderlay + profile.LeakTenantHostRoutesToUnderlay = protoProfile.LeakTenantHostRoutesToUnderlay + profile.TenantLeakCommunitiesAccepted = protoProfile.TenantLeakCommunitiesAccepted + if protoProfile.RouteTargetImports != nil { + targets := vpcRouteTargetsFromProto(protoProfile.RouteTargetImports.Values) + profile.RouteTargetImports = &targets + } + if protoProfile.RouteTargetsOnExports != nil { + targets := vpcRouteTargetsFromProto(protoProfile.RouteTargetsOnExports.Values) + profile.RouteTargetsOnExports = &targets + } + if protoProfile.AcceptedLeaksFromUnderlay != nil { + prefixes := vpcPrefixesFromProto(protoProfile.AcceptedLeaksFromUnderlay.Values) + profile.AcceptedLeaksFromUnderlay = &prefixes + } + if protoProfile.AllowedAnycastPrefixes != nil { + prefixes := vpcPrefixesFromProto(protoProfile.AllowedAnycastPrefixes.Values) + profile.AllowedAnycastPrefixes = &prefixes + } +} + +// ToProto converts a resolved VPC routing profile to the Core status representation. +func (profile *VpcEffectiveRoutingProfile) ToProto() *corev1.VpcEffectiveRoutingProfile { + if profile == nil { + return nil + } + + return &corev1.VpcEffectiveRoutingProfile{ + RouteTargetImports: vpcRouteTargetsToProto(profile.RouteTargetImports), + RouteTargetsOnExports: vpcRouteTargetsToProto(profile.RouteTargetsOnExports), + LeakDefaultRouteFromUnderlay: profile.LeakDefaultRouteFromUnderlay, + LeakTenantHostRoutesToUnderlay: profile.LeakTenantHostRoutesToUnderlay, + TenantLeakCommunitiesAccepted: profile.TenantLeakCommunitiesAccepted, + AcceptedLeaksFromUnderlay: vpcPrefixesToProto(profile.AcceptedLeaksFromUnderlay), + AllowedAnycastPrefixes: vpcPrefixesToProto(profile.AllowedAnycastPrefixes), + Internal: profile.Internal, + AccessTier: profile.AccessTier, + } +} + +// FromProto populates a resolved VPC routing profile from the Core status representation. +func (profile *VpcEffectiveRoutingProfile) FromProto(protoProfile *corev1.VpcEffectiveRoutingProfile) { + *profile = VpcEffectiveRoutingProfile{} + if protoProfile == nil { + return + } + + profile.RouteTargetImports = vpcRouteTargetsFromProto(protoProfile.RouteTargetImports) + profile.RouteTargetsOnExports = vpcRouteTargetsFromProto(protoProfile.RouteTargetsOnExports) + profile.LeakDefaultRouteFromUnderlay = protoProfile.LeakDefaultRouteFromUnderlay + profile.LeakTenantHostRoutesToUnderlay = protoProfile.LeakTenantHostRoutesToUnderlay + profile.TenantLeakCommunitiesAccepted = protoProfile.TenantLeakCommunitiesAccepted + profile.AcceptedLeaksFromUnderlay = vpcPrefixesFromProto(protoProfile.AcceptedLeaksFromUnderlay) + profile.AllowedAnycastPrefixes = vpcPrefixesFromProto(protoProfile.AllowedAnycastPrefixes) + profile.Internal = protoProfile.Internal + profile.AccessTier = protoProfile.AccessTier +} + // Vpc represents entries in the vpc table type Vpc struct { bun.BaseModel `bun:"table:vpc,alias:v"` @@ -139,6 +316,8 @@ type Vpc struct { NVLinkLogicalPartition *NVLinkLogicalPartition `bun:"rel:belongs-to,join:nvlink_logical_partition_id=id"` NetworkVirtualizationType *string `bun:"network_virtualization_type"` RoutingProfile *string `bun:"routing_profile"` + RoutingProfileOverrides *VpcRoutingProfileOverrides `bun:"routing_profile_overrides,type:jsonb"` + EffectiveRoutingProfile *VpcEffectiveRoutingProfile `bun:"effective_routing_profile,type:jsonb"` ControllerVpcID *uuid.UUID `bun:"controller_vpc_id,type:uuid"` ActiveVni *int `bun:"active_vni,type:integer"` NetworkSecurityGroupID *string `bun:"network_security_group_id"` @@ -169,8 +348,9 @@ func (vpc *Vpc) GetSiteID() *uuid.UUID { // protos (create / update) are produced by `ToProto` methods on the // corresponding API request types in api/pkg/api/model/vpc.go. // -// Desired configuration is emitted via the structured `config` field -// and the allocated VNI via `status`. The deprecated flat mirror fields +// Desired configuration, including routing-profile overrides, is emitted via +// the structured `config` field. Controller-resolved state (the allocated VNI +// and effective routing profile) is emitted via `status`. The deprecated flat mirror fields // are no longer populated: site agents at or after commit a2e3f88b read // exclusively from `config`/`status`. func (vpc *Vpc) ToProto() *corev1.Vpc { @@ -207,6 +387,7 @@ func (vpc *Vpc) ToProto() *corev1.Vpc { DefaultNvlinkLogicalPartitionId: nvllpProto, Vni: cutil.IntPtrToUint32Ptr(vpc.Vni), RoutingProfileType: vpc.RoutingProfile, + RoutingProfileOverrides: vpc.RoutingProfileOverrides.ToProto(), NetworkVirtualizationType: networkVirtualizationType, } @@ -218,8 +399,12 @@ func (vpc *Vpc) ToProto() *corev1.Vpc { } allocatedVni := cutil.IntPtrToUint32Ptr(vpc.ActiveVni) - if allocatedVni != nil { - proto.Status = &corev1.VpcStatus{Vni: allocatedVni} + effectiveRoutingProfile := vpc.EffectiveRoutingProfile.ToProto() + if allocatedVni != nil || effectiveRoutingProfile != nil { + proto.Status = &corev1.VpcStatus{ + Vni: allocatedVni, + EffectiveRoutingProfile: effectiveRoutingProfile, + } } return proto @@ -236,8 +421,9 @@ func (vpc *Vpc) ToProto() *corev1.Vpc { // - `Name` is sourced from `proto.Metadata.Name` when set, falling // back to the legacy top-level `proto.Name` when metadata omits it. // - Desired-configuration fields (Org, NSG, NVLink, virtualization -// type, routing profile, requested VNI) are read from the structured -// `config`. The allocated VNI comes from `status`, not `config`. +// type, named routing profile, routing-profile overrides, requested VNI) +// are read from structured `config`. Controller-resolved fields (allocated +// VNI and effective routing profile) are read from `status`. // - Optional pointer fields (NetworkSecurityGroupID, // NVLinkLogicalPartitionID) are cleared when the proto omits them // OR when the proto value is invalid (e.g. an unparseable UUID). @@ -265,11 +451,21 @@ func (vpc *Vpc) FromProto(proto *corev1.Vpc) { vpc.Org = cfg.TenantOrganizationId vpc.NetworkSecurityGroupID = cfg.NetworkSecurityGroupId vpc.RoutingProfile = cfg.RoutingProfileType + vpc.RoutingProfileOverrides = nil + if cfg.RoutingProfileOverrides != nil { + vpc.RoutingProfileOverrides = &VpcRoutingProfileOverrides{} + vpc.RoutingProfileOverrides.FromProto(cfg.RoutingProfileOverrides) + } vpc.Vni = cutil.Uint32PtrToIntPtr(cfg.Vni) vpc.ActiveVni = nil + vpc.EffectiveRoutingProfile = nil status := proto.GetStatus() if status != nil { vpc.ActiveVni = cutil.Uint32PtrToIntPtr(status.Vni) + if status.EffectiveRoutingProfile != nil { + vpc.EffectiveRoutingProfile = &VpcEffectiveRoutingProfile{} + vpc.EffectiveRoutingProfile.FromProto(status.EffectiveRoutingProfile) + } } vpc.NVLinkLogicalPartitionID = nil @@ -315,6 +511,7 @@ type VpcCreateInput struct { NVLinkLogicalPartitionID *uuid.UUID NetworkVirtualizationType *string RoutingProfile *string + RoutingProfileOverrides *VpcRoutingProfileOverrides ControllerVpcID *uuid.UUID NetworkSecurityGroupID *string NetworkSecurityGroupPropagationDetails *NetworkSecurityGroupPropagationDetails @@ -331,6 +528,8 @@ type VpcUpdateInput struct { Description *string NetworkVirtualizationType *string RoutingProfile *string + RoutingProfileOverrides *VpcRoutingProfileOverrides + EffectiveRoutingProfile *VpcEffectiveRoutingProfile ControllerVpcID *uuid.UUID ActiveVni *int NVLinkLogicalPartitionID *uuid.UUID @@ -348,6 +547,8 @@ type VpcClearInput struct { Description bool ControllerVpcID bool RoutingProfile bool + RoutingProfileOverrides bool + EffectiveRoutingProfile bool NVLinkLogicalPartitionID bool NetworkSecurityGroupID bool NetworkSecurityGroupPropagationDetails bool @@ -680,6 +881,7 @@ func (vsd VpcSQLDAO) Create(ctx context.Context, tx *db.Tx, input VpcCreateInput NVLinkLogicalPartitionID: input.NVLinkLogicalPartitionID, NetworkVirtualizationType: input.NetworkVirtualizationType, RoutingProfile: input.RoutingProfile, + RoutingProfileOverrides: input.RoutingProfileOverrides, ControllerVpcID: input.ControllerVpcID, NetworkSecurityGroupID: input.NetworkSecurityGroupID, NetworkSecurityGroupPropagationDetails: input.NetworkSecurityGroupPropagationDetails, @@ -755,6 +957,16 @@ func (vsd VpcSQLDAO) Update(ctx context.Context, tx *db.Tx, input VpcUpdateInput vsd.tracerSpan.SetAttribute(vpcDAOSpan, "routing_profile", *input.RoutingProfile) } + if input.RoutingProfileOverrides != nil { + v.RoutingProfileOverrides = input.RoutingProfileOverrides + updatedFields = append(updatedFields, "routing_profile_overrides") + } + + if input.EffectiveRoutingProfile != nil { + v.EffectiveRoutingProfile = input.EffectiveRoutingProfile + updatedFields = append(updatedFields, "effective_routing_profile") + } + if input.ActiveVni != nil { v.ActiveVni = input.ActiveVni updatedFields = append(updatedFields, "active_vni") @@ -850,6 +1062,16 @@ func (vsd VpcSQLDAO) Clear(ctx context.Context, tx *db.Tx, input VpcClearInput) updatedFields = append(updatedFields, "routing_profile") } + if input.RoutingProfileOverrides { + v.RoutingProfileOverrides = nil + updatedFields = append(updatedFields, "routing_profile_overrides") + } + + if input.EffectiveRoutingProfile { + v.EffectiveRoutingProfile = nil + updatedFields = append(updatedFields, "effective_routing_profile") + } + if input.Labels { v.Labels = nil updatedFields = append(updatedFields, "labels") diff --git a/rest-api/db/pkg/db/model/vpc_test.go b/rest-api/db/pkg/db/model/vpc_test.go index db7b9e436f..b396991f25 100644 --- a/rest-api/db/pkg/db/model/vpc_test.go +++ b/rest-api/db/pkg/db/model/vpc_test.go @@ -969,6 +969,7 @@ func TestVpcSQLDAO_CreateFromParams(t *testing.T) { siteID uuid.UUID networkVirtualizationType *string routingProfile *string + routingProfileOverrides *VpcRoutingProfileOverrides controllerVpcID *uuid.UUID activeVni *int networkSecurityGroupID *string @@ -997,6 +998,11 @@ func TestVpcSQLDAO_CreateFromParams(t *testing.T) { st := testBuildSite(t, dbSession, nil, ip.ID, "test-site", "Test Site", ip.Org, ipu.ID) networkSecurityGroup := testInstanceBuildNetworkSecurityGroup(t, dbSession, tn, st, "testNetworkSecurityGroup") + routingProfileOverrides := &VpcRoutingProfileOverrides{ + RouteTargetImports: &[]VpcRouteTarget{{ASN: 64512, VNI: 101}}, + LeakDefaultRouteFromUnderlay: cutil.GetPtr(false), + AllowedAnycastPrefixes: &[]string{}, + } vpc := &Vpc{ Name: "test-vpc", @@ -1007,6 +1013,7 @@ func TestVpcSQLDAO_CreateFromParams(t *testing.T) { SiteID: st.ID, NetworkVirtualizationType: cutil.GetPtr(VpcEthernetVirtualizer), RoutingProfile: cutil.GetPtr("INTERNAL"), + RoutingProfileOverrides: routingProfileOverrides, ControllerVpcID: cutil.GetPtr(uuid.New()), ActiveVni: nil, Vni: cutil.GetPtr(555), @@ -1054,6 +1061,7 @@ func TestVpcSQLDAO_CreateFromParams(t *testing.T) { siteID: vpc.SiteID, networkVirtualizationType: vpc.NetworkVirtualizationType, routingProfile: vpc.RoutingProfile, + routingProfileOverrides: vpc.RoutingProfileOverrides, controllerVpcID: vpc.ControllerVpcID, activeVni: vpc.ActiveVni, vni: vpc.Vni, @@ -1085,6 +1093,7 @@ func TestVpcSQLDAO_CreateFromParams(t *testing.T) { SiteID: tt.args.siteID, NetworkVirtualizationType: tt.args.networkVirtualizationType, RoutingProfile: tt.args.routingProfile, + RoutingProfileOverrides: tt.args.routingProfileOverrides, ControllerVpcID: tt.args.controllerVpcID, Vni: tt.args.vni, NetworkSecurityGroupID: tt.args.networkSecurityGroupID, @@ -1107,6 +1116,7 @@ func TestVpcSQLDAO_CreateFromParams(t *testing.T) { assert.Equal(t, len(tt.want.Labels), len(got.Labels)) assert.Equal(t, *tt.want.ControllerVpcID, *got.ControllerVpcID) assert.Equal(t, tt.want.RoutingProfile, got.RoutingProfile) + assert.Equal(t, tt.want.RoutingProfileOverrides, got.RoutingProfileOverrides) if tt.want.Vni != nil { assert.NotNil(t, got.Vni) assert.Equal(t, *tt.want.Vni, *got.Vni) @@ -1117,6 +1127,11 @@ func TestVpcSQLDAO_CreateFromParams(t *testing.T) { assert.Equal(t, tt.want.Status, got.Status) assert.Equal(t, tt.want.CreatedBy, got.CreatedBy) + // A fresh read proves the JSONB override value was persisted. + persisted, gerr := vsd.GetByID(tt.args.ctx, nil, got.ID, nil) + require.NoError(t, gerr) + assert.Equal(t, tt.want.RoutingProfileOverrides, persisted.RoutingProfileOverrides) + if tt.verifyChildSpanner { span := otrace.SpanFromContext(ctx) assert.True(t, span.SpanContext().IsValid()) @@ -1148,12 +1163,24 @@ func TestVpcSQLDAO_Update(t *testing.T) { networkSecurityGroup2 := testInstanceBuildNetworkSecurityGroup(t, dbSession, tn, st, "testNetworkSecurityGroup2") vpc := testBuildVpc(t, dbSession, nil, "test-vpc", nil, tn.Org, ip.ID, tn.ID, st.ID, nil, cutil.GetPtr(VpcEthernetVirtualizer), nil, nil, nil, tnu.ID, &networkSecurityGroup.ID) + routingProfileOverrides := &VpcRoutingProfileOverrides{ + RouteTargetsOnExports: &[]VpcRouteTarget{{ASN: 64513, VNI: 202}}, + TenantLeakCommunitiesAccepted: cutil.GetPtr(true), + } + effectiveRoutingProfile := &VpcEffectiveRoutingProfile{ + RouteTargetsOnExports: []VpcRouteTarget{{ASN: 64513, VNI: 202}}, + TenantLeakCommunitiesAccepted: true, + Internal: true, + AccessTier: 9, + } uvpc := &Vpc{ Name: "test-updated", Description: cutil.GetPtr("Test Updated"), NetworkVirtualizationType: cutil.GetPtr(VpcEthernetVirtualizerWithNVUE), RoutingProfile: cutil.GetPtr("EXTERNAL"), + RoutingProfileOverrides: routingProfileOverrides, + EffectiveRoutingProfile: effectiveRoutingProfile, NetworkSecurityGroupID: &networkSecurityGroup2.ID, ControllerVpcID: cutil.GetPtr(uuid.New()), ActiveVni: cutil.GetPtr(777), @@ -1186,6 +1213,8 @@ func TestVpcSQLDAO_Update(t *testing.T) { description *string networkVirtualizationType *string routingProfile *string + routingProfileOverrides *VpcRoutingProfileOverrides + effectiveRoutingProfile *VpcEffectiveRoutingProfile networkSecurityGroupID *string NetworkSecurityGroupPropagationDetails *NetworkSecurityGroupPropagationDetails ControllervpcID *uuid.UUID @@ -1215,6 +1244,8 @@ func TestVpcSQLDAO_Update(t *testing.T) { description: uvpc.Description, networkVirtualizationType: uvpc.NetworkVirtualizationType, routingProfile: uvpc.RoutingProfile, + routingProfileOverrides: uvpc.RoutingProfileOverrides, + effectiveRoutingProfile: uvpc.EffectiveRoutingProfile, networkSecurityGroupID: uvpc.NetworkSecurityGroupID, NetworkSecurityGroupPropagationDetails: uvpc.NetworkSecurityGroupPropagationDetails, ControllervpcID: uvpc.ControllerVpcID, @@ -1241,6 +1272,8 @@ func TestVpcSQLDAO_Update(t *testing.T) { Description: tt.args.description, NetworkVirtualizationType: tt.args.networkVirtualizationType, RoutingProfile: tt.args.routingProfile, + RoutingProfileOverrides: tt.args.routingProfileOverrides, + EffectiveRoutingProfile: tt.args.effectiveRoutingProfile, NetworkSecurityGroupID: tt.args.networkSecurityGroupID, NetworkSecurityGroupPropagationDetails: tt.args.NetworkSecurityGroupPropagationDetails, ControllerVpcID: tt.args.ControllervpcID, @@ -1261,6 +1294,8 @@ func TestVpcSQLDAO_Update(t *testing.T) { assert.Equal(t, *tt.want.Description, *got.Description) assert.Equal(t, *tt.want.NetworkVirtualizationType, *got.NetworkVirtualizationType) assert.Equal(t, tt.want.RoutingProfile, got.RoutingProfile) + assert.Equal(t, tt.want.RoutingProfileOverrides, got.RoutingProfileOverrides) + assert.Equal(t, tt.want.EffectiveRoutingProfile, got.EffectiveRoutingProfile) assert.Equal(t, *tt.want.ControllerVpcID, *got.ControllerVpcID) assert.Equal(t, *tt.want.ActiveVni, *got.ActiveVni) assert.Equal(t, *tt.want.Vni, *got.Vni) @@ -1276,6 +1311,12 @@ func TestVpcSQLDAO_Update(t *testing.T) { assert.Equal(t, tt.want.Labels, got.Labels) assert.Equal(t, tt.want.Status, got.Status) + // A fresh read verifies both JSONB columns, not only the update return value. + persisted, gerr := vsd.GetByID(tt.args.ctx, nil, got.ID, nil) + require.NoError(t, gerr) + assert.Equal(t, tt.want.RoutingProfileOverrides, persisted.RoutingProfileOverrides) + assert.Equal(t, tt.want.EffectiveRoutingProfile, persisted.EffectiveRoutingProfile) + assert.NotEqualValues(t, got.Updated, vpc.Updated) if tt.verifyChildSpanner { @@ -1286,6 +1327,25 @@ func TestVpcSQLDAO_Update(t *testing.T) { } }) } + + t.Run("preserves an omitted effective routing profile", func(t *testing.T) { + // Updating desired overrides alone must not clear cached controller state. + replacementOverrides := &VpcRoutingProfileOverrides{ + AllowedAnycastPrefixes: &[]string{"192.0.2.0/24"}, + } + updated, err := NewVpcDAO(dbSession).Update(ctx, nil, VpcUpdateInput{ + VpcID: vpc.ID, + RoutingProfileOverrides: replacementOverrides, + }) + require.NoError(t, err) + assert.Equal(t, replacementOverrides, updated.RoutingProfileOverrides) + assert.Equal(t, effectiveRoutingProfile, updated.EffectiveRoutingProfile) + + persisted, err := NewVpcDAO(dbSession).GetByID(ctx, nil, vpc.ID, nil) + require.NoError(t, err) + assert.Equal(t, replacementOverrides, persisted.RoutingProfileOverrides) + assert.Equal(t, effectiveRoutingProfile, persisted.EffectiveRoutingProfile) + }) } func TestVpcSQLDAO_DeleteByID(t *testing.T) { @@ -1386,6 +1446,14 @@ func TestVpcSQLDAO_ClearFromParams(t *testing.T) { vpc.NetworkSecurityGroupPropagationDetails = &NetworkSecurityGroupPropagationDetails{ NetworkSecurityGroupPropagationObjectStatus: &corev1.NetworkSecurityGroupPropagationObjectStatus{}, } + vpc.RoutingProfileOverrides = &VpcRoutingProfileOverrides{ + LeakDefaultRouteFromUnderlay: cutil.GetPtr(false), + } + vpc.EffectiveRoutingProfile = &VpcEffectiveRoutingProfile{ + LeakDefaultRouteFromUnderlay: true, + Internal: true, + AccessTier: 9, + } testUpdateVpc(t, dbSession, vpc) @@ -1405,6 +1473,8 @@ func TestVpcSQLDAO_ClearFromParams(t *testing.T) { labels bool networkSecuritygroupID bool networkSecurityGroupPropagationDetails bool + routingProfileOverrides bool + effectiveRoutingProfile bool } tests := []struct { name string @@ -1426,6 +1496,8 @@ func TestVpcSQLDAO_ClearFromParams(t *testing.T) { labels: true, networkSecuritygroupID: true, networkSecurityGroupPropagationDetails: true, + routingProfileOverrides: true, + effectiveRoutingProfile: true, }, wantErr: false, verifyChildSpanner: true, @@ -1445,6 +1517,8 @@ func TestVpcSQLDAO_ClearFromParams(t *testing.T) { Labels: tt.args.labels, NetworkSecurityGroupID: tt.args.networkSecuritygroupID, NetworkSecurityGroupPropagationDetails: tt.args.networkSecurityGroupPropagationDetails, + RoutingProfileOverrides: tt.args.routingProfileOverrides, + EffectiveRoutingProfile: tt.args.effectiveRoutingProfile, } got, err := vsd.Clear(tt.args.ctx, tt.args.tx, input) @@ -1474,6 +1548,20 @@ func TestVpcSQLDAO_ClearFromParams(t *testing.T) { assert.Nil(t, got.NetworkSecurityGroupID) assert.Nil(t, got.NetworkSecurityGroup) } + + if tt.args.routingProfileOverrides { + assert.Nil(t, got.RoutingProfileOverrides) + } + + if tt.args.effectiveRoutingProfile { + assert.Nil(t, got.EffectiveRoutingProfile) + } + + // A fresh lookup verifies both JSONB columns were cleared in storage. + persisted, gerr := vsd.GetByID(tt.args.ctx, nil, got.ID, nil) + require.NoError(t, gerr) + assert.Nil(t, persisted.RoutingProfileOverrides) + assert.Nil(t, persisted.EffectiveRoutingProfile) }) } } @@ -1754,6 +1842,8 @@ func TestVpc_FromProto(t *testing.T) { Vni: &staleRequested, ActiveVni: &staleActive, NVLinkLogicalPartitionID: &staleNvllp, + RoutingProfileOverrides: &VpcRoutingProfileOverrides{LeakDefaultRouteFromUnderlay: cutil.GetPtr(false)}, + EffectiveRoutingProfile: &VpcEffectiveRoutingProfile{Internal: true, AccessTier: 4}, Labels: map[string]string{"old": "val"}, } v.FromProto(&corev1.Vpc{ @@ -1776,6 +1866,8 @@ func TestVpc_FromProto(t *testing.T) { assert.Nil(t, v.Vni) assert.Nil(t, v.ActiveVni) assert.Nil(t, v.NVLinkLogicalPartitionID) + assert.Nil(t, v.RoutingProfileOverrides) + assert.Nil(t, v.EffectiveRoutingProfile) assert.Nil(t, v.Description) assert.Nil(t, v.Labels) }) @@ -1836,6 +1928,23 @@ func TestVpc_ToProtoFromProto_RoundTrip(t *testing.T) { Vni: &requested, ActiveVni: &active, NVLinkLogicalPartitionID: &nvllpID, + RoutingProfileOverrides: &VpcRoutingProfileOverrides{ + RouteTargetImports: &[]VpcRouteTarget{{ASN: 64512, VNI: 23}}, + RouteTargetsOnExports: &[]VpcRouteTarget{}, + LeakDefaultRouteFromUnderlay: cutil.GetPtr(false), + AcceptedLeaksFromUnderlay: &[]string{"10.0.0.1/24"}, + AllowedAnycastPrefixes: &[]string{}, + }, + EffectiveRoutingProfile: &VpcEffectiveRoutingProfile{ + RouteTargetImports: []VpcRouteTarget{{ASN: 64512, VNI: 23}}, + RouteTargetsOnExports: []VpcRouteTarget{}, + LeakDefaultRouteFromUnderlay: true, + LeakTenantHostRoutesToUnderlay: true, + AcceptedLeaksFromUnderlay: []string{"10.0.0.0/24"}, + AllowedAnycastPrefixes: []string{}, + Internal: true, + AccessTier: 4, + }, } got := &Vpc{} @@ -1850,4 +1959,38 @@ func TestVpc_ToProtoFromProto_RoundTrip(t *testing.T) { assert.Equal(t, orig.Vni, got.Vni) assert.Equal(t, orig.ActiveVni, got.ActiveVni) assert.Equal(t, orig.NVLinkLogicalPartitionID, got.NVLinkLogicalPartitionID) + assert.Equal(t, orig.RoutingProfileOverrides, got.RoutingProfileOverrides) + assert.Equal(t, orig.EffectiveRoutingProfile, got.EffectiveRoutingProfile) + + t.Run("normalizes nil effective routing profile lists", func(t *testing.T) { + // Core repeated fields canonicalize nil lists to allocated empty slices. + profileVpc := &Vpc{ + ID: uuid.New(), + Name: "vpc-a", + EffectiveRoutingProfile: &VpcEffectiveRoutingProfile{ + LeakDefaultRouteFromUnderlay: true, + LeakTenantHostRoutesToUnderlay: true, + TenantLeakCommunitiesAccepted: true, + Internal: true, + AccessTier: 4, + }, + } + + roundTripped := &Vpc{} + roundTripped.FromProto(profileVpc.ToProto()) + require.NotNil(t, roundTripped.EffectiveRoutingProfile) + assert.NotNil(t, roundTripped.EffectiveRoutingProfile.RouteTargetImports) + assert.Empty(t, roundTripped.EffectiveRoutingProfile.RouteTargetImports) + assert.NotNil(t, roundTripped.EffectiveRoutingProfile.RouteTargetsOnExports) + assert.Empty(t, roundTripped.EffectiveRoutingProfile.RouteTargetsOnExports) + assert.NotNil(t, roundTripped.EffectiveRoutingProfile.AcceptedLeaksFromUnderlay) + assert.Empty(t, roundTripped.EffectiveRoutingProfile.AcceptedLeaksFromUnderlay) + assert.NotNil(t, roundTripped.EffectiveRoutingProfile.AllowedAnycastPrefixes) + assert.Empty(t, roundTripped.EffectiveRoutingProfile.AllowedAnycastPrefixes) + assert.True(t, roundTripped.EffectiveRoutingProfile.LeakDefaultRouteFromUnderlay) + assert.True(t, roundTripped.EffectiveRoutingProfile.LeakTenantHostRoutesToUnderlay) + assert.True(t, roundTripped.EffectiveRoutingProfile.TenantLeakCommunitiesAccepted) + assert.True(t, roundTripped.EffectiveRoutingProfile.Internal) + assert.Equal(t, uint32(4), roundTripped.EffectiveRoutingProfile.AccessTier) + }) } diff --git a/rest-api/db/pkg/migrations/20260731215443_vpc_routing_profiles.go b/rest-api/db/pkg/migrations/20260731215443_vpc_routing_profiles.go new file mode 100644 index 0000000000..8709836c15 --- /dev/null +++ b/rest-api/db/pkg/migrations/20260731215443_vpc_routing_profiles.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + tx, terr := db.BeginTx(ctx, &sql.TxOptions{}) + if terr != nil { + handlePanic(terr, "failed to begin transaction") + } + + _, err := tx.ExecContext(ctx, `ALTER TABLE vpc ADD COLUMN IF NOT EXISTS routing_profile_overrides JSONB`) + handleError(tx, err) + + _, err = tx.ExecContext(ctx, `ALTER TABLE vpc ADD COLUMN IF NOT EXISTS effective_routing_profile JSONB`) + handleError(tx, err) + + terr = tx.Commit() + if terr != nil { + handlePanic(terr, "failed to commit transaction") + } + + fmt.Print(" [up migration] Added VPC routing-profile columns successfully. ") + return nil + }, func(ctx context.Context, db *bun.DB) error { + _, err := db.ExecContext(ctx, `ALTER TABLE vpc DROP COLUMN IF EXISTS routing_profile_overrides, DROP COLUMN IF EXISTS effective_routing_profile`) + if err != nil { + return err + } + fmt.Print(" [down migration] Dropped VPC routing-profile columns successfully. ") + return nil + }) +} diff --git a/rest-api/docs/index.html b/rest-api/docs/index.html index 0881217b7d..be60c4ddd2 100644 --- a/rest-api/docs/index.html +++ b/rest-api/docs/index.html @@ -2982,7 +2982,29 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Network virtualization type of the VPC. Flat VPCs hold instances on zero-DPU hosts (or hosts with their DPU in NIC mode); their interfaces are bound to underlay (HostInband) network segments and NICo does not drive their data plane.

routingProfile
string or null [ 3 .. 64 ] characters

Routing profile type for the VPC. Populated when Site has Native Networking enabled and network virtualization type is FNN.

-
requestedVni
integer or null [ 1 .. 65535 ]
VpcRoutingProfileOverrides (object) or null

Routing-profile properties set directly on the VPC. Unset properties inherit from the named routing profile.

+
One of
Array of objects or null (VpcRouteTarget)

Route targets imported into the VPC. An empty array overrides the named profile with no imports.

+
Array of objects or null (VpcRouteTarget)

Route targets attached to VPC exports. An empty array overrides the named profile with no export targets.

+
leakDefaultRouteFromUnderlay
boolean or null

Whether the underlay default route is leaked into the VPC.

+
leakTenantHostRoutesToUnderlay
boolean or null

Whether tenant host routes are leaked into the underlay.

+
tenantLeakCommunitiesAccepted
boolean or null

Whether tenant-supplied route-leak communities are honored.

+
acceptedLeaksFromUnderlay
Array of strings or null

IPv4 or IPv6 CIDR prefixes allowed to leak from the underlay. An empty array disables explicitly accepted leaks.

+
allowedAnycastPrefixes
Array of strings or null

IPv4 or IPv6 CIDR prefixes tenant hosts may announce as anycast routes. An empty array disables anycast announcements.

+
object (VpcEffectiveRoutingProfile)

Fully resolved routing profile last reported by Core for the VPC. This property is included only when the requesting Tenant has effective TargetedInstanceCreation permission for the VPC's Site.

+
required
Array of objects (VpcRouteTarget)
required
Array of objects (VpcRouteTarget)
leakDefaultRouteFromUnderlay
required
boolean
leakTenantHostRoutesToUnderlay
required
boolean
tenantLeakCommunitiesAccepted
required
boolean
acceptedLeaksFromUnderlay
required
Array of strings
allowedAnycastPrefixes
required
Array of strings
internal
required
boolean

Operator-controlled internal-routing classification inherited from the named profile.

+
accessTier
required
integer <int64> [ 0 .. 4294967295 ]

Operator-controlled access tier inherited from the named profile.

+
requestedVni
integer or null [ 1 .. 65535 ]

Explicitly requested VNI for the VPC if one was requested at creation time

vni
integer or null [ 1 .. 65535 ]

Active VNI assigned to the VPC

@@ -3046,7 +3068,31 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Network virtualization type of the VPC. If no value is specified, then defaults to FNN if Site has native networking enabled, or ETHERNET_VIRTUALIZER if native networking is disabled. Flat VPCs hold instances on zero-DPU hosts (or hosts with their DPU in NIC mode) and are never auto-selected -- FLAT must be specified explicitly.

routingProfile
string or null [ 3 .. 64 ] characters

Specify routing profile for the VPC. Only supported when networkVirtualizationType is set to FNN, or when networkVirtualizationType is omitted and Site has Native Networking enabled. Requires Tenant to have elevated privilege. Current accepted values are privileged-internal, internal, and external.

-
networkSecurityGroupId
string or null
VpcRoutingProfileOverrides (object) or null

Routing-profile properties to overlay on the resolved named profile. Only supported for FNN VPCs and requires TargetedInstanceCreation to be effective for the Tenant at the VPC's Site. routingProfile may be omitted when the Site and Tenant configuration select a named profile.

+
One of
Array of objects or null (VpcRouteTarget)

Route targets imported into the VPC. An empty array overrides the named profile with no imports.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
Array of objects or null (VpcRouteTarget)

Route targets attached to VPC exports. An empty array overrides the named profile with no export targets.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
boolean or null

Whether the underlay default route is leaked into the VPC.

+
leakTenantHostRoutesToUnderlay
boolean or null

Whether tenant host routes are leaked into the underlay.

+
tenantLeakCommunitiesAccepted
boolean or null

Whether tenant-supplied route-leak communities are honored.

+
acceptedLeaksFromUnderlay
Array of strings or null

IPv4 or IPv6 CIDR prefixes allowed to leak from the underlay. An empty array disables explicitly accepted leaks.

+
allowedAnycastPrefixes
Array of strings or null

IPv4 or IPv6 CIDR prefixes tenant hosts may announce as anycast routes. An empty array disables anycast announcements.

+
networkSecurityGroupId
string or null

ID of the Network Security Group to attach to the VPC

vni
integer or null [ 1 .. 65535 ]

Explicitly requested VNI for the VPC

@@ -3074,7 +3120,45 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Network virtualization type of the VPC. Flat VPCs hold instances on zero-DPU hosts (or hosts with their DPU in NIC mode); their interfaces are bound to underlay (HostInband) network segments and NICo does not drive their data plane.

routingProfile
string or null [ 3 .. 64 ] characters

Routing profile type for the VPC. Populated when Site has Native Networking enabled and network virtualization type is FNN.

-
requestedVni
integer or null [ 1 .. 65535 ]
VpcRoutingProfileOverrides (object) or null

Routing-profile properties set directly on the VPC. Unset properties inherit from the named routing profile.

+
One of
Array of objects or null (VpcRouteTarget)

Route targets imported into the VPC. An empty array overrides the named profile with no imports.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
Array of objects or null (VpcRouteTarget)

Route targets attached to VPC exports. An empty array overrides the named profile with no export targets.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
boolean or null

Whether the underlay default route is leaked into the VPC.

+
leakTenantHostRoutesToUnderlay
boolean or null

Whether tenant host routes are leaked into the underlay.

+
tenantLeakCommunitiesAccepted
boolean or null

Whether tenant-supplied route-leak communities are honored.

+
acceptedLeaksFromUnderlay
Array of strings or null

IPv4 or IPv6 CIDR prefixes allowed to leak from the underlay. An empty array disables explicitly accepted leaks.

+
allowedAnycastPrefixes
Array of strings or null

IPv4 or IPv6 CIDR prefixes tenant hosts may announce as anycast routes. An empty array disables anycast announcements.

+
object (VpcEffectiveRoutingProfile)

Fully resolved routing profile last reported by Core for the VPC. This property is included only when the requesting Tenant has effective TargetedInstanceCreation permission for the VPC's Site.

+
required
Array of objects (VpcRouteTarget)
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
required
Array of objects (VpcRouteTarget)
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
required
boolean
leakTenantHostRoutesToUnderlay
required
boolean
tenantLeakCommunitiesAccepted
required
boolean
acceptedLeaksFromUnderlay
required
Array of strings
allowedAnycastPrefixes
required
Array of strings
internal
required
boolean

Operator-controlled internal-routing classification inherited from the named profile.

+
accessTier
required
integer <int64> [ 0 .. 4294967295 ]

Operator-controlled access tier inherited from the named profile.

+
requestedVni
integer or null [ 1 .. 65535 ]

Explicitly requested VNI for the VPC if one was requested at creation time

vni
integer or null [ 1 .. 65535 ]

Active VNI assigned to the VPC

@@ -3164,7 +3248,45 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Network virtualization type of the VPC. Flat VPCs hold instances on zero-DPU hosts (or hosts with their DPU in NIC mode); their interfaces are bound to underlay (HostInband) network segments and NICo does not drive their data plane.

routingProfile
string or null [ 3 .. 64 ] characters

Routing profile type for the VPC. Populated when Site has Native Networking enabled and network virtualization type is FNN.

-
requestedVni
integer or null [ 1 .. 65535 ]
VpcRoutingProfileOverrides (object) or null

Routing-profile properties set directly on the VPC. Unset properties inherit from the named routing profile.

+
One of
Array of objects or null (VpcRouteTarget)

Route targets imported into the VPC. An empty array overrides the named profile with no imports.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
Array of objects or null (VpcRouteTarget)

Route targets attached to VPC exports. An empty array overrides the named profile with no export targets.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
boolean or null

Whether the underlay default route is leaked into the VPC.

+
leakTenantHostRoutesToUnderlay
boolean or null

Whether tenant host routes are leaked into the underlay.

+
tenantLeakCommunitiesAccepted
boolean or null

Whether tenant-supplied route-leak communities are honored.

+
acceptedLeaksFromUnderlay
Array of strings or null

IPv4 or IPv6 CIDR prefixes allowed to leak from the underlay. An empty array disables explicitly accepted leaks.

+
allowedAnycastPrefixes
Array of strings or null

IPv4 or IPv6 CIDR prefixes tenant hosts may announce as anycast routes. An empty array disables anycast announcements.

+
object (VpcEffectiveRoutingProfile)

Fully resolved routing profile last reported by Core for the VPC. This property is included only when the requesting Tenant has effective TargetedInstanceCreation permission for the VPC's Site.

+
required
Array of objects (VpcRouteTarget)
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
required
Array of objects (VpcRouteTarget)
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
required
boolean
leakTenantHostRoutesToUnderlay
required
boolean
tenantLeakCommunitiesAccepted
required
boolean
acceptedLeaksFromUnderlay
required
Array of strings
allowedAnycastPrefixes
required
Array of strings
internal
required
boolean

Operator-controlled internal-routing classification inherited from the named profile.

+
accessTier
required
integer <int64> [ 0 .. 4294967295 ]

Operator-controlled access tier inherited from the named profile.

+
requestedVni
integer or null [ 1 .. 65535 ]

Explicitly requested VNI for the VPC if one was requested at creation time

vni
integer or null [ 1 .. 65535 ]

Active VNI assigned to the VPC

@@ -3252,7 +3374,31 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

ID of the Network Security Group to attach to the VPC

nvLinkLogicalPartitionId
string or null <uuid>

ID of the default NVLink Logical Partition that GPUs for all Instances in the VPC will attach to. Can only be updated if VPC currently has no active Instances

-
object (Labels) <= 10 properties
VpcRoutingProfileOverrides (object) or null

Replaces the current inline routing-profile definition when present. Requires TargetedInstanceCreation to be effective for the Tenant at the VPC's Site. Omission or null preserves the current definition. An empty object restores inheritance for every property; a partial object replaces the previous definition and inherits its omitted properties from the named profile.

+
One of
Array of objects or null (VpcRouteTarget)

Route targets imported into the VPC. An empty array overrides the named profile with no imports.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
Array of objects or null (VpcRouteTarget)

Route targets attached to VPC exports. An empty array overrides the named profile with no export targets.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
boolean or null

Whether the underlay default route is leaked into the VPC.

+
leakTenantHostRoutesToUnderlay
boolean or null

Whether tenant host routes are leaked into the underlay.

+
tenantLeakCommunitiesAccepted
boolean or null

Whether tenant-supplied route-leak communities are honored.

+
acceptedLeaksFromUnderlay
Array of strings or null

IPv4 or IPv6 CIDR prefixes allowed to leak from the underlay. An empty array disables explicitly accepted leaks.

+
allowedAnycastPrefixes
Array of strings or null

IPv4 or IPv6 CIDR prefixes tenant hosts may announce as anycast routes. An empty array disables anycast announcements.

+
object (Labels) <= 10 properties

Update labels of the VPC. Up to 10 key-value pairs can be specified. The labels will be replaced with the labels sent in the request. Any labels not included in the request will be removed. To retain existing labels, fetch them first and include them in this request.

property name*
additional property
string

Responses

routingProfile
string or null [ 3 .. 64 ] characters

Routing profile type for the VPC. Populated when Site has Native Networking enabled and network virtualization type is FNN.

-
requestedVni
integer or null [ 1 .. 65535 ]
VpcRoutingProfileOverrides (object) or null

Routing-profile properties set directly on the VPC. Unset properties inherit from the named routing profile.

+
One of
Array of objects or null (VpcRouteTarget)

Route targets imported into the VPC. An empty array overrides the named profile with no imports.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
Array of objects or null (VpcRouteTarget)

Route targets attached to VPC exports. An empty array overrides the named profile with no export targets.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
boolean or null

Whether the underlay default route is leaked into the VPC.

+
leakTenantHostRoutesToUnderlay
boolean or null

Whether tenant host routes are leaked into the underlay.

+
tenantLeakCommunitiesAccepted
boolean or null

Whether tenant-supplied route-leak communities are honored.

+
acceptedLeaksFromUnderlay
Array of strings or null

IPv4 or IPv6 CIDR prefixes allowed to leak from the underlay. An empty array disables explicitly accepted leaks.

+
allowedAnycastPrefixes
Array of strings or null

IPv4 or IPv6 CIDR prefixes tenant hosts may announce as anycast routes. An empty array disables anycast announcements.

+
object (VpcEffectiveRoutingProfile)

Fully resolved routing profile last reported by Core for the VPC. This property is included only when the requesting Tenant has effective TargetedInstanceCreation permission for the VPC's Site.

+
required
Array of objects (VpcRouteTarget)
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
required
Array of objects (VpcRouteTarget)
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
required
boolean
leakTenantHostRoutesToUnderlay
required
boolean
tenantLeakCommunitiesAccepted
required
boolean
acceptedLeaksFromUnderlay
required
Array of strings
allowedAnycastPrefixes
required
Array of strings
internal
required
boolean

Operator-controlled internal-routing classification inherited from the named profile.

+
accessTier
required
integer <int64> [ 0 .. 4294967295 ]

Operator-controlled access tier inherited from the named profile.

+
requestedVni
integer or null [ 1 .. 65535 ]

Explicitly requested VNI for the VPC if one was requested at creation time

vni
integer or null [ 1 .. 65535 ]

Active VNI assigned to the VPC

@@ -3368,7 +3552,45 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Network virtualization type of the VPC. Flat VPCs hold instances on zero-DPU hosts (or hosts with their DPU in NIC mode); their interfaces are bound to underlay (HostInband) network segments and NICo does not drive their data plane.

routingProfile
string or null [ 3 .. 64 ] characters

Routing profile type for the VPC. Populated when Site has Native Networking enabled and network virtualization type is FNN.

-
requestedVni
integer or null [ 1 .. 65535 ]
VpcRoutingProfileOverrides (object) or null

Routing-profile properties set directly on the VPC. Unset properties inherit from the named routing profile.

+
One of
Array of objects or null (VpcRouteTarget)

Route targets imported into the VPC. An empty array overrides the named profile with no imports.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
Array of objects or null (VpcRouteTarget)

Route targets attached to VPC exports. An empty array overrides the named profile with no export targets.

+
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
boolean or null

Whether the underlay default route is leaked into the VPC.

+
leakTenantHostRoutesToUnderlay
boolean or null

Whether tenant host routes are leaked into the underlay.

+
tenantLeakCommunitiesAccepted
boolean or null

Whether tenant-supplied route-leak communities are honored.

+
acceptedLeaksFromUnderlay
Array of strings or null

IPv4 or IPv6 CIDR prefixes allowed to leak from the underlay. An empty array disables explicitly accepted leaks.

+
allowedAnycastPrefixes
Array of strings or null

IPv4 or IPv6 CIDR prefixes tenant hosts may announce as anycast routes. An empty array disables anycast announcements.

+
object (VpcEffectiveRoutingProfile)

Fully resolved routing profile last reported by Core for the VPC. This property is included only when the requesting Tenant has effective TargetedInstanceCreation permission for the VPC's Site.

+
required
Array of objects (VpcRouteTarget)
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
required
Array of objects (VpcRouteTarget)
Array
asn
integer <int64> [ 0 .. 4294967295 ]

Autonomous system number.

+
vni
integer <int64> [ 0 .. 4294967295 ]

Route-target VNI.

+
leakDefaultRouteFromUnderlay
required
boolean
leakTenantHostRoutesToUnderlay
required
boolean
tenantLeakCommunitiesAccepted
required
boolean
acceptedLeaksFromUnderlay
required
Array of strings
allowedAnycastPrefixes
required
Array of strings
internal
required
boolean

Operator-controlled internal-routing classification inherited from the named profile.

+
accessTier
required
integer <int64> [ 0 .. 4294967295 ]

Operator-controlled access tier inherited from the named profile.

+
requestedVni
integer or null [ 1 .. 65535 ]

Explicitly requested VNI for the VPC if one was requested at creation time

vni
integer or null [ 1 .. 65535 ]

Active VNI assigned to the VPC

@@ -16928,7 +17150,7 @@

Typical API Call Flow for Tenant