diff --git a/rest-api/api/pkg/api/handler/infinibandinterface.go b/rest-api/api/pkg/api/handler/infinibandinterface.go index 2e9091604f..3b34e4d5fe 100644 --- a/rest-api/api/pkg/api/handler/infinibandinterface.go +++ b/rest-api/api/pkg/api/handler/infinibandinterface.go @@ -175,7 +175,7 @@ func (gaibih GetAllInfiniBandInterfaceHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(gaibih.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant", nil) diff --git a/rest-api/api/pkg/api/handler/instance.go b/rest-api/api/pkg/api/handler/instance.go index e9e6acdcb3..2fb96562c0 100644 --- a/rest-api/api/pkg/api/handler/instance.go +++ b/rest-api/api/pkg/api/handler/instance.go @@ -3963,7 +3963,7 @@ func (gih GetInstanceHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(gih.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant", nil) @@ -4208,7 +4208,7 @@ func (gaih GetAllInstanceHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(gaih.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant for org", nil) @@ -5068,7 +5068,7 @@ func (gisdh GetInstanceStatusDetailsHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(gisdh.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant", nil) diff --git a/rest-api/api/pkg/api/handler/nvlinkinterface.go b/rest-api/api/pkg/api/handler/nvlinkinterface.go index 6cdd97cced..3840188917 100644 --- a/rest-api/api/pkg/api/handler/nvlinkinterface.go +++ b/rest-api/api/pkg/api/handler/nvlinkinterface.go @@ -176,7 +176,7 @@ func (gaish GetAllNVLinkInterfaceHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(gaish.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant", nil) diff --git a/rest-api/api/pkg/api/handler/serviceaccount.go b/rest-api/api/pkg/api/handler/serviceaccount.go index d5d42db12e..7004790958 100644 --- a/rest-api/api/pkg/api/handler/serviceaccount.go +++ b/rest-api/api/pkg/api/handler/serviceaccount.go @@ -90,7 +90,7 @@ func (gcsah GetCurrentServiceAccountHandler) Handle(c echo.Context) error { var tn *cdbm.Tenant - tns, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tns, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant for org, DB error", nil) diff --git a/rest-api/api/pkg/api/handler/serviceaccount_test.go b/rest-api/api/pkg/api/handler/serviceaccount_test.go index 2c88f76e57..d06ad46120 100644 --- a/rest-api/api/pkg/api/handler/serviceaccount_test.go +++ b/rest-api/api/pkg/api/handler/serviceaccount_test.go @@ -20,6 +20,8 @@ import ( "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + cutil "github.com/NVIDIA/infra-controller/rest-api/common/pkg/util" ) func TestServiceAccountHandler_GetCurrent(t *testing.T) { @@ -124,7 +126,7 @@ func TestServiceAccountHandler_GetCurrent(t *testing.T) { ips, ipErr := ipDAO.GetAllByOrg(ctx, nil, org1, nil) require.NoError(t, ipErr) require.Len(t, ips, 1) - tns, tnErr := tnDAO.GetAllByOrg(ctx, nil, org1, nil) + tns, _, tnErr := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org1}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) require.NoError(t, tnErr) require.Len(t, tns, 1) diff --git a/rest-api/api/pkg/api/handler/sshkey.go b/rest-api/api/pkg/api/handler/sshkey.go index 09b87449e4..b90dff8ffc 100644 --- a/rest-api/api/pkg/api/handler/sshkey.go +++ b/rest-api/api/pkg/api/handler/sshkey.go @@ -418,7 +418,7 @@ func (uskh UpdateSSHKeyHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(uskh.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant for org", nil) @@ -586,7 +586,7 @@ func (gskh GetSSHKeyHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(gskh.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant for org", nil) @@ -706,7 +706,7 @@ func (gaskh GetAllSSHKeyHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(gaskh.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant for org", nil) @@ -852,7 +852,7 @@ func (dskh DeleteSSHKeyHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(dskh.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant for org", nil) diff --git a/rest-api/api/pkg/api/handler/tenant.go b/rest-api/api/pkg/api/handler/tenant.go index 345a815134..4b5ab28bf8 100644 --- a/rest-api/api/pkg/api/handler/tenant.go +++ b/rest-api/api/pkg/api/handler/tenant.go @@ -158,7 +158,7 @@ func (gcth GetCurrentTenantHandler) Handle(c echo.Context) error { // Re-read inside the tx so the existence check and any create/update // happen against the same locked snapshot. - tns, derr := tnDAO.GetAllByOrg(ctx, tx, org, nil) + tns, _, derr := tnDAO.GetAll(ctx, tx, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if derr != nil { logger.Error().Err(derr).Msg("error retrieving Tenant for this org") return nil, cutil.NewAPIError(http.StatusInternalServerError, "Failed to retrieve current Tenant", nil) @@ -274,7 +274,7 @@ func (gcth GetCurrentTenantStatsHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(gcth.dbSession) - tns, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tns, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant", nil) diff --git a/rest-api/api/pkg/api/handler/tenantaccount.go b/rest-api/api/pkg/api/handler/tenantaccount.go index 48f27c3331..2e9df2574d 100644 --- a/rest-api/api/pkg/api/handler/tenantaccount.go +++ b/rest-api/api/pkg/api/handler/tenantaccount.go @@ -141,7 +141,7 @@ func (ctah CreateTenantAccountHandler) Handle(c echo.Context) error { tenantOrg = &tenant.Org } else { tenantOrg = apiRequest.TenantOrg - tenants, serr := tnDAO.GetAllByOrg(ctx, nil, *tenantOrg, nil) + tenants, _, serr := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{*tenantOrg}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if serr != nil { logger.Warn().Err(err).Msg("error retrieving tenant") return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to retrieve Tenant specified in request", nil) diff --git a/rest-api/api/pkg/api/handler/util/common/common.go b/rest-api/api/pkg/api/handler/util/common/common.go index fd0c6c7c8a..5023f4e05e 100644 --- a/rest-api/api/pkg/api/handler/util/common/common.go +++ b/rest-api/api/pkg/api/handler/util/common/common.go @@ -100,7 +100,7 @@ func GetInfrastructureProviderForOrg(ctx context.Context, tx *cdb.Tx, dbSession func GetTenantForOrg(ctx context.Context, tx *cdb.Tx, dbSession *cdb.Session, org string) (*cdbm.Tenant, error) { tnDAO := cdbm.NewTenantDAO(dbSession) - ts, err := tnDAO.GetAllByOrg(ctx, tx, org, nil) + ts, _, err := tnDAO.GetAll(ctx, tx, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { return nil, err } diff --git a/rest-api/api/pkg/api/handler/vpc.go b/rest-api/api/pkg/api/handler/vpc.go index b102685d32..6b93e38db2 100644 --- a/rest-api/api/pkg/api/handler/vpc.go +++ b/rest-api/api/pkg/api/handler/vpc.go @@ -1423,7 +1423,7 @@ func (gavh GetAllVPCHandler) Handle(c echo.Context) error { // Get Tenant for this org tnDAO := cdbm.NewTenantDAO(gavh.dbSession) - tenants, err := tnDAO.GetAllByOrg(ctx, nil, org, nil) + tenants, _, err := tnDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{Orgs: []string{org}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) if err != nil { logger.Error().Err(err).Msg("error retrieving Tenant for this org") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant for org", nil) diff --git a/rest-api/api/pkg/api/handler/vpcpeering.go b/rest-api/api/pkg/api/handler/vpcpeering.go index 2c1d6baa8f..3b42515c7a 100644 --- a/rest-api/api/pkg/api/handler/vpcpeering.go +++ b/rest-api/api/pkg/api/handler/vpcpeering.go @@ -395,7 +395,7 @@ func (cvph CreateVpcPeeringHandler) Handle(c echo.Context) error { } // Update API model with best-known status. - apiVpcPeering := model.NewAPIVpcPeering(*vpcPeering) + apiVpcPeering := model.NewAPIVpcPeering(*vpcPeering, nil) apiVpcPeering.Status = status logger.Info().Msg("finishing API handler") @@ -432,6 +432,9 @@ func NewGetAllVpcPeeringHandler(dbSession *cdb.Session, tc tclient.Client, cfg * // @Param org path string true "Name of NGC organization" // @Param siteId query string false "Filter by Site ID" // @Param isMultiTenant query bool false "Filter by single-tenant or multi-tenant peerings" +// @Param status query string false "Filter by status (repeatable for multiple values)" +// @Param vpcId query string false "Filter by VPC ID involved in the peering (as vpc1 or vpc2)" +// @Param peerTenantId query string false "Filter by tenant ID of a VPC involved in the peering (repeatable for multiple values)" // @Param includeRelation query string false "Related entities to include in response e.g. 'Vpc1', 'Vpc2', 'Site'" // @Param pageNumber query integer false "Page number of results returned" // @Param pageSize query integer false "Number of results per page" @@ -524,6 +527,95 @@ func (gavph GetAllVpcPeeringHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, errMsg, nil) } + // Get status from query param + qStatuses := qParams["status"] + if len(qStatuses) > 0 { + gavph.tracerSpan.SetAttribute(handlerSpan, attribute.StringSlice("status", qStatuses), logger) + for _, status := range qStatuses { + if !cdbm.VpcPeeringStatusMap[status] { + logger.Warn().Msg(fmt.Sprintf("invalid value in status query: %v", status)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid Status value %v in query", status), nil) + } + filterInput.Statuses = append(filterInput.Statuses, status) + } + } + + // Get vpcId from query param + vpcIDStrs := qParams["vpcId"] + if len(vpcIDStrs) > 0 { + gavph.tracerSpan.SetAttribute(handlerSpan, attribute.StringSlice("vpcId", vpcIDStrs), logger) + vpcIDs := make([]uuid.UUID, 0, len(vpcIDStrs)) + for _, vpcIDStr := range vpcIDStrs { + vpcID, err := uuid.Parse(vpcIDStr) + if err != nil { + logger.Warn().Msg(fmt.Sprintf("invalid value in vpcId query: %v", vpcIDStr)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid VPC ID %v in query", vpcIDStr), nil) + } + vpcIDs = append(vpcIDs, vpcID) + } + vpcDAO := cdbm.NewVpcDAO(gavph.dbSession) + vpcs, _, err := vpcDAO.GetAll( + ctx, + nil, + cdbm.VpcFilterInput{VpcIDs: vpcIDs}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + nil, + ) + if err != nil { + logger.Error().Err(err).Msg("error retrieving VPCs from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve VPCs specified in query", nil) + } + vpcIDsMap := make(map[uuid.UUID]struct{}, len(vpcs)) + for _, vpc := range vpcs { + vpcIDsMap[vpc.ID] = struct{}{} + } + for _, vpcID := range vpcIDs { + if _, ok := vpcIDsMap[vpcID]; !ok { + logger.Warn().Msg(fmt.Sprintf("could not find VPC with ID %v specified in query", vpcID)) + return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find VPC with ID %v specified in query", vpcID), nil) + } + } + filterInput.VpcIDs = vpcIDs + } + + // Get peerTenantId from query param + peerTenantIDStrs := qParams["peerTenantId"] + if len(peerTenantIDStrs) > 0 { + gavph.tracerSpan.SetAttribute(handlerSpan, attribute.StringSlice("peerTenantId", peerTenantIDStrs), logger) + peerTenantIDs := make([]uuid.UUID, 0, len(peerTenantIDStrs)) + for _, peerTenantIDStr := range peerTenantIDStrs { + peerTenantID, err := uuid.Parse(peerTenantIDStr) + if err != nil { + logger.Warn().Msg(fmt.Sprintf("invalid value in peerTenantId query: %v", peerTenantIDStr)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid peer tenant ID %v in query", peerTenantIDStr), nil) + } + peerTenantIDs = append(peerTenantIDs, peerTenantID) + } + tnDAO := cdbm.NewTenantDAO(gavph.dbSession) + peerTenants, _, err := tnDAO.GetAll( + ctx, + nil, + cdbm.TenantFilterInput{TenantIDs: peerTenantIDs}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + nil, + ) + if err != nil { + logger.Error().Err(err).Msg("error retrieving Tenants from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve tenants specified in query", nil) + } + peerTenantIDsMap := make(map[uuid.UUID]struct{}, len(peerTenants)) + for _, peerTenant := range peerTenants { + peerTenantIDsMap[peerTenant.ID] = struct{}{} + } + for _, peerTenantID := range peerTenantIDs { + if _, ok := peerTenantIDsMap[peerTenantID]; !ok { + logger.Warn().Msg(fmt.Sprintf("could not find tenant with ID %v specified in query", peerTenantID)) + return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find tenant with ID %v specified in query", peerTenantID), nil) + } + } + filterInput.PeerTenantIDs = peerTenantIDs + } + // Validate pagination request pageRequest := pagination.PageRequest{} err := c.Bind(&pageRequest) @@ -558,16 +650,47 @@ func (gavph GetAllVpcPeeringHandler) Handle(c echo.Context) error { Offset: pageRequest.Offset, OrderBy: pageRequest.OrderBy, } + + // If VPC ID or peer tenant ID is specified, include the VPC and tenant relations + if len(vpcIDStrs) > 0 || len(peerTenantIDStrs) > 0 { + for _, relation := range []string{cdbm.Vpc1RelationName, cdbm.Vpc2RelationName, cdbm.TenantRelationName} { + if !slices.Contains(qIncludeRelations, relation) { + qIncludeRelations = append(qIncludeRelations, relation) + } + } + } vpcPeerings, total, err := vpcPeeringDAO.GetAll(ctx, nil, filterInput, vpcPeeringPageInput, qIncludeRelations) if err != nil { logger.Error().Err(err).Msg("error retrieving VPC Peerings from DB") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve VPC Peerings, DB error", nil) } + dbMappedPeeringTenants := make(map[uuid.UUID]*cdbm.Tenant) + vpcPeeringTenantIDs := []uuid.UUID{} + for _, vpcPeering := range vpcPeerings { + if vpcPeering.Vpc1 != nil { + vpcPeeringTenantIDs = append(vpcPeeringTenantIDs, vpcPeering.Vpc1.TenantID) + } + if vpcPeering.Vpc2 != nil { + vpcPeeringTenantIDs = append(vpcPeeringTenantIDs, vpcPeering.Vpc2.TenantID) + } + } + if len(vpcPeeringTenantIDs) > 0 { + tenantDAO := cdbm.NewTenantDAO(gavph.dbSession) + tenants, _, err := tenantDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{TenantIDs: vpcPeeringTenantIDs}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) + if err != nil { + logger.Error().Err(err).Msg("error retrieving Tenants from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenants, DB error", nil) + } + for _, tenant := range tenants { + dbMappedPeeringTenants[tenant.ID] = &tenant + } + } + // Build API response apiVpcPeerings := make([]model.APIVpcPeering, len(vpcPeerings)) for i, vpcPeering := range vpcPeerings { - apiVpcPeerings[i] = model.NewAPIVpcPeering(vpcPeering) + apiVpcPeerings[i] = model.NewAPIVpcPeering(vpcPeering, dbMappedPeeringTenants) } // Create pagination response header @@ -614,7 +737,7 @@ func NewGetVpcPeeringHandler(dbSession *cdb.Session, tc tclient.Client, cfg *con // @Security ApiKeyAuth // @Param org path string true "Name of NGC organization" // @Param id path string true "ID of VPC Peering" -// @Param includeRelation query string false "Related entities to include in response e.g. 'Vpc1', 'Vpc2', 'Site'"" +// @Param includeRelation query string false "Related entities to include in response e.g. 'Vpc1', 'Vpc2', 'Site', 'Tenant'"" // @Success 200 {object} model.APIVpcPeering // @Router /v2/org/{org}/nico/vpc-peering/{id} [get] func (gvph GetVpcPeeringHandler) Handle(c echo.Context) error { @@ -671,7 +794,7 @@ func (gvph GetVpcPeeringHandler) Handle(c echo.Context) error { if !providerAuthorized && tenant != nil { // Get two VPCs of the VPC Peering vpcDAO := cdbm.NewVpcDAO(gvph.dbSession) - vpc1, err := vpcDAO.GetByID(ctx, nil, vpcPeering.Vpc1ID, nil) + vpc1, err := vpcDAO.GetByID(ctx, nil, vpcPeering.Vpc1ID, []string{cdbm.TenantRelationName}) if err != nil { if err == cdb.ErrDoesNotExist { return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find VPC with ID: %s", vpcPeering.Vpc1ID.String()), nil) @@ -679,7 +802,8 @@ func (gvph GetVpcPeeringHandler) Handle(c echo.Context) error { logger.Error().Err(err).Msg("error retrieving VPC 1 of VPC Peering from DB") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to retrieve VPC 1 with ID: %s, DB error", vpcPeering.Vpc1ID.String()), nil) } - vpc2, err := vpcDAO.GetByID(ctx, nil, vpcPeering.Vpc2ID, nil) + + vpc2, err := vpcDAO.GetByID(ctx, nil, vpcPeering.Vpc2ID, []string{cdbm.TenantRelationName}) if err != nil { if err == cdb.ErrDoesNotExist { return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find VPC with ID: %s", vpcPeering.Vpc2ID.String()), nil) @@ -706,8 +830,32 @@ func (gvph GetVpcPeeringHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "User does not have access to the VPC Peering", nil) } + // Map tenant to the VPCs + dbMappedPeeringTenants := make(map[uuid.UUID]*cdbm.Tenant) + tenantIDs := []uuid.UUID{} + + if vpcPeering.Vpc1 != nil { + tenantIDs = append(tenantIDs, vpcPeering.Vpc1.TenantID) + } + if vpcPeering.Vpc2 != nil { + tenantIDs = append(tenantIDs, vpcPeering.Vpc2.TenantID) + } + + // Fetch Tenants from DB if VPCs have tenants + if len(tenantIDs) > 0 { + tenantDAO := cdbm.NewTenantDAO(gvph.dbSession) + tenants, _, err := tenantDAO.GetAll(ctx, nil, cdbm.TenantFilterInput{TenantIDs: tenantIDs}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) + if err != nil { + logger.Error().Err(err).Msg("error retrieving Tenants from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenants, DB error", nil) + } + for _, tenant := range tenants { + dbMappedPeeringTenants[tenant.ID] = &tenant + } + } + // Convert to API model - apiVpcPeering := model.NewAPIVpcPeering(*vpcPeering) + apiVpcPeering := model.NewAPIVpcPeering(*vpcPeering, dbMappedPeeringTenants) logger.Info().Msg("finishing API handler") diff --git a/rest-api/api/pkg/api/handler/vpcpeering_test.go b/rest-api/api/pkg/api/handler/vpcpeering_test.go index 812cb420b2..2139826f17 100644 --- a/rest-api/api/pkg/api/handler/vpcpeering_test.go +++ b/rest-api/api/pkg/api/handler/vpcpeering_test.go @@ -462,7 +462,7 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { vpc8 := common.TestBuildVPC(t, dbSession, "vpc-8", ip, tnProvider, st1, nil, nil, nil, cdbm.VpcStatusReady, ipu2) // Tenant-created peerings - _ = common.TestBuildVpcPeering(t, dbSession, vpc1.ID, vpc2.ID, st1.ID, nil, &tn1.ID, false, tnu1.ID) + vp12 := common.TestBuildVpcPeering(t, dbSession, vpc1.ID, vpc2.ID, st1.ID, nil, &tn1.ID, false, tnu1.ID) _ = common.TestBuildVpcPeering(t, dbSession, vpc1.ID, vpc3.ID, st1.ID, nil, &tn1.ID, false, tnu1.ID) _ = common.TestBuildVpcPeering(t, dbSession, vpc7.ID, vpc8.ID, st1.ID, nil, &tnProvider.ID, false, ipu2.ID) @@ -472,18 +472,26 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { _ = common.TestBuildVpcPeering(t, dbSession, vpc3.ID, vpc4.ID, st1.ID, &ip.ID, nil, true, ipu.ID) _ = common.TestBuildVpcPeering(t, dbSession, vpc5.ID, vpc6.ID, st2.ID, &ip2.ID, nil, true, ipu2.ID) + vpPeeringDAO := cdbm.NewVpcPeeringDAO(dbSession) + err := vpPeeringDAO.UpdateStatusByID(ctx, nil, vp12.ID, cdbm.VpcPeeringStatusReady) + require.NoError(t, err) + tracer, _, ctx := common.TestCommonTraceProviderSetup(t, ctx) mockTC := &tmocks.Client{} cfg := common.GetTestConfig() tests := []struct { - name string - reqOrgName string - queryParams map[string]string - user *cdbm.User - expectedStatus int - expectedCount int - validatePagination bool + name string + reqOrgName string + queryParams map[string]string + queryString string + user *cdbm.User + expectedStatus int + expectedCount int + validatePagination bool + validateTenantIDs bool + validateTenantIDsPresent bool + requirePeerTenantIDs []uuid.UUID }{ { name: "error when user not found in request context", @@ -511,14 +519,6 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { user: tnu1, expectedStatus: http.StatusBadRequest, }, - { - name: "tenant admin 1 lists across all sites when siteId omitted", - reqOrgName: tnOrg1, - user: tnu1, - expectedStatus: http.StatusOK, - expectedCount: 6, - validatePagination: true, - }, { name: "tenant admin 1 lists peerings in site 1", reqOrgName: tnOrg1, @@ -605,6 +605,109 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { expectedStatus: http.StatusOK, expectedCount: 1, }, + { + name: "error when status query value is invalid", + reqOrgName: tnOrg1, + queryParams: map[string]string{"status": "NotAStatus"}, + user: tnu1, + expectedStatus: http.StatusBadRequest, + }, + { + name: "tenant admin 1 filters by ready status", + reqOrgName: tnOrg1, + queryParams: map[string]string{"status": cdbm.VpcPeeringStatusReady}, + user: tnu1, + expectedStatus: http.StatusOK, + expectedCount: 1, + }, + { + name: "tenant admin 1 filters by pending status", + reqOrgName: tnOrg1, + queryParams: map[string]string{"status": cdbm.VpcPeeringStatusPending}, + user: tnu1, + expectedStatus: http.StatusOK, + expectedCount: 5, + }, + { + name: "error when vpcId is invalid", + reqOrgName: tnOrg1, + queryParams: map[string]string{"vpcId": "not-a-uuid"}, + user: tnu1, + expectedStatus: http.StatusBadRequest, + }, + { + name: "error when vpcId does not exist", + reqOrgName: tnOrg1, + queryParams: map[string]string{"vpcId": uuid.New().String()}, + user: tnu1, + expectedStatus: http.StatusNotFound, + }, + { + name: "tenant admin 1 filters by vpcId", + reqOrgName: tnOrg1, + queryParams: map[string]string{"vpcId": vpc1.ID.String()}, + user: tnu1, + expectedStatus: http.StatusOK, + expectedCount: 3, + }, + { + name: "error when peerTenantId is invalid", + reqOrgName: tnOrg1, + queryParams: map[string]string{"peerTenantId": "not-a-uuid"}, + user: tnu1, + expectedStatus: http.StatusBadRequest, + }, + { + name: "error when peerTenantId does not exist", + reqOrgName: tnOrg1, + queryParams: map[string]string{"peerTenantId": uuid.New().String()}, + user: tnu1, + expectedStatus: http.StatusNotFound, + }, + { + name: "tenant admin 1 filters by peerTenantId", + reqOrgName: tnOrg1, + queryParams: map[string]string{"peerTenantId": tn2.ID.String()}, + user: tnu1, + expectedStatus: http.StatusOK, + expectedCount: 4, + }, + { + name: "provider and tenant admin filters by peerTenantId for tenant 2", + reqOrgName: ipOrg2, + queryParams: map[string]string{"peerTenantId": tn2.ID.String()}, + user: ipu2, + expectedStatus: http.StatusOK, + expectedCount: 1, + requirePeerTenantIDs: []uuid.UUID{tn2.ID}, + }, + { + name: "provider and tenant admin filters by peerTenantId for provider tenant", + reqOrgName: ipOrg2, + queryParams: map[string]string{"peerTenantId": tnProvider.ID.String()}, + user: ipu2, + expectedStatus: http.StatusOK, + expectedCount: 1, + requirePeerTenantIDs: []uuid.UUID{tnProvider.ID}, + }, + { + name: "provider and tenant admin filters by multiple peerTenantId values", + reqOrgName: ipOrg2, + queryString: fmt.Sprintf("peerTenantId=%s&peerTenantId=%s", tn2.ID, tnProvider.ID), + user: ipu2, + expectedStatus: http.StatusOK, + expectedCount: 2, + requirePeerTenantIDs: []uuid.UUID{tn2.ID, tnProvider.ID}, + }, + { + name: "tenant admin 1 filters by vpcId and peerTenantId", + reqOrgName: tnOrg1, + queryParams: map[string]string{"vpcId": vpc1.ID.String(), "peerTenantId": tn2.ID.String()}, + user: tnu1, + expectedStatus: http.StatusOK, + expectedCount: 1, + validateTenantIDs: true, + }, } for _, tt := range tests { @@ -618,13 +721,17 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { e := echo.New() url := "/?" - first := true - for k, v := range tt.queryParams { - if !first { - url += "&" + if tt.queryString != "" { + url += tt.queryString + } else { + first := true + for k, v := range tt.queryParams { + if !first { + url += "&" + } + url += fmt.Sprintf("%s=%s", k, v) + first = false } - url += fmt.Sprintf("%s=%s", k, v) - first = false } req := httptest.NewRequest(http.MethodGet, url, nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) @@ -655,6 +762,38 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { require.NoError(t, err) assert.Equal(t, tt.expectedCount, pageResp.Total) } + if tt.validateTenantIDsPresent { + for _, peering := range list { + require.NotNil(t, peering.Vpc1.Tenant.ID) + require.NotNil(t, peering.Vpc2.Tenant.ID) + } + } + if tt.validateTenantIDs { + require.NotEmpty(t, list) + for _, peering := range list { + require.NotNil(t, peering.Vpc1.Tenant.ID) + require.NotNil(t, peering.Vpc2.Tenant.ID) + tenantIDs := []string{peering.Vpc1.TenantID, peering.Vpc2.TenantID} + assert.Contains(t, tenantIDs, tn1.ID.String()) + assert.Contains(t, tenantIDs, tn2.ID.String()) + } + } + if len(tt.requirePeerTenantIDs) > 0 { + matchedPeerTenantIDs := make(map[uuid.UUID]bool, len(tt.requirePeerTenantIDs)) + for _, peering := range list { + require.NotNil(t, peering.Vpc1.Tenant.ID) + require.NotNil(t, peering.Vpc2.Tenant.ID) + for _, tenantID := range tt.requirePeerTenantIDs { + tenantIDStr := tenantID.String() + if peering.Vpc1.Tenant.ID == tenantIDStr || peering.Vpc2.Tenant.ID == tenantIDStr { + matchedPeerTenantIDs[tenantID] = true + } + } + } + for _, tenantID := range tt.requirePeerTenantIDs { + assert.True(t, matchedPeerTenantIDs[tenantID], "expected at least one peering involving tenant %s", tenantID) + } + } } }) } @@ -736,12 +875,23 @@ func TestGetVpcPeeringHandler_Handle(t *testing.T) { cfg := common.GetTestConfig() tests := []struct { - name string - reqOrgName string - peeringID string - user *cdbm.User - expectedStatus int - expectedID string + name string + reqOrgName string + peeringID string + user *cdbm.User + includeRelations []string + expectedStatus int + expectedID string + validateTenantInfo bool + expectVpc1TenantID string + expectVpc1TenantOrg string + expectVpc2TenantID string + expectVpc2TenantOrg string + expectNoVpcSummaries bool + validatePeeringTenant bool + expectPeeringTenantID string + expectPeeringTenantOrg string + expectNoPeeringTenant bool }{ { name: "error when user not found in request context", @@ -846,6 +996,78 @@ func TestGetVpcPeeringHandler_Handle(t *testing.T) { user: ipu2, expectedStatus: http.StatusForbidden, }, + { + name: "tenant info present when Vpc1 and Vpc2 included for same-tenant peering", + reqOrgName: tnOrg1, + peeringID: vp12.ID.String(), + user: tnu1, + includeRelations: []string{cdbm.Vpc1RelationName, cdbm.Vpc2RelationName}, + expectedStatus: http.StatusOK, + expectedID: vp12.ID.String(), + validateTenantInfo: true, + expectVpc1TenantID: tn1.ID.String(), + expectVpc1TenantOrg: tnOrg1, + expectVpc2TenantID: tn1.ID.String(), + expectVpc2TenantOrg: tnOrg1, + }, + { + name: "tenant info present when Vpc1 and Vpc2 included for cross-tenant peering", + reqOrgName: tnOrg1, + peeringID: vp14.ID.String(), + user: tnu1, + includeRelations: []string{cdbm.Vpc1RelationName, cdbm.Vpc2RelationName}, + expectedStatus: http.StatusOK, + expectedID: vp14.ID.String(), + validateTenantInfo: true, + expectVpc1TenantID: tn1.ID.String(), + expectVpc1TenantOrg: tnOrg1, + expectVpc2TenantID: tn2.ID.String(), + expectVpc2TenantOrg: tnOrg2, + }, + { + name: "vpc summaries absent when includeRelation omitted", + reqOrgName: tnOrg1, + peeringID: vp12.ID.String(), + user: tnu1, + expectedStatus: http.StatusOK, + expectedID: vp12.ID.String(), + expectNoVpcSummaries: true, + expectNoPeeringTenant: true, + }, + { + name: "peering tenant summary present when Tenant included for tenant-created peering", + reqOrgName: tnOrg1, + peeringID: vp12.ID.String(), + user: tnu1, + includeRelations: []string{cdbm.TenantRelationName}, + expectedStatus: http.StatusOK, + expectedID: vp12.ID.String(), + validatePeeringTenant: true, + expectPeeringTenantID: tn1.ID.String(), + expectPeeringTenantOrg: tnOrg1, + }, + { + name: "peering tenant summary present when Tenant included for provider tenant admin peering", + reqOrgName: ipOrg2, + peeringID: vp78.ID.String(), + user: ipu2, + includeRelations: []string{cdbm.TenantRelationName}, + expectedStatus: http.StatusOK, + expectedID: vp78.ID.String(), + validatePeeringTenant: true, + expectPeeringTenantID: tnProvider.ID.String(), + expectPeeringTenantOrg: ipOrg2, + }, + { + name: "peering tenant summary absent for provider-created peering even when Tenant included", + reqOrgName: tnOrg1, + peeringID: vp14.ID.String(), + user: tnu1, + includeRelations: []string{cdbm.TenantRelationName}, + expectedStatus: http.StatusOK, + expectedID: vp14.ID.String(), + expectNoPeeringTenant: true, + }, } for _, tt := range tests { @@ -858,7 +1080,18 @@ func TestGetVpcPeeringHandler_Handle(t *testing.T) { } e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) + url := "/" + if len(tt.includeRelations) > 0 { + for i, relation := range tt.includeRelations { + if i == 0 { + url += "?" + } else { + url += "&" + } + url += fmt.Sprintf("includeRelation=%s", relation) + } + } + req := httptest.NewRequest(http.MethodGet, url, nil) rec := httptest.NewRecorder() ec := e.NewContext(req, rec) @@ -878,6 +1111,30 @@ func TestGetVpcPeeringHandler_Handle(t *testing.T) { err := json.Unmarshal(rec.Body.Bytes(), &apiVP) require.NoError(t, err) assert.Equal(t, tt.expectedID, apiVP.ID) + if tt.validateTenantInfo { + require.NotNil(t, apiVP.Vpc1) + require.NotNil(t, apiVP.Vpc2) + require.NotNil(t, apiVP.Vpc1.Tenant) + require.NotNil(t, apiVP.Vpc2.Tenant) + assert.Equal(t, tt.expectVpc1TenantID, apiVP.Vpc1.Tenant.ID) + assert.Equal(t, tt.expectVpc1TenantOrg, apiVP.Vpc1.Tenant.Org) + assert.Equal(t, tt.expectVpc2TenantID, apiVP.Vpc2.Tenant.ID) + assert.Equal(t, tt.expectVpc2TenantOrg, apiVP.Vpc2.Tenant.Org) + } + if tt.expectNoVpcSummaries { + assert.Nil(t, apiVP.Vpc1) + assert.Nil(t, apiVP.Vpc2) + } + if tt.validatePeeringTenant { + require.NotNil(t, apiVP.TenantID) + require.NotNil(t, apiVP.Tenant) + assert.Equal(t, tt.expectPeeringTenantID, *apiVP.TenantID) + assert.Equal(t, tt.expectPeeringTenantOrg, apiVP.Tenant.Org) + } + if tt.expectNoPeeringTenant { + assert.Nil(t, apiVP.Tenant) + assert.Nil(t, apiVP.TenantID) + } } }) } diff --git a/rest-api/api/pkg/api/model/vpcpeering.go b/rest-api/api/pkg/api/model/vpcpeering.go index d5b6e52b59..1e31a6610f 100644 --- a/rest-api/api/pkg/api/model/vpcpeering.go +++ b/rest-api/api/pkg/api/model/vpcpeering.go @@ -9,7 +9,9 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" validationis "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/google/uuid" + "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/model/util" cdbm "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/model" cwssaws "github.com/NVIDIA/infra-controller/rest-api/workflow-schema/schema/site-agent/workflows/v1" ) @@ -71,22 +73,26 @@ func (vpcr *APIVpcPeeringCreateRequest) ToProto(vp *cdbm.VpcPeering) *cwssaws.Vp } } -// APIVpcPeering represents a VPC peering connection +// APIVpcPeering represents a VPC Peering connection type APIVpcPeering struct { - // ID is the unique UUID v4 identifier of the VPC peering in NICo Cloud + // ID is the unique UUID v4 identifier of the VPC Peering ID string `json:"id"` - // Vpc1ID is the ID of the first VPC in the peering + // Vpc1ID is the ID of the first VPC in the Peering Vpc1ID string `json:"vpc1Id"` - // Vpc1 is the summary of the first VPC in the peering - Vpc1 *APIVpcSummary `json:"vpc1,omitempty"` + // Vpc1 is the summary of the first VPC in the Peering + Vpc1 *APIVpcPeeringVpcSummary `json:"vpc1,omitempty"` // Vpc2ID is the ID of the second VPC in the peering Vpc2ID string `json:"vpc2Id"` - // Vpc2 is the summary of the second VPC in the peering - Vpc2 *APIVpcSummary `json:"vpc2,omitempty"` - // SiteID is the ID of the Site where the peering exists + // Vpc2 is the summary of the second VPC in the Peering + Vpc2 *APIVpcPeeringVpcSummary `json:"vpc2,omitempty"` + // SiteID is the ID of the Site where the Peering resides SiteID string `json:"siteId"` - // Site is the summary of the site + // Site is the summary of the Site Site *APISiteSummary `json:"site,omitempty"` + // TenantID is the ID of the Tenant that created the VPC peering + TenantID *string `json:"tenantId,omitempty"` + // Tenant is the summary of the tenant that created the VPC peering + Tenant *APITenantSummary `json:"tenant,omitempty"` // IsMultiTenant indicates if this is a multi-tenant peering IsMultiTenant bool `json:"isMultiTenant"` // Status is the status of the VPC peering @@ -97,8 +103,64 @@ type APIVpcPeering struct { Updated time.Time `json:"updated"` } +// APIVpcPeeringVpcSummary is summarizes a VPC in context of a VPC Peering +type APIVpcPeeringVpcSummary struct { + // ID is the unique UUID v4 identifier of the VPC + ID string `json:"id"` + // Name of the Vpc + Name string `json:"name"` + // TenantID is the ID of the Tenant that owns the VPC + TenantID string `json:"tenantId"` + // Tenant describes details of the Tenant that owns the VPC + Tenant *APIVpcPeeringTenantSummary `json:"tenant"` + // Network virtualization type describe the VPC's virtualization type + NetworkVirtualizationType *string `json:"networkVirtualizationType"` + // Status is the status of the VPC + Status string `json:"status"` +} + +// APIVpcPeeringTenantSummary is summarizes a Tenant in context of a VPC Peering +type APIVpcPeeringTenantSummary struct { + // ID of the Tenant + ID string `json:"id"` + // Org contains the org ID of the Tenant + Org string `json:"org"` + // OrgDisplayName contains the display name of Tenant's org + OrgDisplayName *string `json:"orgDisplayName"` +} + +// NewAPIVpcPeeringTenantSummary creates a new APIVpcPeeringTenantSummary from a database Tenant model +func NewAPIVpcPeeringTenantSummary(dbTenant *cdbm.Tenant) *APIVpcPeeringTenantSummary { + return &APIVpcPeeringTenantSummary{ + ID: dbTenant.ID.String(), + Org: dbTenant.Org, + OrgDisplayName: dbTenant.OrgDisplayName, + } +} + +// NewAPIVpcPeeringSummary creates a new APIVpcPeeringSummary from a database VPC peering model +func NewAPIVpcPeeringVpcSummary(dbVpc *cdbm.Vpc, dbTenant *cdbm.Tenant) *APIVpcPeeringVpcSummary { + if dbVpc == nil { + return nil + } + + apiVpcPeeringVpcSummary := APIVpcPeeringVpcSummary{ + ID: dbVpc.ID.String(), + Name: dbVpc.Name, + TenantID: dbVpc.TenantID.String(), + NetworkVirtualizationType: dbVpc.NetworkVirtualizationType, + Status: dbVpc.Status, + } + + if dbTenant != nil { + apiVpcPeeringVpcSummary.Tenant = NewAPIVpcPeeringTenantSummary(dbTenant) + } + + return &apiVpcPeeringVpcSummary +} + // NewAPIVpcPeering creates a new APIVpcPeering from a database VPC peering model -func NewAPIVpcPeering(dbVpcPeering cdbm.VpcPeering) APIVpcPeering { +func NewAPIVpcPeering(dbVpcPeering cdbm.VpcPeering, dbMappedPeeringTenants map[uuid.UUID]*cdbm.Tenant) APIVpcPeering { apiVpcPeering := APIVpcPeering{ ID: dbVpcPeering.ID.String(), Vpc1ID: dbVpcPeering.Vpc1ID.String(), @@ -112,42 +174,24 @@ func NewAPIVpcPeering(dbVpcPeering cdbm.VpcPeering) APIVpcPeering { // Expand relations if available. if dbVpcPeering.Vpc1 != nil { - apiVpcPeering.Vpc1 = NewAPIVpcSummary(dbVpcPeering.Vpc1) + apiVpcPeering.Vpc1 = NewAPIVpcPeeringVpcSummary(dbVpcPeering.Vpc1, nil) + peerTenant1, _ := dbMappedPeeringTenants[dbVpcPeering.Vpc1.TenantID] + apiVpcPeering.Vpc1 = NewAPIVpcPeeringVpcSummary(dbVpcPeering.Vpc1, peerTenant1) } + if dbVpcPeering.Vpc2 != nil { - apiVpcPeering.Vpc2 = NewAPIVpcSummary(dbVpcPeering.Vpc2) + apiVpcPeering.Vpc2 = NewAPIVpcPeeringVpcSummary(dbVpcPeering.Vpc2, nil) + peerTenant2, _ := dbMappedPeeringTenants[dbVpcPeering.Vpc2.TenantID] + apiVpcPeering.Vpc2 = NewAPIVpcPeeringVpcSummary(dbVpcPeering.Vpc2, peerTenant2) } + if dbVpcPeering.Site != nil { apiVpcPeering.Site = NewAPISiteSummary(dbVpcPeering.Site) } - return apiVpcPeering -} - -// APIVpcPeeringSummary represents a summary of a VPC peering connection -type APIVpcPeeringSummary struct { - // ID is the unique UUID v4 identifier of the VPC peering in NICo Cloud - ID string `json:"id"` - // Vpc1ID is the ID of the first VPC in the peering - Vpc1ID string `json:"vpc1Id"` - // Vpc2ID is the ID of the second VPC in the peering - Vpc2ID string `json:"vpc2Id"` - // IsMultiTenant indicates if this is a multi-tenant peering - IsMultiTenant bool `json:"isMultiTenant"` - // Status is the status of the VPC peering - Status string `json:"status"` -} - -// NewAPIVpcPeeringSummary creates a new APIVpcPeeringSummary from a database VPC peering model -func NewAPIVpcPeeringSummary(dbVpcPeering *cdbm.VpcPeering) *APIVpcPeeringSummary { - if dbVpcPeering == nil { - return nil - } - return &APIVpcPeeringSummary{ - ID: dbVpcPeering.ID.String(), - Vpc1ID: dbVpcPeering.Vpc1ID.String(), - Vpc2ID: dbVpcPeering.Vpc2ID.String(), - Status: dbVpcPeering.Status, - IsMultiTenant: dbVpcPeering.IsMultiTenant, + if dbVpcPeering.Tenant != nil { + apiVpcPeering.TenantID = util.GetUUIDPtrToStrPtr(dbVpcPeering.TenantID) + apiVpcPeering.Tenant = NewAPITenantSummary(dbVpcPeering.Tenant) } + return apiVpcPeering } diff --git a/rest-api/api/pkg/api/model/vpcpeering_test.go b/rest-api/api/pkg/api/model/vpcpeering_test.go index b8b4368ac8..cfef349211 100644 --- a/rest-api/api/pkg/api/model/vpcpeering_test.go +++ b/rest-api/api/pkg/api/model/vpcpeering_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/NVIDIA/infra-controller/rest-api/common/pkg/util" cdbm "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/model" ) @@ -132,6 +133,8 @@ func TestAPIVpcPeeringCreateRequest_ToProto(t *testing.T) { func TestNewAPIVpcPeering(t *testing.T) { now := time.Now() + vpc1TenantID := uuid.New() + vpc2TenantID := uuid.New() dbVpcPeering := cdbm.VpcPeering{ ID: uuid.New(), Vpc1ID: uuid.New(), @@ -141,9 +144,39 @@ func TestNewAPIVpcPeering(t *testing.T) { Status: cdbm.VpcPeeringStatusReady, Created: now, Updated: now, + Vpc1: &cdbm.Vpc{ + ID: uuid.New(), + TenantID: vpc1TenantID, + Name: "vpc-1", + Status: cdbm.VpcStatusReady, + }, + Vpc2: &cdbm.Vpc{ + ID: uuid.New(), + TenantID: vpc2TenantID, + Name: "vpc-2", + Status: cdbm.VpcStatusReady, + }, + TenantID: &vpc1TenantID, + Tenant: &cdbm.Tenant{ + ID: vpc1TenantID, + Org: "test-org", + OrgDisplayName: util.GetPtr("Org Display name"), + }, } - api := NewAPIVpcPeering(dbVpcPeering) + dbMappedPeeringTenantsMap := map[uuid.UUID]*cdbm.Tenant{ + vpc1TenantID: { + ID: vpc1TenantID, + Org: "test-org", + OrgDisplayName: util.GetPtr("Org Display name"), + }, + vpc2TenantID: { + ID: vpc2TenantID, + Org: "test-org", + OrgDisplayName: util.GetPtr("Org Display name"), + }, + } + api := NewAPIVpcPeering(dbVpcPeering, dbMappedPeeringTenantsMap) assert.Equal(t, dbVpcPeering.ID.String(), api.ID) assert.Equal(t, dbVpcPeering.Vpc1ID.String(), api.Vpc1ID) @@ -153,34 +186,51 @@ func TestNewAPIVpcPeering(t *testing.T) { assert.Equal(t, dbVpcPeering.Status, api.Status) assert.Equal(t, dbVpcPeering.Created, api.Created) assert.Equal(t, dbVpcPeering.Updated, api.Updated) - + require.NotNil(t, api.Vpc1) + require.NotNil(t, api.Vpc1.Tenant) + assert.Equal(t, vpc1TenantID.String(), api.Vpc1.Tenant.ID) + require.NotNil(t, api.Vpc2) + require.NotNil(t, api.Vpc2.Tenant) + assert.Equal(t, vpc2TenantID.String(), api.Vpc2.Tenant.ID) + require.NotNil(t, api.Tenant) + require.NotNil(t, api.TenantID) + assert.Equal(t, dbVpcPeering.TenantID.String(), *api.TenantID) + assert.Equal(t, dbVpcPeering.Tenant.Org, api.Tenant.Org) } -func TestNewAPIVpcPeeringSummary(t *testing.T) { - now := time.Now() - dbVpcPeering := &cdbm.VpcPeering{ - ID: uuid.New(), - Vpc1ID: uuid.New(), - Vpc2ID: uuid.New(), - SiteID: uuid.New(), - IsMultiTenant: false, - Status: cdbm.VpcPeeringStatusPending, - Created: now, - Updated: now, +func TestNewAPIVpcPeeringVpcSummary(t *testing.T) { + vpc1TenantID := uuid.New() + dbVpc := &cdbm.Vpc{ + ID: uuid.New(), + TenantID: vpc1TenantID, + Name: "vpc-1", + Status: cdbm.VpcStatusReady, + } + dbTenant := &cdbm.Tenant{ + ID: vpc1TenantID, + Org: "test-org", + OrgDisplayName: util.GetPtr("Org Display name"), } t.Run("ok when db model is provided", func(t *testing.T) { - summary := NewAPIVpcPeeringSummary(dbVpcPeering) + summary := NewAPIVpcPeeringVpcSummary(dbVpc, dbTenant) + assert.NotNil(t, summary) + assert.Equal(t, dbVpc.ID.String(), summary.ID) + assert.Equal(t, dbVpc.Name, summary.Name) + assert.Equal(t, dbVpc.TenantID.String(), summary.TenantID) + assert.Equal(t, dbVpc.Status, summary.Status) + require.NotNil(t, summary.Tenant) + assert.Equal(t, vpc1TenantID.String(), summary.Tenant.ID) + }) + + t.Run("ok when tenant is nil", func(t *testing.T) { + summary := NewAPIVpcPeeringVpcSummary(dbVpc, nil) assert.NotNil(t, summary) - assert.Equal(t, dbVpcPeering.ID.String(), summary.ID) - assert.Equal(t, dbVpcPeering.Vpc1ID.String(), summary.Vpc1ID) - assert.Equal(t, dbVpcPeering.Vpc2ID.String(), summary.Vpc2ID) - assert.Equal(t, dbVpcPeering.Status, summary.Status) - assert.Equal(t, dbVpcPeering.IsMultiTenant, summary.IsMultiTenant) + assert.Nil(t, summary.Tenant) }) t.Run("returns nil when db model is nil", func(t *testing.T) { - summary := NewAPIVpcPeeringSummary(nil) + summary := NewAPIVpcPeeringVpcSummary(nil, nil) assert.Nil(t, summary) }) } diff --git a/rest-api/db/pkg/db/model/tenant.go b/rest-api/db/pkg/db/model/tenant.go index 56c107fa00..5e356595ee 100644 --- a/rest-api/db/pkg/db/model/tenant.go +++ b/rest-api/db/pkg/db/model/tenant.go @@ -9,6 +9,7 @@ import ( "time" "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db" + "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/paginator" stracer "github.com/NVIDIA/infra-controller/rest-api/db/pkg/tracer" cwssaws "github.com/NVIDIA/infra-controller/rest-api/workflow-schema/schema/site-agent/workflows/v1" "github.com/google/uuid" @@ -19,8 +20,21 @@ import ( const ( // TenantRelationName is the relation name for the Tenant model TenantRelationName = "Tenant" + + // TenantOrderByDefault default field to be used for ordering when none specified + TenantOrderByDefault = "created" ) +// TenantOrderByFields is a list of fields that can be used for ordering Tenants +var TenantOrderByFields = []string{"created", "name", "org"} + +// TenantFilterInput input parameters for GetAll method +type TenantFilterInput struct { + TenantIDs []uuid.UUID + Orgs []string + OrgDisplayNames []string +} + // TenantCreateInput input parameters for Create method type TenantCreateInput struct { Name string @@ -113,7 +127,7 @@ type TenantDAO interface { // GetByID(ctx context.Context, tx *db.Tx, id uuid.UUID, includeRelations []string) (*Tenant, error) // - GetAllByOrg(ctx context.Context, tx *db.Tx, org string, includeRelations []string) ([]Tenant, error) + GetAll(ctx context.Context, tx *db.Tx, filter TenantFilterInput, page paginator.PageInput, includeRelations []string) ([]Tenant, int, error) // Create(ctx context.Context, tx *db.Tx, input TenantCreateInput) (*Tenant, error) // @@ -157,30 +171,59 @@ func (tsd TenantSQLDAO) GetByID(ctx context.Context, tx *db.Tx, id uuid.UUID, in return tn, nil } -// GetAllByOrg returns all Tenants for an Org -func (tsd TenantSQLDAO) GetAllByOrg(ctx context.Context, tx *db.Tx, org string, includeRelations []string) ([]Tenant, error) { - // Create a child span and set the attributes for current request - ctx, tnDAOSpan := tsd.tracerSpan.CreateChildInCurrentContext(ctx, "TenantDAO.GetAllByOrg") +func (tsd TenantSQLDAO) setQueryWithFilter(filter TenantFilterInput, query *bun.SelectQuery, tnDAOSpan *stracer.CurrentContextSpan) *bun.SelectQuery { + if filter.Orgs != nil { + query = query.Where("tn.org IN (?)", bun.In(filter.Orgs)) + tsd.tracerSpan.SetAttribute(tnDAOSpan, "orgs", filter.Orgs) + } + + if filter.OrgDisplayNames != nil { + query = query.Where("tn.org_display_name IN (?)", bun.In(filter.OrgDisplayNames)) + tsd.tracerSpan.SetAttribute(tnDAOSpan, "org_display_names", filter.OrgDisplayNames) + } + + if filter.TenantIDs != nil { + query = query.Where("tn.id IN (?)", bun.In(filter.TenantIDs)) + tsd.tracerSpan.SetAttribute(tnDAOSpan, "tenant_ids", filter.TenantIDs) + } + + return query +} + +// GetAll returns all Tenants matching the given filter. +func (tsd TenantSQLDAO) GetAll(ctx context.Context, tx *db.Tx, filter TenantFilterInput, page paginator.PageInput, includeRelations []string) ([]Tenant, int, error) { + ctx, tnDAOSpan := tsd.tracerSpan.CreateChildInCurrentContext(ctx, "TenantDAO.GetAll") if tnDAOSpan != nil { defer tnDAOSpan.End() - - tsd.tracerSpan.SetAttribute(tnDAOSpan, "org", org) } tns := []Tenant{} + if filter.TenantIDs != nil && len(filter.TenantIDs) == 0 { + return tns, 0, nil + } - query := db.GetIDB(tx, tsd.dbSession).NewSelect().Model(&tns).Where("tn.org = ?", org) + query := db.GetIDB(tx, tsd.dbSession).NewSelect().Model(&tns) + query = tsd.setQueryWithFilter(filter, query, tnDAOSpan) for _, relation := range includeRelations { query = query.Relation(relation) } - err := query.Scan(ctx) + if page.OrderBy == nil { + page.OrderBy = paginator.NewDefaultOrderBy(TenantOrderByDefault) + } + + pag, err := paginator.NewPaginator(ctx, query, page.Offset, page.Limit, page.OrderBy, TenantOrderByFields) if err != nil { - return nil, err + return nil, 0, err + } + + err = pag.Query.Limit(pag.Limit).Offset(pag.Offset).Scan(ctx) + if err != nil { + return nil, 0, err } - return tns, nil + return tns, pag.Total, nil } // Create creates a new Tenant from the given input diff --git a/rest-api/db/pkg/db/model/tenant_test.go b/rest-api/db/pkg/db/model/tenant_test.go index 6eba23e502..9b634af5a0 100644 --- a/rest-api/db/pkg/db/model/tenant_test.go +++ b/rest-api/db/pkg/db/model/tenant_test.go @@ -9,6 +9,7 @@ import ( cutil "github.com/NVIDIA/infra-controller/rest-api/common/pkg/util" "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db" + "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/paginator" stracer "github.com/NVIDIA/infra-controller/rest-api/db/pkg/tracer" "github.com/NVIDIA/infra-controller/rest-api/db/pkg/util" "github.com/google/uuid" @@ -206,7 +207,8 @@ func TestTenantSQLDAO_GetAllByOrg(t *testing.T) { ipsd := TenantSQLDAO{ dbSession: tt.fields.dbSession, } - got, err := ipsd.GetAllByOrg(tt.args.ctx, nil, tt.args.org, nil) + + got, _, err := ipsd.GetAll(ctx, nil, TenantFilterInput{Orgs: []string{org}}, paginator.PageInput{Limit: cutil.GetPtr(paginator.TotalLimit)}, nil) assert.NoError(t, err) for i, tn := range tt.want { @@ -458,6 +460,149 @@ func TestTenantSQLDAO_Update(t *testing.T) { } } +func TestTenantSQLDAO_GetAll(t *testing.T) { + ctx := context.Background() + dbSession := util.GetTestDBSession(t, false) + defer dbSession.Close() + + err := dbSession.DB.ResetModel(ctx, (*Tenant)(nil)) + require.NoError(t, err) + + createdBy := uuid.New() + tenantAlphaOne := Tenant{ + ID: uuid.New(), + Name: "tenant-alpha-one", + Org: "org-alpha", + OrgDisplayName: cutil.GetPtr("Alpha Display"), + CreatedBy: createdBy, + } + tenantAlphaTwo := Tenant{ + ID: uuid.New(), + Name: "tenant-alpha-two", + Org: "org-alpha", + OrgDisplayName: cutil.GetPtr("Beta Display"), + CreatedBy: createdBy, + } + tenantBetaOne := Tenant{ + ID: uuid.New(), + Name: "tenant-beta-one", + Org: "org-beta", + OrgDisplayName: cutil.GetPtr("Alpha Display"), + CreatedBy: createdBy, + } + tenantGamma := Tenant{ + ID: uuid.New(), + Name: "tenant-gamma", + Org: "org-gamma", + CreatedBy: createdBy, + } + + tenants := []Tenant{tenantAlphaOne, tenantAlphaTwo, tenantBetaOne, tenantGamma} + _, err = dbSession.DB.NewInsert().Model(&tenants).Exec(ctx) + require.NoError(t, err) + + tsd := NewTenantDAO(dbSession) + page := paginator.PageInput{Limit: cutil.GetPtr(paginator.TotalLimit)} + + tests := []struct { + name string + filter TenantFilterInput + expectedCount int + expectedIDs []uuid.UUID + }{ + { + name: "no filters returns all tenants", + filter: TenantFilterInput{}, + expectedCount: 4, + expectedIDs: []uuid.UUID{ + tenantAlphaOne.ID, + tenantAlphaTwo.ID, + tenantBetaOne.ID, + tenantGamma.ID, + }, + }, + { + name: "Orgs filter matches a single org", + filter: TenantFilterInput{Orgs: []string{"org-alpha"}}, + expectedCount: 2, + expectedIDs: []uuid.UUID{tenantAlphaOne.ID, tenantAlphaTwo.ID}, + }, + { + name: "Orgs filter matches multiple orgs", + filter: TenantFilterInput{Orgs: []string{"org-alpha", "org-beta"}}, + expectedCount: 3, + expectedIDs: []uuid.UUID{ + tenantAlphaOne.ID, + tenantAlphaTwo.ID, + tenantBetaOne.ID, + }, + }, + { + name: "Orgs filter with no matches returns empty", + filter: TenantFilterInput{Orgs: []string{"org-missing"}}, + expectedCount: 0, + }, + { + name: "OrgDisplayNames filter matches a single display name", + filter: TenantFilterInput{OrgDisplayNames: []string{"Alpha Display"}}, + expectedCount: 2, + expectedIDs: []uuid.UUID{tenantAlphaOne.ID, tenantBetaOne.ID}, + }, + { + name: "OrgDisplayNames filter matches multiple display names", + filter: TenantFilterInput{OrgDisplayNames: []string{"Alpha Display", "Beta Display"}}, + expectedCount: 3, + expectedIDs: []uuid.UUID{ + tenantAlphaOne.ID, + tenantAlphaTwo.ID, + tenantBetaOne.ID, + }, + }, + { + name: "OrgDisplayNames filter with no matches returns empty", + filter: TenantFilterInput{OrgDisplayNames: []string{"Missing Display"}}, + expectedCount: 0, + }, + { + name: "Orgs and OrgDisplayNames filters are combined", + filter: TenantFilterInput{ + Orgs: []string{"org-alpha"}, + OrgDisplayNames: []string{"Alpha Display"}, + }, + expectedCount: 1, + expectedIDs: []uuid.UUID{tenantAlphaOne.ID}, + }, + { + name: "TenantIDs filter matches requested tenants", + filter: TenantFilterInput{TenantIDs: []uuid.UUID{tenantAlphaTwo.ID, tenantGamma.ID}}, + expectedCount: 2, + expectedIDs: []uuid.UUID{tenantAlphaTwo.ID, tenantGamma.ID}, + }, + { + name: "empty TenantIDs filter returns empty", + filter: TenantFilterInput{TenantIDs: []uuid.UUID{}}, + expectedCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, total, err := tsd.GetAll(ctx, nil, tt.filter, page, nil) + require.NoError(t, err) + assert.Equal(t, tt.expectedCount, total) + assert.Len(t, got, tt.expectedCount) + + if tt.expectedIDs != nil { + gotIDs := make([]uuid.UUID, len(got)) + for i, tenant := range got { + gotIDs[i] = tenant.ID + } + assert.ElementsMatch(t, tt.expectedIDs, gotIDs) + } + }) + } +} + func TestTenantSQLDAO_Delete(t *testing.T) { type fields struct { dbSession *db.Session diff --git a/rest-api/db/pkg/db/model/vpcpeering.go b/rest-api/db/pkg/db/model/vpcpeering.go index 24f184905b..8f161a0781 100644 --- a/rest-api/db/pkg/db/model/vpcpeering.go +++ b/rest-api/db/pkg/db/model/vpcpeering.go @@ -172,7 +172,11 @@ type VpcPeeringFilterInput struct { IsMultiTenant *bool InfrastructureProviderIDs []uuid.UUID TenantIDs []uuid.UUID - Statuses []string + // PeerTenantIDs filters peerings where vpc1 OR vpc2 belongs to any of the specified + // tenants. This is a user-requested filter and is distinct from TenantIDs, which + // scopes results to the caller's authorization context. + PeerTenantIDs []uuid.UUID + Statuses []string } func (vp *VpcPeering) BeforeCreateTable(ctx context.Context, query *bun.CreateTableQuery) error { @@ -350,6 +354,15 @@ func (vpsd VpcPeeringSQLDAO) setQueryWithFilter(filter VpcPeeringFilterInput, qu vpsd.tracerSpan.SetAttribute(vpDAOSpan, "tenant_ids", filter.TenantIDs) } + if len(filter.PeerTenantIDs) > 0 { + query = query.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { + return q. + WhereOr("vp.vpc1_id IN (SELECT id FROM vpc WHERE tenant_id IN (?))", bun.In(filter.PeerTenantIDs)). + WhereOr("vp.vpc2_id IN (SELECT id FROM vpc WHERE tenant_id IN (?))", bun.In(filter.PeerTenantIDs)) + }) + vpsd.tracerSpan.SetAttribute(vpDAOSpan, "peer_tenant_ids", filter.PeerTenantIDs) + } + return query, nil } diff --git a/rest-api/db/pkg/db/model/vpcpeering_test.go b/rest-api/db/pkg/db/model/vpcpeering_test.go index 0d75824116..5e97842dc4 100644 --- a/rest-api/db/pkg/db/model/vpcpeering_test.go +++ b/rest-api/db/pkg/db/model/vpcpeering_test.go @@ -331,6 +331,7 @@ func TestVpcPeeringSQLDAO_GetAll(t *testing.T) { isMultiTenant *bool infrastructureProviderIDs []uuid.UUID tenantIDs []uuid.UUID + peerTenantIDs []uuid.UUID statuses []string expectError bool @@ -401,6 +402,21 @@ func TestVpcPeeringSQLDAO_GetAll(t *testing.T) { expectCount: 1, verifyChildSpanner: true, }, + { + desc: "GetAll with filters on PeerTenantIDs", + peerTenantIDs: []uuid.UUID{tenant2.ID}, + expectError: false, + expectCount: 1, + verifyChildSpanner: true, + }, + { + desc: "GetAll with filters on TenantIDs and PeerTenantIDs", + tenantIDs: []uuid.UUID{tenant.ID}, + peerTenantIDs: []uuid.UUID{tenant2.ID}, + expectError: false, + expectCount: 1, + verifyChildSpanner: true, + }, { desc: "GetAll with filters on pending statuses", ids: nil, @@ -462,6 +478,7 @@ func TestVpcPeeringSQLDAO_GetAll(t *testing.T) { IsMultiTenant: tc.isMultiTenant, InfrastructureProviderIDs: tc.infrastructureProviderIDs, TenantIDs: tc.tenantIDs, + PeerTenantIDs: tc.peerTenantIDs, Statuses: tc.statuses, }, paginator.PageInput{Offset: tc.paramOffset, Limit: tc.paramLimit, OrderBy: tc.paramOrderBy}, tc.includeRelations) diff --git a/rest-api/db/pkg/migrations/migrations_test.go b/rest-api/db/pkg/migrations/migrations_test.go index dbf093ae8f..6de950e1a0 100644 --- a/rest-api/db/pkg/migrations/migrations_test.go +++ b/rest-api/db/pkg/migrations/migrations_test.go @@ -904,7 +904,7 @@ func Test_tenantConfigUpMigration(t *testing.T) { // GetAll operating systems and verify tnDAO := model.NewTenantDAO(dbSession) - tns, err := tnDAO.GetAllByOrg(context.Background(), nil, tenant1.Org, nil) + tns, _, err := tnDAO.GetAll(ctx, nil, model.TenantFilterInput{Orgs: []string{tenant1.Org}}, paginator.PageInput{Limit: cutil.GetPtr(paginator.TotalLimit)}, nil) assert.Nil(t, err) assert.Equal(t, 1, len(tns)) assert.Equal(t, tenant1.ID, tns[0].ID) diff --git a/rest-api/docs/index.html b/rest-api/docs/index.html index 4d28726847..495f362add 100644 --- a/rest-api/docs/index.html +++ b/rest-api/docs/index.html @@ -2906,7 +2906,13 @@

Typical API Call Flow for Tenant

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

Ordering for pagination query

isMultiTenant
boolean

Optional filter by peering tenancy type (single-tenant or multi-tenant).

-
includeRelation
string
Enum: "Vpc1" "Vpc2" "Site"
status
string
Enum: "Pending" "Configuring" "Requested" "Ready" "Deleting" "Error"

Optional filter by peering status. Repeat the parameter to match multiple statuses.

+
vpcId
string <uuid>

Optional filter by VPC ID involved in the peering as either vpc1 or vpc2. Repeat the parameter to match multiple VPCs.

+
peerTenantId
string <uuid>

Optional filter by tenant ID of a VPC involved in the peering. Repeat the parameter to match multiple tenants.

+
includeRelation
string
Enum: "Vpc1" "Vpc2" "Site" "Tenant"

Related entity to expand

Responses

vpc1Id
string <uuid>

ID of the first VPC in the peering

-
vpc2Id
string <uuid>
object (VpcPeeringVpcSummary)

Summary of the first VPC in the peering.

+
id
string <uuid>

ID of the VPC

+
name
string [ 2 .. 256 ] characters

Name of the VPC

+
tenantId
string <uuid>

ID of the tenant

+
object (VpcPeeringTenantSummary)

Summary of the tenant

+
networkVirtualizationType
string or null
Enum: "ETHERNET_VIRTUALIZER" "FNN" "FLAT"

Network virtualization type of the VPC

+
status
string (VpcStatus)
Enum: "Pending" "Provisioning" "Ready" "Configuring" "Deleting" "Error"

Status of the VPC

+
vpc2Id
string <uuid>

ID of the second VPC in the peering

-
siteId
string <uuid>
object (VpcPeeringVpcSummary)

Summary of the second VPC in the peering.

+
id
string <uuid>

ID of the VPC

+
name
string [ 2 .. 256 ] characters

Name of the VPC

+
tenantId
string <uuid>

ID of the tenant

+
object (VpcPeeringTenantSummary)

Summary of the tenant

+
networkVirtualizationType
string or null
Enum: "ETHERNET_VIRTUALIZER" "FNN" "FLAT"

Network virtualization type of the VPC

+
status
string (VpcStatus)
Enum: "Pending" "Provisioning" "Ready" "Configuring" "Deleting" "Error"

Status of the VPC

+
siteId
string <uuid>

ID of the Site where the peering exists

-
isMultiTenant
boolean
object (SiteSummary)

Summary of the Site where the peering exists.

+
id
string <uuid>

Unique UUID v4 identifier for the Site

+
name
string [ 2 .. 256 ] characters

Name of the Site

+
infrastructureProviderId
string <uuid>

ID of the Infrastructure Provider that owns the Site

+
isSerialConsoleEnabled
boolean

Indicates if Serial Console is enabled for the Site by the Provider

+
isOnline
boolean

Indicates if the Site is currently reachable from Cloud

+
object (SiteCapabilities)

Site capabilities used for feature availability and configuration

+
status
string (SiteStatus)
Enum: "Pending" "Registered" "Error"

Status of the Site

+
tenantId
string <uuid>

ID of the tenant that created the VPC peering.

+
object (TenantSummary)

Summary of the tenant that created the VPC peering.

+
org
string

Name of the org this tenant belongs to

+
orgDisplayName
string or null

Display name of the org the Tenant belongs to

+
object (TenantCapabilities)

Features that are enabled/disabled for Tenant

+
isMultiTenant
boolean

Indicates if this is a multi-tenant peering (VPCs from different tenants)

status
string (VpcPeeringStatus)
Enum: "Pending" "Configuring" "Requested" "Ready" "Deleting" "Error"

Status of the VPC peering

@@ -2934,7 +2994,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create VPC peering

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-peering

Response samples

Content type
application/json
[
  • {
    }
]

Create VPC peering

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Unique identifier of the VPC peering

vpc1Id
string <uuid>

ID of the first VPC in the peering

-
vpc2Id
string <uuid>
object (VpcPeeringVpcSummary)

Summary of the first VPC in the peering.

+
id
string <uuid>

ID of the VPC

+
name
string [ 2 .. 256 ] characters

Name of the VPC

+
tenantId
string <uuid>

ID of the tenant

+
object (VpcPeeringTenantSummary)

Summary of the tenant

+
id
string <uuid>

ID of the Tenant

+
org
string

Name of the org this tenant belongs to

+
orgDisplayName
string or null

Display name of the org the Tenant belongs to

+
networkVirtualizationType
string or null
Enum: "ETHERNET_VIRTUALIZER" "FNN" "FLAT"

Network virtualization type of the VPC

+
status
string (VpcStatus)
Enum: "Pending" "Provisioning" "Ready" "Configuring" "Deleting" "Error"

Status of the VPC

+
vpc2Id
string <uuid>

ID of the second VPC in the peering

-
siteId
string <uuid>
object (VpcPeeringVpcSummary)

Summary of the second VPC in the peering.

+
id
string <uuid>

ID of the VPC

+
name
string [ 2 .. 256 ] characters

Name of the VPC

+
tenantId
string <uuid>

ID of the tenant

+
object (VpcPeeringTenantSummary)

Summary of the tenant

+
id
string <uuid>

ID of the Tenant

+
org
string

Name of the org this tenant belongs to

+
orgDisplayName
string or null

Display name of the org the Tenant belongs to

+
networkVirtualizationType
string or null
Enum: "ETHERNET_VIRTUALIZER" "FNN" "FLAT"

Network virtualization type of the VPC

+
status
string (VpcStatus)
Enum: "Pending" "Provisioning" "Ready" "Configuring" "Deleting" "Error"

Status of the VPC

+
siteId
string <uuid>

ID of the Site where the peering exists

-
isMultiTenant
boolean
object (SiteSummary)

Summary of the Site where the peering exists.

+
id
string <uuid>

Unique UUID v4 identifier for the Site

+
name
string [ 2 .. 256 ] characters

Name of the Site

+
infrastructureProviderId
string <uuid>

ID of the Infrastructure Provider that owns the Site

+
isSerialConsoleEnabled
boolean

Indicates if Serial Console is enabled for the Site by the Provider

+
isOnline
boolean

Indicates if the Site is currently reachable from Cloud

+
object (SiteCapabilities)

Site capabilities used for feature availability and configuration

+
nativeNetworking
boolean

Whether the Site supports native networking

+
networkSecurityGroup
boolean

Whether the Site supports Network Security Groups

+
nvLinkPartition
boolean

Whether the Site supports NVLink partitioning

+
flow
boolean

Whether the Site supports Flow-based operations

+
imageBasedOperatingSystem
boolean

Whether the Site supports image-based operating system provisioning

+
status
string (SiteStatus)
Enum: "Pending" "Registered" "Error"

Status of the Site

+
tenantId
string <uuid>

ID of the tenant that created the VPC peering.

+
object (TenantSummary)

Summary of the tenant that created the VPC peering.

+
org
string

Name of the org this tenant belongs to

+
orgDisplayName
string or null

Display name of the org the Tenant belongs to

+
object (TenantCapabilities)

Features that are enabled/disabled for Tenant

+
targetedInstanceCreation
boolean <uuid>

Indicates whether Tenant can create Instances by specifying Machine ID

+
isMultiTenant
boolean

Indicates if this is a multi-tenant peering (VPCs from different tenants)

status
string (VpcPeeringStatus)
Enum: "Pending" "Configuring" "Requested" "Ready" "Deleting" "Error"

Status of the VPC peering

@@ -2976,7 +3114,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "vpc1Id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc2Id": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc1Id": "bafbaf4c-c730-48ae-8f5d-d7d251f3315a",
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "isMultiTenant": true,
  • "status": "Pending",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve a VPC peering

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-peering

Request samples

Content type
application/json
{
  • "vpc1Id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc2Id": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc1Id": "bafbaf4c-c730-48ae-8f5d-d7d251f3315a",
  • "vpc1": {
    },
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "vpc2": {
    },
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "site": {
    },
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "tenant": {
    },
  • "isMultiTenant": true,
  • "status": "Pending",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve a VPC peering

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Name of the Org

id
required
string <uuid>

VPC Peering ID

-
query Parameters
includeRelation
string
Enum: "Vpc1" "Vpc2" "Site"
query Parameters
includeRelation
string
Enum: "Vpc1" "Vpc2" "Site" "Tenant"

Related entity to expand

Responses

vpc1Id
string <uuid>

ID of the first VPC in the peering

-
vpc2Id
string <uuid>
object (VpcPeeringVpcSummary)

Summary of the first VPC in the peering.

+
id
string <uuid>

ID of the VPC

+
name
string [ 2 .. 256 ] characters

Name of the VPC

+
tenantId
string <uuid>

ID of the tenant

+
object (VpcPeeringTenantSummary)

Summary of the tenant

+
id
string <uuid>

ID of the Tenant

+
org
string

Name of the org this tenant belongs to

+
orgDisplayName
string or null

Display name of the org the Tenant belongs to

+
networkVirtualizationType
string or null
Enum: "ETHERNET_VIRTUALIZER" "FNN" "FLAT"

Network virtualization type of the VPC

+
status
string (VpcStatus)
Enum: "Pending" "Provisioning" "Ready" "Configuring" "Deleting" "Error"

Status of the VPC

+
vpc2Id
string <uuid>

ID of the second VPC in the peering

-
siteId
string <uuid>
object (VpcPeeringVpcSummary)

Summary of the second VPC in the peering.

+
id
string <uuid>

ID of the VPC

+
name
string [ 2 .. 256 ] characters

Name of the VPC

+
tenantId
string <uuid>

ID of the tenant

+
object (VpcPeeringTenantSummary)

Summary of the tenant

+
id
string <uuid>

ID of the Tenant

+
org
string

Name of the org this tenant belongs to

+
orgDisplayName
string or null

Display name of the org the Tenant belongs to

+
networkVirtualizationType
string or null
Enum: "ETHERNET_VIRTUALIZER" "FNN" "FLAT"

Network virtualization type of the VPC

+
status
string (VpcStatus)
Enum: "Pending" "Provisioning" "Ready" "Configuring" "Deleting" "Error"

Status of the VPC

+
siteId
string <uuid>

ID of the Site where the peering exists

-
isMultiTenant
boolean
object (SiteSummary)

Summary of the Site where the peering exists.

+
id
string <uuid>

Unique UUID v4 identifier for the Site

+
name
string [ 2 .. 256 ] characters

Name of the Site

+
infrastructureProviderId
string <uuid>

ID of the Infrastructure Provider that owns the Site

+
isSerialConsoleEnabled
boolean

Indicates if Serial Console is enabled for the Site by the Provider

+
isOnline
boolean

Indicates if the Site is currently reachable from Cloud

+
object (SiteCapabilities)

Site capabilities used for feature availability and configuration

+
nativeNetworking
boolean

Whether the Site supports native networking

+
networkSecurityGroup
boolean

Whether the Site supports Network Security Groups

+
nvLinkPartition
boolean

Whether the Site supports NVLink partitioning

+
flow
boolean

Whether the Site supports Flow-based operations

+
imageBasedOperatingSystem
boolean

Whether the Site supports image-based operating system provisioning

+
status
string (SiteStatus)
Enum: "Pending" "Registered" "Error"

Status of the Site

+
tenantId
string <uuid>

ID of the tenant that created the VPC peering.

+
object (TenantSummary)

Summary of the tenant that created the VPC peering.

+
org
string

Name of the org this tenant belongs to

+
orgDisplayName
string or null

Display name of the org the Tenant belongs to

+
object (TenantCapabilities)

Features that are enabled/disabled for Tenant

+
targetedInstanceCreation
boolean <uuid>

Indicates whether Tenant can create Instances by specifying Machine ID

+
isMultiTenant
boolean

Indicates if this is a multi-tenant peering (VPCs from different tenants)

status
string (VpcPeeringStatus)
Enum: "Pending" "Configuring" "Requested" "Ready" "Deleting" "Error"

Status of the VPC peering

@@ -3016,7 +3232,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc1Id": "bafbaf4c-c730-48ae-8f5d-d7d251f3315a",
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "isMultiTenant": true,
  • "status": "Pending",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a VPC peering

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-peering/{id}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc1Id": "bafbaf4c-c730-48ae-8f5d-d7d251f3315a",
  • "vpc1": {
    },
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "vpc2": {
    },
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "site": {
    },
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "tenant": {
    },
  • "isMultiTenant": true,
  • "status": "Pending",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a VPC peering

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Kubernetes Cluster

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/credential/bmc

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "kind": "SiteWideRoot",
  • "password": "string",
  • "username": "string",
  • "macAddress": "string"
}

Response samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "kind": "SiteWideRoot",
  • "username": "string",
  • "macAddress": "string"
}