From 196ad1377e4899c437ad850994fab9cd1396c6ce Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Wed, 24 Jun 2026 17:01:11 -0700 Subject: [PATCH 01/18] feat(rest-api): Enhance get-all VPC peering filtering and peer tenant visibility --- rest-api/api/pkg/api/handler/vpcpeering.go | 69 +++++++++++++- .../api/pkg/api/handler/vpcpeering_test.go | 93 ++++++++++++++++++- rest-api/api/pkg/api/model/vpcpeering.go | 8 ++ rest-api/api/pkg/api/model/vpcpeering_test.go | 19 +++- rest-api/db/pkg/db/model/vpcpeering.go | 15 ++- rest-api/db/pkg/db/model/vpcpeering_test.go | 17 ++++ rest-api/openapi/spec.yaml | 37 ++++++++ rest-api/sdk/standard/api_vpc_peering.go | 30 ++++++ rest-api/sdk/standard/client.go | 5 +- .../model_batch_instance_create_request.go | 5 +- .../sdk/standard/model_expected_machine.go | 3 +- .../model_expected_machine_create_request.go | 3 +- .../model_expected_machine_update_request.go | 3 +- .../standard/model_expected_power_shelf.go | 3 +- ...del_expected_power_shelf_create_request.go | 3 +- ...del_expected_power_shelf_update_request.go | 3 +- rest-api/sdk/standard/model_expected_rack.go | 5 +- .../model_expected_rack_create_request.go | 5 +- .../model_expected_rack_update_request.go | 5 +- .../sdk/standard/model_expected_switch.go | 3 +- .../model_expected_switch_create_request.go | 3 +- .../model_expected_switch_update_request.go | 3 +- .../standard/model_infini_band_partition.go | 5 +- ...el_infini_band_partition_create_request.go | 3 +- ...el_infini_band_partition_update_request.go | 5 +- rest-api/sdk/standard/model_instance.go | 5 +- .../standard/model_instance_create_request.go | 5 +- rest-api/sdk/standard/model_instance_type.go | 3 +- .../model_instance_type_create_request.go | 3 +- .../model_instance_type_update_request.go | 5 +- .../standard/model_instance_update_request.go | 5 +- rest-api/sdk/standard/model_machine.go | 5 +- .../standard/model_machine_update_request.go | 5 +- .../standard/model_network_security_group.go | 3 +- ...l_network_security_group_create_request.go | 5 +- ...l_network_security_group_update_request.go | 5 +- rest-api/sdk/standard/model_vpc.go | 5 +- .../sdk/standard/model_vpc_create_request.go | 5 +- rest-api/sdk/standard/model_vpc_peering.go | 74 +++++++++++++++ .../sdk/standard/model_vpc_update_request.go | 5 +- 40 files changed, 408 insertions(+), 83 deletions(-) diff --git a/rest-api/api/pkg/api/handler/vpcpeering.go b/rest-api/api/pkg/api/handler/vpcpeering.go index 2c1d6baa8f..d5e66444b3 100644 --- a/rest-api/api/pkg/api/handler/vpcpeering.go +++ b/rest-api/api/pkg/api/handler/vpcpeering.go @@ -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" // @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,54 @@ func (gavph GetAllVpcPeeringHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, errMsg, nil) } + // Get status from query param + if statusStrings := qParams["status"]; len(statusStrings) != 0 { + gavph.tracerSpan.SetAttribute(handlerSpan, attribute.StringSlice("status", statusStrings), logger) + for _, status := range statusStrings { + if !cdbm.VpcPeeringStatusMap[status] { + logger.Warn().Msg(fmt.Sprintf("invalid value in status query: %v", status)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid Status value in query", nil) + } + filterInput.Statuses = append(filterInput.Statuses, status) + } + } + + // Get vpcId from query param + if vpcIDStrs := qParams["vpcId"]; len(vpcIDStrs) != 0 { + gavph.tracerSpan.SetAttribute(handlerSpan, attribute.StringSlice("vpcId", vpcIDStrs), logger) + for _, vpcIDStr := range vpcIDStrs { + vpc, verr := common.GetVpcFromIDString(ctx, nil, vpcIDStr, nil, gavph.dbSession) + if verr != nil { + if verr == common.ErrInvalidID { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid VPC ID %v in query", vpcIDStr), nil) + } + if errors.Is(verr, cdb.ErrDoesNotExist) { + return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find VPC with ID %v specified in query", vpcIDStr), nil) + } + logger.Error().Err(verr).Msg("error retrieving Vpc from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to retrieve VPC with ID %v specified in query", vpcIDStr), nil) + } + filterInput.VpcIDs = append(filterInput.VpcIDs, vpc.ID) + } + } + + // Get peerTenantId from query param + if peerTenantIDStr := c.QueryParam("peerTenantId"); peerTenantIDStr != "" { + gavph.tracerSpan.SetAttribute(handlerSpan, attribute.String("peer_tenant_id", peerTenantIDStr), logger) + peerTenant, verr := common.GetTenantFromIDString(ctx, nil, peerTenantIDStr, gavph.dbSession) + if verr != nil { + if verr == common.ErrInvalidID { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid peer tenant ID %v in query", peerTenantIDStr), nil) + } + if errors.Is(verr, cdb.ErrDoesNotExist) { + return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find tenant with ID %v specified in query", peerTenantIDStr), nil) + } + logger.Error().Err(verr).Msg("error retrieving Tenant from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to retrieve tenant with ID %v specified in query", peerTenantIDStr), nil) + } + filterInput.PeerTenantIDs = []uuid.UUID{peerTenant.ID} + } + // Validate pagination request pageRequest := pagination.PageRequest{} err := c.Bind(&pageRequest) @@ -558,7 +609,8 @@ func (gavph GetAllVpcPeeringHandler) Handle(c echo.Context) error { Offset: pageRequest.Offset, OrderBy: pageRequest.OrderBy, } - vpcPeerings, total, err := vpcPeeringDAO.GetAll(ctx, nil, filterInput, vpcPeeringPageInput, qIncludeRelations) + includeRelations := vpcPeeringIncludeRelationsForTenantIDs(qIncludeRelations) + vpcPeerings, total, err := vpcPeeringDAO.GetAll(ctx, nil, filterInput, vpcPeeringPageInput, includeRelations) 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) @@ -656,7 +708,8 @@ func (gvph GetVpcPeeringHandler) Handle(c echo.Context) error { // Get VPC Peering from DB by ID vpcPeeringDAO := cdbm.NewVpcPeeringDAO(gvph.dbSession) - vpcPeering, err := vpcPeeringDAO.GetByID(ctx, nil, peeringUUID, qIncludeRelations) + includeRelations := vpcPeeringIncludeRelationsForTenantIDs(qIncludeRelations) + vpcPeering, err := vpcPeeringDAO.GetByID(ctx, nil, peeringUUID, includeRelations) if err != nil { if err == cdb.ErrDoesNotExist { return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find VPC Peering with ID: %s", peeringUUID.String()), nil) @@ -944,3 +997,15 @@ func (dvph DeleteVpcPeeringHandler) Handle(c echo.Context) error { return c.NoContent(http.StatusNoContent) } + +// vpcPeeringIncludeRelationsForTenantIDs ensures Vpc1 and Vpc2 are loaded so +// vpc1TenantId and vpc2TenantId can be populated in the API response. +func vpcPeeringIncludeRelationsForTenantIDs(includeRelations []string) []string { + relations := slices.Clone(includeRelations) + for _, relation := range []string{cdbm.Vpc1RelationName, cdbm.Vpc2RelationName} { + if !slices.Contains(relations, relation) { + relations = append(relations, relation) + } + } + return relations +} diff --git a/rest-api/api/pkg/api/handler/vpcpeering_test.go b/rest-api/api/pkg/api/handler/vpcpeering_test.go index 812cb420b2..b56f72ed95 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,6 +472,10 @@ 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() @@ -484,6 +488,7 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { expectedStatus int expectedCount int validatePagination bool + validateTenantIDs bool }{ { name: "error when user not found in request context", @@ -605,6 +610,82 @@ 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: "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 { @@ -655,6 +736,16 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { require.NoError(t, err) assert.Equal(t, tt.expectedCount, pageResp.Total) } + if tt.validateTenantIDs { + require.NotEmpty(t, list) + for _, peering := range list { + require.NotNil(t, peering.Vpc1TenantId) + require.NotNil(t, peering.Vpc2TenantId) + tenantIDs := []string{*peering.Vpc1TenantId, *peering.Vpc2TenantId} + assert.Contains(t, tenantIDs, tn1.ID.String()) + assert.Contains(t, tenantIDs, tn2.ID.String()) + } + } } }) } diff --git a/rest-api/api/pkg/api/model/vpcpeering.go b/rest-api/api/pkg/api/model/vpcpeering.go index d5b6e52b59..5ff08e6399 100644 --- a/rest-api/api/pkg/api/model/vpcpeering.go +++ b/rest-api/api/pkg/api/model/vpcpeering.go @@ -79,10 +79,14 @@ type APIVpcPeering struct { Vpc1ID string `json:"vpc1Id"` // Vpc1 is the summary of the first VPC in the peering Vpc1 *APIVpcSummary `json:"vpc1,omitempty"` + // Vpc1TenantId is the ID of the tenant that owns vpc1 + Vpc1TenantId *string `json:"vpc1TenantId,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"` + // Vpc2TenantId is the ID of the tenant that owns vpc2 + Vpc2TenantId *string `json:"vpc2TenantId,omitempty"` // SiteID is the ID of the Site where the peering exists SiteID string `json:"siteId"` // Site is the summary of the site @@ -113,9 +117,13 @@ func NewAPIVpcPeering(dbVpcPeering cdbm.VpcPeering) APIVpcPeering { // Expand relations if available. if dbVpcPeering.Vpc1 != nil { apiVpcPeering.Vpc1 = NewAPIVpcSummary(dbVpcPeering.Vpc1) + tenantID := dbVpcPeering.Vpc1.TenantID.String() + apiVpcPeering.Vpc1TenantId = &tenantID } if dbVpcPeering.Vpc2 != nil { apiVpcPeering.Vpc2 = NewAPIVpcSummary(dbVpcPeering.Vpc2) + tenantID := dbVpcPeering.Vpc2.TenantID.String() + apiVpcPeering.Vpc2TenantId = &tenantID } if dbVpcPeering.Site != nil { apiVpcPeering.Site = NewAPISiteSummary(dbVpcPeering.Site) diff --git a/rest-api/api/pkg/api/model/vpcpeering_test.go b/rest-api/api/pkg/api/model/vpcpeering_test.go index b8b4368ac8..31043a313d 100644 --- a/rest-api/api/pkg/api/model/vpcpeering_test.go +++ b/rest-api/api/pkg/api/model/vpcpeering_test.go @@ -132,6 +132,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,6 +143,18 @@ 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, + }, } api := NewAPIVpcPeering(dbVpcPeering) @@ -153,7 +167,10 @@ 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.Vpc1TenantId) + assert.Equal(t, vpc1TenantID.String(), *api.Vpc1TenantId) + require.NotNil(t, api.Vpc2TenantId) + assert.Equal(t, vpc2TenantID.String(), *api.Vpc2TenantId) } func TestNewAPIVpcPeeringSummary(t *testing.T) { 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/openapi/spec.yaml b/rest-api/openapi/spec.yaml index 47b6f50095..54960cc982 100644 --- a/rest-api/openapi/spec.yaml +++ b/rest-api/openapi/spec.yaml @@ -2635,6 +2635,33 @@ paths: name: isMultiTenant required: false description: Optional filter by peering tenancy type (single-tenant or multi-tenant). + - schema: + type: string + enum: + - Pending + - Configuring + - Requested + - Ready + - Deleting + - Error + in: query + name: status + required: false + description: Optional filter by peering status. Repeat the parameter to match multiple statuses. + - schema: + type: string + format: uuid + in: query + name: vpcId + required: false + description: Optional filter by VPC ID involved in the peering as either vpc1 or vpc2. + - schema: + type: string + format: uuid + in: query + name: peerTenantId + required: false + description: Optional filter by tenant ID of a VPC involved in the peering. - schema: type: string enum: @@ -14657,11 +14684,21 @@ components: format: uuid readOnly: true description: ID of the first VPC in the peering + vpc1TenantId: + type: string + format: uuid + readOnly: true + description: ID of the tenant that owns vpc1 vpc2Id: type: string format: uuid readOnly: true description: ID of the second VPC in the peering + vpc2TenantId: + type: string + format: uuid + readOnly: true + description: ID of the tenant that owns vpc2 siteId: type: string format: uuid diff --git a/rest-api/sdk/standard/api_vpc_peering.go b/rest-api/sdk/standard/api_vpc_peering.go index 4bcfbcff87..4f85481d20 100644 --- a/rest-api/sdk/standard/api_vpc_peering.go +++ b/rest-api/sdk/standard/api_vpc_peering.go @@ -319,6 +319,9 @@ type ApiGetAllVpcPeeringRequest struct { pageSize *int32 orderBy *string isMultiTenant *bool + status *string + vpcId *string + peerTenantId *string includeRelation *string } @@ -352,6 +355,24 @@ func (r ApiGetAllVpcPeeringRequest) IsMultiTenant(isMultiTenant bool) ApiGetAllV return r } +// Optional filter by peering status. Repeat the parameter to match multiple statuses. +func (r ApiGetAllVpcPeeringRequest) Status(status string) ApiGetAllVpcPeeringRequest { + r.status = &status + return r +} + +// Optional filter by VPC ID involved in the peering as either vpc1 or vpc2. +func (r ApiGetAllVpcPeeringRequest) VpcId(vpcId string) ApiGetAllVpcPeeringRequest { + r.vpcId = &vpcId + return r +} + +// Optional filter by tenant ID of a VPC involved in the peering. +func (r ApiGetAllVpcPeeringRequest) PeerTenantId(peerTenantId string) ApiGetAllVpcPeeringRequest { + r.peerTenantId = &peerTenantId + return r +} + // Related entity to expand func (r ApiGetAllVpcPeeringRequest) IncludeRelation(includeRelation string) ApiGetAllVpcPeeringRequest { r.includeRelation = &includeRelation @@ -424,6 +445,15 @@ func (a *VPCPeeringAPIService) GetAllVpcPeeringExecute(r ApiGetAllVpcPeeringRequ if r.isMultiTenant != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "isMultiTenant", r.isMultiTenant, "form", "") } + if r.status != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "status", r.status, "form", "") + } + if r.vpcId != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "vpcId", r.vpcId, "form", "") + } + if r.peerTenantId != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "peerTenantId", r.peerTenantId, "form", "") + } if r.includeRelation != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "includeRelation", r.includeRelation, "form", "") } diff --git a/rest-api/sdk/standard/client.go b/rest-api/sdk/standard/client.go index d631a2b8a9..e1ab022e3f 100644 --- a/rest-api/sdk/standard/client.go +++ b/rest-api/sdk/standard/client.go @@ -605,7 +605,10 @@ func addFile(w *multipart.Writer, fieldName, path string) error { if err != nil { return err } - defer file.Close() + err = file.Close() + if err != nil { + return err + } part, err := w.CreateFormFile(fieldName, filepath.Base(path)) if err != nil { diff --git a/rest-api/sdk/standard/model_batch_instance_create_request.go b/rest-api/sdk/standard/model_batch_instance_create_request.go index c0fd3fd48a..a240d9798b 100644 --- a/rest-api/sdk/standard/model_batch_instance_create_request.go +++ b/rest-api/sdk/standard/model_batch_instance_create_request.go @@ -49,9 +49,8 @@ type BatchInstanceCreateRequest struct { // When set to true, the iPXE script specified by OS or overridden here will always be run when rebooting the Instances. OS must be of iPXE type. AlwaysBootWithCustomIpxe *bool `json:"alwaysBootWithCustomIpxe,omitempty"` // When set to true, the Instances will be enabled with the Phone Home service. - PhoneHomeEnabled *bool `json:"phoneHomeEnabled,omitempty"` - // Key-value objects to be applied to all instances (shared across all instances) - Labels map[string]string `json:"labels,omitempty"` + PhoneHomeEnabled *bool `json:"phoneHomeEnabled,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Interface configuration shared across all instances. At least one interface must be specified unless `autoNetwork` is true. Either Subnet or VPC Prefix interfaces allowed, only one of the Subnets or VPC Prefixes can be attached over Physical interface. Interface `ipAddress` is not supported for batch instance creation requests. Mutually exclusive with `autoNetwork`: when `autoNetwork` is true this list MUST be empty. Interfaces []InterfaceCreateRequest `json:"interfaces,omitempty"` // When true, asks NICo to auto-resolve each Instance's network interfaces from the host's underlay (HostInband) network segments. Intended for instances on zero-DPU hosts (or hosts with their DPU in NIC mode). When true: (1) the target VPC's `networkVirtualizationType` MUST be `FLAT`, (2) `interfaces` MUST be empty or omitted, and (3) `secondaryVpcIds` MUST be empty or omitted. diff --git a/rest-api/sdk/standard/model_expected_machine.go b/rest-api/sdk/standard/model_expected_machine.go index cf15142758..3f0b46e867 100644 --- a/rest-api/sdk/standard/model_expected_machine.go +++ b/rest-api/sdk/standard/model_expected_machine.go @@ -58,8 +58,7 @@ type ExpectedMachine struct { // Tray index within the rack TrayIdx NullableInt32 `json:"trayIdx,omitempty"` // Host ID within the tray - HostId NullableInt32 `json:"hostId,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Machines + HostId NullableInt32 `json:"hostId,omitempty"` Labels map[string]string `json:"labels,omitempty"` // ISO 8601 datetime when the Expected Machine was created Created *time.Time `json:"created,omitempty"` diff --git a/rest-api/sdk/standard/model_expected_machine_create_request.go b/rest-api/sdk/standard/model_expected_machine_create_request.go index f4bc9daf19..12f6992b10 100644 --- a/rest-api/sdk/standard/model_expected_machine_create_request.go +++ b/rest-api/sdk/standard/model_expected_machine_create_request.go @@ -55,8 +55,7 @@ type ExpectedMachineCreateRequest struct { // Tray index within the rack TrayIdx NullableInt32 `json:"trayIdx,omitempty"` // Host ID within the tray - HostId NullableInt32 `json:"hostId,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Machines + HostId NullableInt32 `json:"hostId,omitempty"` Labels map[string]string `json:"labels,omitempty"` } diff --git a/rest-api/sdk/standard/model_expected_machine_update_request.go b/rest-api/sdk/standard/model_expected_machine_update_request.go index 1bbe2b1d73..795f7727bb 100644 --- a/rest-api/sdk/standard/model_expected_machine_update_request.go +++ b/rest-api/sdk/standard/model_expected_machine_update_request.go @@ -53,8 +53,7 @@ type ExpectedMachineUpdateRequest struct { // Tray index within the rack TrayIdx NullableInt32 `json:"trayIdx,omitempty"` // Host ID within the tray - HostId NullableInt32 `json:"hostId,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Machines + HostId NullableInt32 `json:"hostId,omitempty"` Labels map[string]string `json:"labels,omitempty"` } diff --git a/rest-api/sdk/standard/model_expected_power_shelf.go b/rest-api/sdk/standard/model_expected_power_shelf.go index 2d16e542c7..c0b3af60e7 100644 --- a/rest-api/sdk/standard/model_expected_power_shelf.go +++ b/rest-api/sdk/standard/model_expected_power_shelf.go @@ -48,8 +48,7 @@ type ExpectedPowerShelf struct { // Tray index within the rack TrayIdx NullableInt32 `json:"trayIdx,omitempty"` // Host ID within the tray - HostId NullableInt32 `json:"hostId,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Power Shelves + HostId NullableInt32 `json:"hostId,omitempty"` Labels map[string]string `json:"labels,omitempty"` // ISO 8601 datetime when the Expected Power Shelf was created Created *time.Time `json:"created,omitempty"` diff --git a/rest-api/sdk/standard/model_expected_power_shelf_create_request.go b/rest-api/sdk/standard/model_expected_power_shelf_create_request.go index 2612d70b02..2763b2788c 100644 --- a/rest-api/sdk/standard/model_expected_power_shelf_create_request.go +++ b/rest-api/sdk/standard/model_expected_power_shelf_create_request.go @@ -51,8 +51,7 @@ type ExpectedPowerShelfCreateRequest struct { // Tray index within the rack TrayIdx NullableInt32 `json:"trayIdx,omitempty"` // Host ID within the tray - HostId NullableInt32 `json:"hostId,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Power Shelves + HostId NullableInt32 `json:"hostId,omitempty"` Labels map[string]string `json:"labels,omitempty"` } diff --git a/rest-api/sdk/standard/model_expected_power_shelf_update_request.go b/rest-api/sdk/standard/model_expected_power_shelf_update_request.go index 20d872f1d0..8791a51156 100644 --- a/rest-api/sdk/standard/model_expected_power_shelf_update_request.go +++ b/rest-api/sdk/standard/model_expected_power_shelf_update_request.go @@ -49,8 +49,7 @@ type ExpectedPowerShelfUpdateRequest struct { // Tray index within the rack TrayIdx NullableInt32 `json:"trayIdx,omitempty"` // Host ID within the tray - HostId NullableInt32 `json:"hostId,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Power Shelves + HostId NullableInt32 `json:"hostId,omitempty"` Labels map[string]string `json:"labels,omitempty"` } diff --git a/rest-api/sdk/standard/model_expected_rack.go b/rest-api/sdk/standard/model_expected_rack.go index 0ee67b28e7..a18a054327 100644 --- a/rest-api/sdk/standard/model_expected_rack.go +++ b/rest-api/sdk/standard/model_expected_rack.go @@ -34,9 +34,8 @@ type ExpectedRack struct { // Human-readable name of the Expected Rack Name *string `json:"name,omitempty"` // Human-readable description of the Expected Rack - Description *string `json:"description,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Racks. Well-known keys (`chassis.*`, `location.*`) are used to convey chassis identity and physical location. - Labels map[string]string `json:"labels,omitempty"` + Description *string `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // ISO 8601 datetime when the Expected Rack was created Created *time.Time `json:"created,omitempty"` // ISO 8601 datetime when the Expected Rack was last updated diff --git a/rest-api/sdk/standard/model_expected_rack_create_request.go b/rest-api/sdk/standard/model_expected_rack_create_request.go index bf8e32ba9a..a8aa3f9b2f 100644 --- a/rest-api/sdk/standard/model_expected_rack_create_request.go +++ b/rest-api/sdk/standard/model_expected_rack_create_request.go @@ -33,9 +33,8 @@ type ExpectedRackCreateRequest struct { // Human-readable name of the Expected Rack Name NullableString `json:"name,omitempty"` // Human-readable description of the Expected Rack - Description NullableString `json:"description,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Racks. Well-known keys (`chassis.*`, `location.*`) are used to convey chassis identity and physical location. - Labels map[string]string `json:"labels,omitempty"` + Description NullableString `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` } type _ExpectedRackCreateRequest ExpectedRackCreateRequest diff --git a/rest-api/sdk/standard/model_expected_rack_update_request.go b/rest-api/sdk/standard/model_expected_rack_update_request.go index 3c6b39f1d0..c0c7152fc9 100644 --- a/rest-api/sdk/standard/model_expected_rack_update_request.go +++ b/rest-api/sdk/standard/model_expected_rack_update_request.go @@ -31,9 +31,8 @@ type ExpectedRackUpdateRequest struct { // Human-readable name of the Expected Rack Name NullableString `json:"name,omitempty"` // Human-readable description of the Expected Rack - Description NullableString `json:"description,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Racks. Well-known keys (`chassis.*`, `location.*`) are used to convey chassis identity and physical location. - Labels map[string]string `json:"labels,omitempty"` + Description NullableString `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` } // NewExpectedRackUpdateRequest instantiates a new ExpectedRackUpdateRequest object diff --git a/rest-api/sdk/standard/model_expected_switch.go b/rest-api/sdk/standard/model_expected_switch.go index 813c498b80..4267e45a2a 100644 --- a/rest-api/sdk/standard/model_expected_switch.go +++ b/rest-api/sdk/standard/model_expected_switch.go @@ -48,8 +48,7 @@ type ExpectedSwitch struct { // Tray index within the rack TrayIdx NullableInt32 `json:"trayIdx,omitempty"` // Host ID within the tray - HostId NullableInt32 `json:"hostId,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Switches + HostId NullableInt32 `json:"hostId,omitempty"` Labels map[string]string `json:"labels,omitempty"` // ISO 8601 datetime when the Expected Switch was created Created *time.Time `json:"created,omitempty"` diff --git a/rest-api/sdk/standard/model_expected_switch_create_request.go b/rest-api/sdk/standard/model_expected_switch_create_request.go index 8e2d7c8e13..503e18a9dc 100644 --- a/rest-api/sdk/standard/model_expected_switch_create_request.go +++ b/rest-api/sdk/standard/model_expected_switch_create_request.go @@ -55,8 +55,7 @@ type ExpectedSwitchCreateRequest struct { // Tray index within the rack TrayIdx NullableInt32 `json:"trayIdx,omitempty"` // Host ID within the tray - HostId NullableInt32 `json:"hostId,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Switches + HostId NullableInt32 `json:"hostId,omitempty"` Labels map[string]string `json:"labels,omitempty"` } diff --git a/rest-api/sdk/standard/model_expected_switch_update_request.go b/rest-api/sdk/standard/model_expected_switch_update_request.go index 62d39e825d..69fa8f13b8 100644 --- a/rest-api/sdk/standard/model_expected_switch_update_request.go +++ b/rest-api/sdk/standard/model_expected_switch_update_request.go @@ -53,8 +53,7 @@ type ExpectedSwitchUpdateRequest struct { // Tray index within the rack TrayIdx NullableInt32 `json:"trayIdx,omitempty"` // Host ID within the tray - HostId NullableInt32 `json:"hostId,omitempty"` - // User-defined key-value pairs for organizing and categorizing Expected Switches + HostId NullableInt32 `json:"hostId,omitempty"` Labels map[string]string `json:"labels,omitempty"` } diff --git a/rest-api/sdk/standard/model_infini_band_partition.go b/rest-api/sdk/standard/model_infini_band_partition.go index 3b1711832a..4c174c91e8 100644 --- a/rest-api/sdk/standard/model_infini_band_partition.go +++ b/rest-api/sdk/standard/model_infini_band_partition.go @@ -46,9 +46,8 @@ type InfiniBandPartition struct { // MTU configured for the InfiniBand Partition Mtu NullableInt32 `json:"mtu,omitempty"` // Whether SHARP is enabled for the InfiniBand Partition - EnableSharp *bool `json:"enableSharp,omitempty"` - // String key-value pairs describing InfiniBand Partition labels. Up to 10 key-value pairs can be specified - Labels map[string]string `json:"labels,omitempty"` + EnableSharp *bool `json:"enableSharp,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Status of the InfiniBand Partition Status *InfiniBandPartitionStatus `json:"status,omitempty"` // Chronological status history for the InfiniBand Partition diff --git a/rest-api/sdk/standard/model_infini_band_partition_create_request.go b/rest-api/sdk/standard/model_infini_band_partition_create_request.go index 72d8f2e621..d5e8ec015b 100644 --- a/rest-api/sdk/standard/model_infini_band_partition_create_request.go +++ b/rest-api/sdk/standard/model_infini_band_partition_create_request.go @@ -29,8 +29,7 @@ type InfiniBandPartitionCreateRequest struct { // Optional description of the Partition Description NullableString `json:"description,omitempty"` // ID of the Site the Partition should belong to - SiteId string `json:"siteId"` - // String key-value pairs describing Partition labels. Up to 10 key-value pairs can be specified + SiteId string `json:"siteId"` Labels map[string]string `json:"labels,omitempty"` } diff --git a/rest-api/sdk/standard/model_infini_band_partition_update_request.go b/rest-api/sdk/standard/model_infini_band_partition_update_request.go index a78f0efa95..b4cd969871 100644 --- a/rest-api/sdk/standard/model_infini_band_partition_update_request.go +++ b/rest-api/sdk/standard/model_infini_band_partition_update_request.go @@ -27,9 +27,8 @@ type InfiniBandPartitionUpdateRequest struct { // Name of the InfiniBand Partition Name string `json:"name"` // Description of the InfiniBand Partition - Description NullableString `json:"description,omitempty"` - // String key-value pairs describing Partition labels. Up to 10 key-value pairs can be specified - Labels map[string]string `json:"labels,omitempty"` + Description NullableString `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` } type _InfiniBandPartitionUpdateRequest InfiniBandPartitionUpdateRequest diff --git a/rest-api/sdk/standard/model_instance.go b/rest-api/sdk/standard/model_instance.go index 043115b44e..da89be33c2 100644 --- a/rest-api/sdk/standard/model_instance.go +++ b/rest-api/sdk/standard/model_instance.go @@ -60,9 +60,8 @@ type Instance struct { // Indicates whether the Phone Home service should be enabled or disabled for the Instance PhoneHomeEnabled *bool `json:"phoneHomeEnabled,omitempty"` // UserData is inherited from Operating System or specified by user if allowed - UserData NullableString `json:"userData,omitempty"` - // User-specified Instance labels - Labels map[string]string `json:"labels,omitempty"` + UserData NullableString `json:"userData,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Indicates whether an update is available for the Instance. Updates can be applied on reboot IsUpdatePending *bool `json:"isUpdatePending,omitempty"` // Serial Console URL for the Instance. Format: ssh://@siteSerialConsoleHostname diff --git a/rest-api/sdk/standard/model_instance_create_request.go b/rest-api/sdk/standard/model_instance_create_request.go index 9d48e52926..81bd5a2c73 100644 --- a/rest-api/sdk/standard/model_instance_create_request.go +++ b/rest-api/sdk/standard/model_instance_create_request.go @@ -49,9 +49,8 @@ type InstanceCreateRequest struct { // When set to true, the iPXE script specified by OS or overridden here will always be run when rebooting the Instance. OS must be of iPXE type. AlwaysBootWithCustomIpxe *bool `json:"alwaysBootWithCustomIpxe,omitempty"` // When set to true, the Instance will be enabled with the Phone Home service. - PhoneHomeEnabled *bool `json:"phoneHomeEnabled,omitempty"` - // User-defined key-value labels - Labels map[string]string `json:"labels,omitempty"` + PhoneHomeEnabled *bool `json:"phoneHomeEnabled,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // At least one interface must be specified unless `autoNetwork` is true. Either Subnet or VPC Prefix interfaces allowed. Only one of the Subnets or VPC Prefixes can be attached over Physical interface. If only one Subnet is specified, then it will be attached over physical interface regardless of the value of isPhysical. In case of VPC Prefix, isPhysical will always be true. Mutually exclusive with `autoNetwork`: when `autoNetwork` is true this list MUST be empty. Interfaces []InterfaceCreateRequest `json:"interfaces,omitempty"` // When true, asks NICo to auto-resolve the Instance's network interfaces from the host's underlay (HostInband) network segments. Intended for instances on zero-DPU hosts (or hosts with their DPU in NIC mode). When true: (1) the target VPC's `networkVirtualizationType` MUST be `FLAT`, (2) `interfaces` MUST be empty or omitted, and (3) `secondaryVpcIds` MUST be empty or omitted. Resolved interfaces surface on the Instance's read response. diff --git a/rest-api/sdk/standard/model_instance_type.go b/rest-api/sdk/standard/model_instance_type.go index f597262024..cb0d2726ed 100644 --- a/rest-api/sdk/standard/model_instance_type.go +++ b/rest-api/sdk/standard/model_instance_type.go @@ -34,8 +34,7 @@ type InstanceType struct { // ID of the Infrastructure Provider that owns the Instance Type InfrastructureProviderId *string `json:"infrastructureProviderId,omitempty"` // ID of the Site that owns the Instance Type - SiteId *string `json:"siteId,omitempty"` - // User-defined key-value labels for the Instance Type + SiteId *string `json:"siteId,omitempty"` Labels map[string]string `json:"labels,omitempty"` // List of capabilities that are supported by the Machine's of this Instance Type MachineCapabilities []MachineCapability `json:"machineCapabilities,omitempty"` diff --git a/rest-api/sdk/standard/model_instance_type_create_request.go b/rest-api/sdk/standard/model_instance_type_create_request.go index 4bc989b38c..5c0bcd0a20 100644 --- a/rest-api/sdk/standard/model_instance_type_create_request.go +++ b/rest-api/sdk/standard/model_instance_type_create_request.go @@ -29,8 +29,7 @@ type InstanceTypeCreateRequest struct { // Description of the Instance Type Description NullableString `json:"description,omitempty"` // ID of the site - SiteId string `json:"siteId"` - // User-defined key-value labels for the Instance Type + SiteId string `json:"siteId"` Labels map[string]string `json:"labels,omitempty"` // Site Controller assigned Machine type ControllerMachineType NullableString `json:"controllerMachineType,omitempty"` diff --git a/rest-api/sdk/standard/model_instance_type_update_request.go b/rest-api/sdk/standard/model_instance_type_update_request.go index a7b93a2e8a..21ccc2cb23 100644 --- a/rest-api/sdk/standard/model_instance_type_update_request.go +++ b/rest-api/sdk/standard/model_instance_type_update_request.go @@ -25,9 +25,8 @@ type InstanceTypeUpdateRequest struct { // Name of the Instance Type Name NullableString `json:"name,omitempty"` // Description of the Instance Type - Description NullableString `json:"description,omitempty"` - // User-defined key-value labels for the Instance Type - Labels map[string]string `json:"labels,omitempty"` + Description NullableString `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // List of Machine Capabilities to match MachineCapabilities []MachineCapability `json:"machineCapabilities,omitempty"` } diff --git a/rest-api/sdk/standard/model_instance_update_request.go b/rest-api/sdk/standard/model_instance_update_request.go index 11b8ebc57d..b3825b8877 100644 --- a/rest-api/sdk/standard/model_instance_update_request.go +++ b/rest-api/sdk/standard/model_instance_update_request.go @@ -45,9 +45,8 @@ type InstanceUpdateRequest struct { // Whether the custom iPXE data should be used for every boot. AlwaysBootWithCustomIpxe NullableBool `json:"alwaysBootWithCustomIpxe,omitempty"` // Indicates whether the Phone Home service should be enabled or disabled for the Instance - PhoneHomeEnabled NullableBool `json:"phoneHomeEnabled,omitempty"` - // Update labels of the Instance. 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. - Labels map[string]string `json:"labels,omitempty"` + PhoneHomeEnabled NullableBool `json:"phoneHomeEnabled,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // IDs of additional VPCs the Instance should attach to through non-primary interfaces. This field may only be specified when every entry in `interfaces` uses `vpcPrefixId`. IDs must be unique, must be valid UUIDs, and must not include the primary `vpcId`. SecondaryVpcIds []string `json:"secondaryVpcIds,omitempty"` // Update Interfaces of the Instance. Mutually exclusive with `autoNetwork`: when `autoNetwork` is true this list MUST be empty. diff --git a/rest-api/sdk/standard/model_machine.go b/rest-api/sdk/standard/model_machine.go index 50bad85d3c..b9029ead89 100644 --- a/rest-api/sdk/standard/model_machine.go +++ b/rest-api/sdk/standard/model_machine.go @@ -56,9 +56,8 @@ type Machine struct { // Health information about the machine Health *MachineHealth `json:"health,omitempty"` // Only available to Providers. Returned if the `includeMetadata` query parameter is specified. Otherwise attribute is omitted from response. - Metadata *MachineMetadata `json:"metadata,omitempty"` - // User-specified Machine labels - Labels map[string]string `json:"labels,omitempty"` + Metadata *MachineMetadata `json:"metadata,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Status represents the status of the machine Status *MachineStatus `json:"status,omitempty"` // Indicates whether the machine is usable by or currently in use by a tenant. diff --git a/rest-api/sdk/standard/model_machine_update_request.go b/rest-api/sdk/standard/model_machine_update_request.go index b81522ad74..f0afefcad5 100644 --- a/rest-api/sdk/standard/model_machine_update_request.go +++ b/rest-api/sdk/standard/model_machine_update_request.go @@ -29,9 +29,8 @@ type MachineUpdateRequest struct { // Set to `true` to enable maintenance mode and to `false` to disable maintenance mode. Can be set by Provider or Privileged Tenant. SetMaintenanceMode NullableBool `json:"setMaintenanceMode,omitempty"` // Optional message describing the reason for moving Machine into maintenance mode. Can be updated by Provider or Privileged Tenant. - MaintenanceMessage NullableString `json:"maintenanceMessage,omitempty"` - // Machine labels will be overwritten, include existing labels to preserve them. Can be updated by Provider or Privileged Tenant. - Labels map[string]string `json:"labels,omitempty"` + MaintenanceMessage NullableString `json:"maintenanceMessage,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Request to enter/exit online repair OnlineRepair *MachineOnlineRepair `json:"onlineRepair,omitempty"` // Required when `onlineRepair.enabled` is true. Must not be set when exiting online repair (`onlineRepair.enabled` false). diff --git a/rest-api/sdk/standard/model_network_security_group.go b/rest-api/sdk/standard/model_network_security_group.go index 1c379d94aa..2daa42415a 100644 --- a/rest-api/sdk/standard/model_network_security_group.go +++ b/rest-api/sdk/standard/model_network_security_group.go @@ -45,8 +45,7 @@ type NetworkSecurityGroup struct { RuleCount *int32 `json:"ruleCount,omitempty"` // Attachment statistics for the Network Security Group. Returned when the `includeAttachmentStats` query parameter is set to true in retrieval endpoints. AttachmentStats *NetworkSecurityGroupStats `json:"attachmentStats,omitempty"` - // Set of labels/tags for the Network Security Group - Labels map[string]string `json:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Date/time when the Network Security Group was created Created *time.Time `json:"created,omitempty"` // Date/time when the Network Security Group was last updated diff --git a/rest-api/sdk/standard/model_network_security_group_create_request.go b/rest-api/sdk/standard/model_network_security_group_create_request.go index f6ea7bf675..a71ab3bdcc 100644 --- a/rest-api/sdk/standard/model_network_security_group_create_request.go +++ b/rest-api/sdk/standard/model_network_security_group_create_request.go @@ -33,9 +33,8 @@ type NetworkSecurityGroupCreateRequest struct { // Egress rules with protocol and destination ports defined but without source ports defined should automatically be made stateful. StatefulEgress *bool `json:"statefulEgress,omitempty"` // Rules that belong to the Network Security Group - Rules []NetworkSecurityGroupRule `json:"rules,omitempty"` - // User-defined key-value labels for the Network Security Group - Labels map[string]string `json:"labels,omitempty"` + Rules []NetworkSecurityGroupRule `json:"rules,omitempty"` + Labels map[string]string `json:"labels,omitempty"` } type _NetworkSecurityGroupCreateRequest NetworkSecurityGroupCreateRequest diff --git a/rest-api/sdk/standard/model_network_security_group_update_request.go b/rest-api/sdk/standard/model_network_security_group_update_request.go index 1356c8edad..7a15676b62 100644 --- a/rest-api/sdk/standard/model_network_security_group_update_request.go +++ b/rest-api/sdk/standard/model_network_security_group_update_request.go @@ -29,9 +29,8 @@ type NetworkSecurityGroupUpdateRequest struct { // Egress rules with protocol and destination ports defined but without source ports defined should automatically be made stateful. StatefulEgress *bool `json:"statefulEgress,omitempty"` // Update rules of the Network Security Group. The rules will be replaced with the rules sent in the request. Any rules not included in the request will be removed. To retain existing rules, fetch them first and include them. - Rules []NetworkSecurityGroupRule `json:"rules,omitempty"` - // User-defined key-value labels for the Network Security Group - Labels map[string]string `json:"labels,omitempty"` + Rules []NetworkSecurityGroupRule `json:"rules,omitempty"` + Labels map[string]string `json:"labels,omitempty"` } // NewNetworkSecurityGroupUpdateRequest instantiates a new NetworkSecurityGroupUpdateRequest object diff --git a/rest-api/sdk/standard/model_vpc.go b/rest-api/sdk/standard/model_vpc.go index fd4a800b6a..63828b7f61 100644 --- a/rest-api/sdk/standard/model_vpc.go +++ b/rest-api/sdk/standard/model_vpc.go @@ -50,9 +50,8 @@ type VPC struct { // Propagation details for the attached Network Security Group NetworkSecurityGroupPropagationDetails *NetworkSecurityGroupPropagationDetails `json:"networkSecurityGroupPropagationDetails,omitempty"` // ID of the default NVLink Logical Partition that GPUs for all Instances in the VPC will attach to - NvLinkLogicalPartitionId NullableString `json:"nvLinkLogicalPartitionId,omitempty"` - // String key-value pairs describing VPC labels - Labels map[string]string `json:"labels,omitempty"` + NvLinkLogicalPartitionId NullableString `json:"nvLinkLogicalPartitionId,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Status of the VPC Status *VpcStatus `json:"status,omitempty"` // History of status changes for the VPC diff --git a/rest-api/sdk/standard/model_vpc_create_request.go b/rest-api/sdk/standard/model_vpc_create_request.go index 844efb3c2f..065156a0f2 100644 --- a/rest-api/sdk/standard/model_vpc_create_request.go +++ b/rest-api/sdk/standard/model_vpc_create_request.go @@ -41,9 +41,8 @@ type VpcCreateRequest struct { // Explicitly requested VNI for the VPC Vni NullableInt32 `json:"vni,omitempty"` // ID of the default NVLink Logical Partition that GPUs for all Instances in the VPC will attach to - NvLinkLogicalPartitionId NullableString `json:"nvLinkLogicalPartitionId,omitempty"` - // String key-value pairs describing VPC labels. Up to 10 key-value pairs can be specified - Labels map[string]string `json:"labels,omitempty"` + NvLinkLogicalPartitionId NullableString `json:"nvLinkLogicalPartitionId,omitempty"` + Labels map[string]string `json:"labels,omitempty"` } type _VpcCreateRequest VpcCreateRequest diff --git a/rest-api/sdk/standard/model_vpc_peering.go b/rest-api/sdk/standard/model_vpc_peering.go index e608f94a58..80865ca7e3 100644 --- a/rest-api/sdk/standard/model_vpc_peering.go +++ b/rest-api/sdk/standard/model_vpc_peering.go @@ -27,8 +27,12 @@ type VpcPeering struct { Id *string `json:"id,omitempty"` // ID of the first VPC in the peering Vpc1Id *string `json:"vpc1Id,omitempty"` + // ID of the tenant that owns vpc1 + Vpc1TenantId *string `json:"vpc1TenantId,omitempty"` // ID of the second VPC in the peering Vpc2Id *string `json:"vpc2Id,omitempty"` + // ID of the tenant that owns vpc2 + Vpc2TenantId *string `json:"vpc2TenantId,omitempty"` // ID of the Site where the peering exists SiteId *string `json:"siteId,omitempty"` // Indicates if this is a multi-tenant peering (VPCs from different tenants) @@ -122,6 +126,38 @@ func (o *VpcPeering) SetVpc1Id(v string) { o.Vpc1Id = &v } +// GetVpc1TenantId returns the Vpc1TenantId field value if set, zero value otherwise. +func (o *VpcPeering) GetVpc1TenantId() string { + if o == nil || IsNil(o.Vpc1TenantId) { + var ret string + return ret + } + return *o.Vpc1TenantId +} + +// GetVpc1TenantIdOk returns a tuple with the Vpc1TenantId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VpcPeering) GetVpc1TenantIdOk() (*string, bool) { + if o == nil || IsNil(o.Vpc1TenantId) { + return nil, false + } + return o.Vpc1TenantId, true +} + +// HasVpc1TenantId returns a boolean if a field has been set. +func (o *VpcPeering) HasVpc1TenantId() bool { + if o != nil && !IsNil(o.Vpc1TenantId) { + return true + } + + return false +} + +// SetVpc1TenantId gets a reference to the given string and assigns it to the Vpc1TenantId field. +func (o *VpcPeering) SetVpc1TenantId(v string) { + o.Vpc1TenantId = &v +} + // GetVpc2Id returns the Vpc2Id field value if set, zero value otherwise. func (o *VpcPeering) GetVpc2Id() string { if o == nil || IsNil(o.Vpc2Id) { @@ -154,6 +190,38 @@ func (o *VpcPeering) SetVpc2Id(v string) { o.Vpc2Id = &v } +// GetVpc2TenantId returns the Vpc2TenantId field value if set, zero value otherwise. +func (o *VpcPeering) GetVpc2TenantId() string { + if o == nil || IsNil(o.Vpc2TenantId) { + var ret string + return ret + } + return *o.Vpc2TenantId +} + +// GetVpc2TenantIdOk returns a tuple with the Vpc2TenantId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VpcPeering) GetVpc2TenantIdOk() (*string, bool) { + if o == nil || IsNil(o.Vpc2TenantId) { + return nil, false + } + return o.Vpc2TenantId, true +} + +// HasVpc2TenantId returns a boolean if a field has been set. +func (o *VpcPeering) HasVpc2TenantId() bool { + if o != nil && !IsNil(o.Vpc2TenantId) { + return true + } + + return false +} + +// SetVpc2TenantId gets a reference to the given string and assigns it to the Vpc2TenantId field. +func (o *VpcPeering) SetVpc2TenantId(v string) { + o.Vpc2TenantId = &v +} + // GetSiteId returns the SiteId field value if set, zero value otherwise. func (o *VpcPeering) GetSiteId() string { if o == nil || IsNil(o.SiteId) { @@ -330,9 +398,15 @@ func (o VpcPeering) ToMap() (map[string]interface{}, error) { if !IsNil(o.Vpc1Id) { toSerialize["vpc1Id"] = o.Vpc1Id } + if !IsNil(o.Vpc1TenantId) { + toSerialize["vpc1TenantId"] = o.Vpc1TenantId + } if !IsNil(o.Vpc2Id) { toSerialize["vpc2Id"] = o.Vpc2Id } + if !IsNil(o.Vpc2TenantId) { + toSerialize["vpc2TenantId"] = o.Vpc2TenantId + } if !IsNil(o.SiteId) { toSerialize["siteId"] = o.SiteId } diff --git a/rest-api/sdk/standard/model_vpc_update_request.go b/rest-api/sdk/standard/model_vpc_update_request.go index cc798e4efb..4ff6e847f2 100644 --- a/rest-api/sdk/standard/model_vpc_update_request.go +++ b/rest-api/sdk/standard/model_vpc_update_request.go @@ -29,9 +29,8 @@ type VpcUpdateRequest struct { // ID of the Network Security Group to attach to the VPC NetworkSecurityGroupId NullableString `json:"networkSecurityGroupId,omitempty"` // 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 - NvLinkLogicalPartitionId NullableString `json:"nvLinkLogicalPartitionId,omitempty"` - // 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. - Labels map[string]string `json:"labels,omitempty"` + NvLinkLogicalPartitionId NullableString `json:"nvLinkLogicalPartitionId,omitempty"` + Labels map[string]string `json:"labels,omitempty"` } // NewVpcUpdateRequest instantiates a new VpcUpdateRequest object From a6b85bcdb2a63a6b04c2c76d053a5bbd84053b87 Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Thu, 25 Jun 2026 10:30:50 -0700 Subject: [PATCH 02/18] Moved inline instead seperate function for inlcude relation --- rest-api/api/pkg/api/handler/vpcpeering.go | 24 ++++++++-------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/rest-api/api/pkg/api/handler/vpcpeering.go b/rest-api/api/pkg/api/handler/vpcpeering.go index d5e66444b3..27daadcff2 100644 --- a/rest-api/api/pkg/api/handler/vpcpeering.go +++ b/rest-api/api/pkg/api/handler/vpcpeering.go @@ -609,7 +609,14 @@ func (gavph GetAllVpcPeeringHandler) Handle(c echo.Context) error { Offset: pageRequest.Offset, OrderBy: pageRequest.OrderBy, } - includeRelations := vpcPeeringIncludeRelationsForTenantIDs(qIncludeRelations) + includeRelations := qIncludeRelations + if len(filterInput.VpcIDs) > 0 || len(filterInput.PeerTenantIDs) > 0 { + for _, relation := range []string{cdbm.Vpc1RelationName, cdbm.Vpc2RelationName} { + if !slices.Contains(includeRelations, relation) { + includeRelations = append(includeRelations, relation) + } + } + } vpcPeerings, total, err := vpcPeeringDAO.GetAll(ctx, nil, filterInput, vpcPeeringPageInput, includeRelations) if err != nil { logger.Error().Err(err).Msg("error retrieving VPC Peerings from DB") @@ -708,8 +715,7 @@ func (gvph GetVpcPeeringHandler) Handle(c echo.Context) error { // Get VPC Peering from DB by ID vpcPeeringDAO := cdbm.NewVpcPeeringDAO(gvph.dbSession) - includeRelations := vpcPeeringIncludeRelationsForTenantIDs(qIncludeRelations) - vpcPeering, err := vpcPeeringDAO.GetByID(ctx, nil, peeringUUID, includeRelations) + vpcPeering, err := vpcPeeringDAO.GetByID(ctx, nil, peeringUUID, qIncludeRelations) if err != nil { if err == cdb.ErrDoesNotExist { return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find VPC Peering with ID: %s", peeringUUID.String()), nil) @@ -997,15 +1003,3 @@ func (dvph DeleteVpcPeeringHandler) Handle(c echo.Context) error { return c.NoContent(http.StatusNoContent) } - -// vpcPeeringIncludeRelationsForTenantIDs ensures Vpc1 and Vpc2 are loaded so -// vpc1TenantId and vpc2TenantId can be populated in the API response. -func vpcPeeringIncludeRelationsForTenantIDs(includeRelations []string) []string { - relations := slices.Clone(includeRelations) - for _, relation := range []string{cdbm.Vpc1RelationName, cdbm.Vpc2RelationName} { - if !slices.Contains(relations, relation) { - relations = append(relations, relation) - } - } - return relations -} From c9f77492345d233aa291485949e4cb522042a94f Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Thu, 25 Jun 2026 11:13:03 -0700 Subject: [PATCH 03/18] Updated based on review comments --- rest-api/api/pkg/api/handler/vpcpeering.go | 44 ++++++------ .../api/pkg/api/handler/vpcpeering_test.go | 71 ++++++++++++------- rest-api/openapi/spec.yaml | 2 +- rest-api/sdk/standard/api_vpc_peering.go | 2 +- rest-api/sdk/standard/client.go | 9 --- rest-api/sdk/standard/model_interface.go | 3 +- .../model_interface_create_request.go | 3 +- 7 files changed, 74 insertions(+), 60 deletions(-) diff --git a/rest-api/api/pkg/api/handler/vpcpeering.go b/rest-api/api/pkg/api/handler/vpcpeering.go index 27daadcff2..d5421aceb1 100644 --- a/rest-api/api/pkg/api/handler/vpcpeering.go +++ b/rest-api/api/pkg/api/handler/vpcpeering.go @@ -434,7 +434,7 @@ func NewGetAllVpcPeeringHandler(dbSession *cdb.Session, tc tclient.Client, cfg * // @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" +// @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" @@ -546,12 +546,14 @@ func (gavph GetAllVpcPeeringHandler) Handle(c echo.Context) error { vpc, verr := common.GetVpcFromIDString(ctx, nil, vpcIDStr, nil, gavph.dbSession) if verr != nil { if verr == common.ErrInvalidID { - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid VPC ID %v in query", vpcIDStr), nil) + logger.Warn().Msg(fmt.Sprintf("invalid value in vpcId query: %v", vpcIDStr)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid VPC ID in query", nil) } if errors.Is(verr, cdb.ErrDoesNotExist) { + logger.Warn().Msg(fmt.Sprintf("could not find VPC with ID %v specified in query", vpcIDStr)) return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find VPC with ID %v specified in query", vpcIDStr), nil) } - logger.Error().Err(verr).Msg("error retrieving Vpc from DB") + logger.Error().Err(verr).Msg("error retrieving VPC from DB") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to retrieve VPC with ID %v specified in query", vpcIDStr), nil) } filterInput.VpcIDs = append(filterInput.VpcIDs, vpc.ID) @@ -559,20 +561,24 @@ func (gavph GetAllVpcPeeringHandler) Handle(c echo.Context) error { } // Get peerTenantId from query param - if peerTenantIDStr := c.QueryParam("peerTenantId"); peerTenantIDStr != "" { - gavph.tracerSpan.SetAttribute(handlerSpan, attribute.String("peer_tenant_id", peerTenantIDStr), logger) - peerTenant, verr := common.GetTenantFromIDString(ctx, nil, peerTenantIDStr, gavph.dbSession) - if verr != nil { - if verr == common.ErrInvalidID { - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid peer tenant ID %v in query", peerTenantIDStr), nil) - } - if errors.Is(verr, cdb.ErrDoesNotExist) { - return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find tenant with ID %v specified in query", peerTenantIDStr), nil) + if peerTenantIDStrs := qParams["peerTenantId"]; len(peerTenantIDStrs) != 0 { + gavph.tracerSpan.SetAttribute(handlerSpan, attribute.StringSlice("peerTenantId", peerTenantIDStrs), logger) + for _, peerTenantIDStr := range peerTenantIDStrs { + peerTenant, verr := common.GetTenantFromIDString(ctx, nil, peerTenantIDStr, gavph.dbSession) + if verr != nil { + if verr == common.ErrInvalidID { + 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) + } + if errors.Is(verr, cdb.ErrDoesNotExist) { + logger.Warn().Msg(fmt.Sprintf("could not find tenant with ID %v specified in query", peerTenantIDStr)) + return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find tenant with ID %v specified in query", peerTenantIDStr), nil) + } + logger.Error().Err(verr).Msg("error retrieving Tenant from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to retrieve tenant with ID %v specified in query", peerTenantIDStr), nil) } - logger.Error().Err(verr).Msg("error retrieving Tenant from DB") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to retrieve tenant with ID %v specified in query", peerTenantIDStr), nil) + filterInput.PeerTenantIDs = append(filterInput.PeerTenantIDs, peerTenant.ID) } - filterInput.PeerTenantIDs = []uuid.UUID{peerTenant.ID} } // Validate pagination request @@ -610,11 +616,9 @@ func (gavph GetAllVpcPeeringHandler) Handle(c echo.Context) error { OrderBy: pageRequest.OrderBy, } includeRelations := qIncludeRelations - if len(filterInput.VpcIDs) > 0 || len(filterInput.PeerTenantIDs) > 0 { - for _, relation := range []string{cdbm.Vpc1RelationName, cdbm.Vpc2RelationName} { - if !slices.Contains(includeRelations, relation) { - includeRelations = append(includeRelations, relation) - } + for _, relation := range []string{cdbm.Vpc1RelationName, cdbm.Vpc2RelationName} { + if !slices.Contains(includeRelations, relation) { + includeRelations = append(includeRelations, relation) } } vpcPeerings, total, err := vpcPeeringDAO.GetAll(ctx, nil, filterInput, vpcPeeringPageInput, includeRelations) diff --git a/rest-api/api/pkg/api/handler/vpcpeering_test.go b/rest-api/api/pkg/api/handler/vpcpeering_test.go index b56f72ed95..db297d167f 100644 --- a/rest-api/api/pkg/api/handler/vpcpeering_test.go +++ b/rest-api/api/pkg/api/handler/vpcpeering_test.go @@ -481,14 +481,16 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { cfg := common.GetTestConfig() tests := []struct { - name string - reqOrgName string - queryParams map[string]string - user *cdbm.User - expectedStatus int - expectedCount int - validatePagination bool - validateTenantIDs bool + name string + reqOrgName string + queryParams map[string]string + queryString string + user *cdbm.User + expectedStatus int + expectedCount int + validatePagination bool + validateTenantIDs bool + validateTenantIDsPresent bool }{ { name: "error when user not found in request context", @@ -517,12 +519,13 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { 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 across all sites when siteId omitted", + reqOrgName: tnOrg1, + user: tnu1, + expectedStatus: http.StatusOK, + expectedCount: 6, + validatePagination: true, + validateTenantIDsPresent: true, }, { name: "tenant admin 1 lists peerings in site 1", @@ -678,12 +681,20 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { expectedCount: 4, }, { - 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, + name: "provider and tenant admin filters by multiple peerTenantId values", + reqOrgName: ipOrg2, + queryString: fmt.Sprintf("peerTenantId=%s&peerTenantId=%s", tn1.ID, tnProvider.ID), + user: ipu2, expectedStatus: http.StatusOK, - expectedCount: 1, + expectedCount: 2, + }, + { + 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, }, } @@ -699,13 +710,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) @@ -736,6 +751,12 @@ 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.Vpc1TenantId) + require.NotNil(t, peering.Vpc2TenantId) + } + } if tt.validateTenantIDs { require.NotEmpty(t, list) for _, peering := range list { diff --git a/rest-api/openapi/spec.yaml b/rest-api/openapi/spec.yaml index 54960cc982..886882abfb 100644 --- a/rest-api/openapi/spec.yaml +++ b/rest-api/openapi/spec.yaml @@ -2661,7 +2661,7 @@ paths: in: query name: peerTenantId required: false - description: Optional filter by tenant ID of a VPC involved in the peering. + description: Optional filter by tenant ID of a VPC involved in the peering. Repeat the parameter to match multiple tenants. - schema: type: string enum: diff --git a/rest-api/sdk/standard/api_vpc_peering.go b/rest-api/sdk/standard/api_vpc_peering.go index 4f85481d20..e323fffd48 100644 --- a/rest-api/sdk/standard/api_vpc_peering.go +++ b/rest-api/sdk/standard/api_vpc_peering.go @@ -367,7 +367,7 @@ func (r ApiGetAllVpcPeeringRequest) VpcId(vpcId string) ApiGetAllVpcPeeringReque return r } -// Optional filter by tenant ID of a VPC involved in the peering. +// Optional filter by tenant ID of a VPC involved in the peering. Repeat the parameter to match multiple tenants. func (r ApiGetAllVpcPeeringRequest) PeerTenantId(peerTenantId string) ApiGetAllVpcPeeringRequest { r.peerTenantId = &peerTenantId return r diff --git a/rest-api/sdk/standard/client.go b/rest-api/sdk/standard/client.go index e1ab022e3f..30b573f2ab 100644 --- a/rest-api/sdk/standard/client.go +++ b/rest-api/sdk/standard/client.go @@ -543,15 +543,6 @@ func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err err *s = string(b) return nil } - if r, ok := v.(*io.Reader); ok { - *r = bytes.NewReader(b) - return nil - } - // Must stay before the JSON branch: json.Unmarshal would base64-decode into *[]byte. - if p, ok := v.(*[]byte); ok { - *p = b - return nil - } if f, ok := v.(*os.File); ok { f, err = os.CreateTemp("", "HttpClientFile") if err != nil { diff --git a/rest-api/sdk/standard/model_interface.go b/rest-api/sdk/standard/model_interface.go index 584892c0c3..3f16367db1 100644 --- a/rest-api/sdk/standard/model_interface.go +++ b/rest-api/sdk/standard/model_interface.go @@ -44,8 +44,7 @@ type Interface struct { // A list of IPv4 or IPv6 addresses IpAddresses []string `json:"ipAddresses,omitempty"` // Explicitly requested IP address for the interface. This is only used for VPC Prefix-based interfaces and is not valid for Subnet-based interfaces. The least-significant host bit must be 1. - RequestedIpAddress NullableString `json:"requestedIpAddress,omitempty"` - // Inline interface-local routing profile options. Only valid for VPC Prefix-based interfaces. + RequestedIpAddress NullableString `json:"requestedIpAddress,omitempty"` InlineRoutingProfile NullableInterfaceInlineRoutingProfile `json:"inlineRoutingProfile,omitempty"` // Status of the Interface Status *InterfaceStatus `json:"status,omitempty"` diff --git a/rest-api/sdk/standard/model_interface_create_request.go b/rest-api/sdk/standard/model_interface_create_request.go index 8aa1eb7e3b..5b5bdb37a1 100644 --- a/rest-api/sdk/standard/model_interface_create_request.go +++ b/rest-api/sdk/standard/model_interface_create_request.go @@ -27,8 +27,7 @@ type InterfaceCreateRequest struct { // ID of the VPC Prefix to attach to the Interface VpcPrefixId *string `json:"vpcPrefixId,omitempty"` // Explicitly requested IP address for the interface. It cannot be specified for Subnet-based interfaces. The least-significant host bit must be 1. - IpAddress NullableString `json:"ipAddress,omitempty"` - // Inline interface-local routing profile options. It cannot be specified for Subnet-based interfaces. + IpAddress NullableString `json:"ipAddress,omitempty"` InlineRoutingProfile NullableInterfaceInlineRoutingProfile `json:"inlineRoutingProfile,omitempty"` // Specifies whether this Subnet or VPC Prefix should be attached to the Instance over physical interface. IsPhysical *bool `json:"isPhysical,omitempty"` From 080750c2156393a0b025afdf4a68f21a1e78e6be Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Thu, 25 Jun 2026 13:45:59 -0700 Subject: [PATCH 04/18] Generated proto --- rest-api/workflow-schema/flow/protobuf/v1/flow_grpc.pb.go | 2 +- .../schema/site-agent/workflows/v1/fmds_nico_grpc.pb.go | 2 +- .../schema/site-agent/workflows/v1/nico_nico.pb.go | 4 ++-- .../schema/site-agent/workflows/v1/nico_nico_grpc.pb.go | 2 +- .../schema/site-agent/workflows/v1/nmx_c_nico_grpc.pb.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rest-api/workflow-schema/flow/protobuf/v1/flow_grpc.pb.go b/rest-api/workflow-schema/flow/protobuf/v1/flow_grpc.pb.go index fb8ba0d8b3..2db7331188 100644 --- a/rest-api/workflow-schema/flow/protobuf/v1/flow_grpc.pb.go +++ b/rest-api/workflow-schema/flow/protobuf/v1/flow_grpc.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc (unknown) // source: flow.proto diff --git a/rest-api/workflow-schema/schema/site-agent/workflows/v1/fmds_nico_grpc.pb.go b/rest-api/workflow-schema/schema/site-agent/workflows/v1/fmds_nico_grpc.pb.go index 89f87771a2..18c59a2356 100644 --- a/rest-api/workflow-schema/schema/site-agent/workflows/v1/fmds_nico_grpc.pb.go +++ b/rest-api/workflow-schema/schema/site-agent/workflows/v1/fmds_nico_grpc.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc (unknown) // source: fmds_nico.proto diff --git a/rest-api/workflow-schema/schema/site-agent/workflows/v1/nico_nico.pb.go b/rest-api/workflow-schema/schema/site-agent/workflows/v1/nico_nico.pb.go index 800bba3fc9..157d3bdfa0 100644 --- a/rest-api/workflow-schema/schema/site-agent/workflows/v1/nico_nico.pb.go +++ b/rest-api/workflow-schema/schema/site-agent/workflows/v1/nico_nico.pb.go @@ -56791,8 +56791,8 @@ func (x *GetMachineBootInterfacesResponse) GetDivergent() bool { type DNSMessage_DNSQuestion struct { state protoimpl.MessageState `protogen:"open.v1"` - QName *string `protobuf:"bytes,1,opt,name=q_name,json=qName,proto3,oneof" json:"q_name,omitempty"` // FQDN including trailing dot - QType *uint32 `protobuf:"varint,2,opt,name=q_type,json=qType,proto3,oneof" json:"q_type,omitempty"` + QName *string `protobuf:"bytes,1,opt,name=q_name,json=qName,proto3,oneof" json:"q_name,omitempty"` // FQDN including trailing dot + QType *uint32 `protobuf:"varint,2,opt,name=q_type,json=qType,proto3,oneof" json:"q_type,omitempty"` // QClass *uint32 `protobuf:"varint,3,opt,name=q_class,json=qClass,proto3,oneof" json:"q_class,omitempty"` // Usually 1 (IN) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/rest-api/workflow-schema/schema/site-agent/workflows/v1/nico_nico_grpc.pb.go b/rest-api/workflow-schema/schema/site-agent/workflows/v1/nico_nico_grpc.pb.go index c84cbdacd5..3bd3828f55 100644 --- a/rest-api/workflow-schema/schema/site-agent/workflows/v1/nico_nico_grpc.pb.go +++ b/rest-api/workflow-schema/schema/site-agent/workflows/v1/nico_nico_grpc.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc (unknown) // source: nico_nico.proto diff --git a/rest-api/workflow-schema/schema/site-agent/workflows/v1/nmx_c_nico_grpc.pb.go b/rest-api/workflow-schema/schema/site-agent/workflows/v1/nmx_c_nico_grpc.pb.go index 12b8742df4..69b8b2310e 100644 --- a/rest-api/workflow-schema/schema/site-agent/workflows/v1/nmx_c_nico_grpc.pb.go +++ b/rest-api/workflow-schema/schema/site-agent/workflows/v1/nmx_c_nico_grpc.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc (unknown) // source: nmx_c_nico.proto From 8c635f5e87364df6ac545dd51bea62501959fd2c Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Thu, 25 Jun 2026 14:20:58 -0700 Subject: [PATCH 05/18] Get all Vpc or Tenant IDs once --- rest-api/api/pkg/api/handler/vpcpeering.go | 84 ++++++---- .../api/pkg/api/handler/vpcpeering_test.go | 42 ++++- rest-api/db/pkg/db/model/tenant.go | 71 +++++++++ rest-api/db/pkg/db/model/tenant_test.go | 144 ++++++++++++++++++ 4 files changed, 312 insertions(+), 29 deletions(-) diff --git a/rest-api/api/pkg/api/handler/vpcpeering.go b/rest-api/api/pkg/api/handler/vpcpeering.go index d5421aceb1..f4425854f7 100644 --- a/rest-api/api/pkg/api/handler/vpcpeering.go +++ b/rest-api/api/pkg/api/handler/vpcpeering.go @@ -542,43 +542,75 @@ func (gavph GetAllVpcPeeringHandler) Handle(c echo.Context) error { // Get vpcId from query param if vpcIDStrs := qParams["vpcId"]; len(vpcIDStrs) != 0 { gavph.tracerSpan.SetAttribute(handlerSpan, attribute.StringSlice("vpcId", vpcIDStrs), logger) + vpcIDs := make([]uuid.UUID, 0, len(vpcIDStrs)) for _, vpcIDStr := range vpcIDStrs { - vpc, verr := common.GetVpcFromIDString(ctx, nil, vpcIDStr, nil, gavph.dbSession) - if verr != nil { - if verr == common.ErrInvalidID { - logger.Warn().Msg(fmt.Sprintf("invalid value in vpcId query: %v", vpcIDStr)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid VPC ID in query", nil) - } - if errors.Is(verr, cdb.ErrDoesNotExist) { - logger.Warn().Msg(fmt.Sprintf("could not find VPC with ID %v specified in query", vpcIDStr)) - return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find VPC with ID %v specified in query", vpcIDStr), nil) - } - logger.Error().Err(verr).Msg("error retrieving VPC from DB") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to retrieve VPC with ID %v specified in query", vpcIDStr), nil) + 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 = append(filterInput.VpcIDs, vpc.ID) } + filterInput.VpcIDs = vpcIDs } // Get peerTenantId from query param if peerTenantIDStrs := qParams["peerTenantId"]; len(peerTenantIDStrs) != 0 { gavph.tracerSpan.SetAttribute(handlerSpan, attribute.StringSlice("peerTenantId", peerTenantIDStrs), logger) + peerTenantIDs := make([]uuid.UUID, 0, len(peerTenantIDStrs)) for _, peerTenantIDStr := range peerTenantIDStrs { - peerTenant, verr := common.GetTenantFromIDString(ctx, nil, peerTenantIDStr, gavph.dbSession) - if verr != nil { - if verr == common.ErrInvalidID { - 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) - } - if errors.Is(verr, cdb.ErrDoesNotExist) { - logger.Warn().Msg(fmt.Sprintf("could not find tenant with ID %v specified in query", peerTenantIDStr)) - return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find tenant with ID %v specified in query", peerTenantIDStr), nil) - } - logger.Error().Err(verr).Msg("error retrieving Tenant from DB") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to retrieve tenant with ID %v specified in query", peerTenantIDStr), nil) + 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 = append(filterInput.PeerTenantIDs, peerTenant.ID) } + filterInput.PeerTenantIDs = peerTenantIDs } // Validate pagination request diff --git a/rest-api/api/pkg/api/handler/vpcpeering_test.go b/rest-api/api/pkg/api/handler/vpcpeering_test.go index db297d167f..7fdff5d175 100644 --- a/rest-api/api/pkg/api/handler/vpcpeering_test.go +++ b/rest-api/api/pkg/api/handler/vpcpeering_test.go @@ -491,6 +491,7 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { validatePagination bool validateTenantIDs bool validateTenantIDsPresent bool + requirePeerTenantIDs []uuid.UUID }{ { name: "error when user not found in request context", @@ -681,12 +682,31 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { expectedCount: 4, }, { - name: "provider and tenant admin filters by multiple peerTenantId values", + name: "provider and tenant admin filters by peerTenantId for tenant 2", reqOrgName: ipOrg2, - queryString: fmt.Sprintf("peerTenantId=%s&peerTenantId=%s", tn1.ID, tnProvider.ID), + queryParams: map[string]string{"peerTenantId": tn2.ID.String()}, user: ipu2, expectedStatus: http.StatusOK, - expectedCount: 2, + 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", @@ -767,6 +787,22 @@ func TestGetAllVpcPeeringHandler_Handle(t *testing.T) { 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.Vpc1TenantId) + require.NotNil(t, peering.Vpc2TenantId) + for _, tenantID := range tt.requirePeerTenantIDs { + tenantIDStr := tenantID.String() + if *peering.Vpc1TenantId == tenantIDStr || *peering.Vpc2TenantId == tenantIDStr { + matchedPeerTenantIDs[tenantID] = true + } + } + } + for _, tenantID := range tt.requirePeerTenantIDs { + assert.True(t, matchedPeerTenantIDs[tenantID], "expected at least one peering involving tenant %s", tenantID) + } + } } }) } diff --git a/rest-api/db/pkg/db/model/tenant.go b/rest-api/db/pkg/db/model/tenant.go index 56c107fa00..b92a002d75 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,6 +127,8 @@ type TenantDAO interface { // GetByID(ctx context.Context, tx *db.Tx, id uuid.UUID, includeRelations []string) (*Tenant, error) // + GetAll(ctx context.Context, tx *db.Tx, filter TenantFilterInput, page paginator.PageInput, includeRelations []string) ([]Tenant, int, error) + // GetAllByOrg(ctx context.Context, tx *db.Tx, org string, includeRelations []string) ([]Tenant, error) // Create(ctx context.Context, tx *db.Tx, input TenantCreateInput) (*Tenant, error) @@ -157,6 +173,61 @@ func (tsd TenantSQLDAO) GetByID(ctx context.Context, tx *db.Tx, id uuid.UUID, in return tn, nil } +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() + } + + tns := []Tenant{} + if filter.TenantIDs != nil && len(filter.TenantIDs) == 0 { + return tns, 0, nil + } + + query := db.GetIDB(tx, tsd.dbSession).NewSelect().Model(&tns) + query = tsd.setQueryWithFilter(filter, query, tnDAOSpan) + + for _, relation := range includeRelations { + query = query.Relation(relation) + } + + 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, 0, err + } + + err = pag.Query.Limit(pag.Limit).Offset(pag.Offset).Scan(ctx) + if err != nil { + return nil, 0, err + } + + return tns, pag.Total, 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 diff --git a/rest-api/db/pkg/db/model/tenant_test.go b/rest-api/db/pkg/db/model/tenant_test.go index 6eba23e502..ebfd0aad66 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" @@ -458,6 +459,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 From d66e402fed2b4e94daeeaf4e3108552bccb8512a Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Thu, 25 Jun 2026 14:23:41 -0700 Subject: [PATCH 06/18] openapi spec and index --- rest-api/docs/index.html | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/rest-api/docs/index.html b/rest-api/docs/index.html index 4d28726847..b772792247 100644 --- a/rest-api/docs/index.html +++ b/rest-api/docs/index.html @@ -2906,6 +2906,12 @@

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

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

+
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"

Related entity to expand

Responses

vpc1Id
string <uuid>

ID of the first VPC in the peering

+
vpc1TenantId
string <uuid>

ID of the tenant that owns vpc1

vpc2Id
string <uuid>

ID of the second VPC in the peering

+
vpc2TenantId
string <uuid>

ID of the tenant that owns vpc2

siteId
string <uuid>

ID of the Site where the peering exists

isMultiTenant
boolean
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

+
vpc1TenantId
string <uuid>

ID of the tenant that owns vpc1

vpc2Id
string <uuid>

ID of the second VPC in the peering

+
vpc2TenantId
string <uuid>

ID of the tenant that owns vpc2

siteId
string <uuid>

ID of the Site where the peering exists

isMultiTenant
boolean
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",
  • "vpc1TenantId": "17a77743-a2a8-4608-ba1f-30e00b645a46",
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "vpc2TenantId": "e0f239d1-1275-4381-b906-d2d28850a664",
  • "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

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

+
vpc1TenantId
string <uuid>

ID of the tenant that owns vpc1

vpc2Id
string <uuid>

ID of the second VPC in the peering

+
vpc2TenantId
string <uuid>

ID of the tenant that owns vpc2

siteId
string <uuid>

ID of the Site where the peering exists

isMultiTenant
boolean
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",
  • "vpc1TenantId": "17a77743-a2a8-4608-ba1f-30e00b645a46",
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "vpc2TenantId": "e0f239d1-1275-4381-b906-d2d28850a664",
  • "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

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"
}