Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
488 changes: 488 additions & 0 deletions rest-api/api/pkg/api/handler/domain.go

Large diffs are not rendered by default.

977 changes: 977 additions & 0 deletions rest-api/api/pkg/api/handler/domain_test.go

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions rest-api/api/pkg/api/model/domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package model

import (
"time"

validation "github.com/go-ozzo/ozzo-validation/v4"
validationis "github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/google/uuid"

cdbm "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/model"
corev1 "github.com/NVIDIA/infra-controller/rest-api/proto/core/gen/v1"
)

// APIDomainCreateRequest is the request body for creating a tenant-owned DNS Domain.
type APIDomainCreateRequest struct {
Name string `json:"name"`
SiteID string `json:"siteId"`
}

// Validate checks the Domain create request before it is sent to Core.
func (dcr APIDomainCreateRequest) Validate() error {
return validation.ValidateStruct(&dcr,
validation.Field(&dcr.Name, validation.Required.Error(validationErrorValueRequired)),
validation.Field(&dcr.SiteID,
validation.Required.Error(validationErrorValueRequired),
validationis.UUID.Error(validationErrorInvalidUUID)),
)
}

// ToProto converts a validated REST request into Core's Domain create request.
func (dcr APIDomainCreateRequest) ToProto() *corev1.CreateDomainRequest {
return &corev1.CreateDomainRequest{Name: dcr.Name}
}

// APIDomainGetAllRequest captures optional Domain list filters.
type APIDomainGetAllRequest struct {
TenantID string `query:"tenantId"`
SiteID string `query:"siteId"`
}

// Validate checks optional Domain list filters.
func (dgar APIDomainGetAllRequest) Validate() error {
return validation.ValidateStruct(&dgar,
validation.Field(&dgar.TenantID,
validation.When(dgar.TenantID != "", validationis.UUID.Error(validationErrorInvalidUUID))),
validation.Field(&dgar.SiteID,
validation.When(dgar.SiteID != "", validationis.UUID.Error(validationErrorInvalidUUID))),
)
}

// APIDomainUpdateRequest is the request body for renaming a tenant-owned DNS Domain.
type APIDomainUpdateRequest struct {
Name string `json:"name"`
ControllerDomainID uuid.UUID `json:"-"`
}

// Validate checks the Domain update request before it is sent to Core.
func (dur APIDomainUpdateRequest) Validate() error {
return validation.ValidateStruct(&dur,
validation.Field(&dur.Name, validation.Required.Error(validationErrorValueRequired)),
)
}

// ToProto converts a validated REST request into Core's Domain update request.
func (dur APIDomainUpdateRequest) ToProto() *corev1.UpdateDomainRequest {
return &corev1.UpdateDomainRequest{
Domain: &corev1.Domain{
Id: &corev1.DomainId{Value: dur.ControllerDomainID.String()},
Name: dur.Name,
},
}
}

// APIDomain is the tenant-facing representation of a DNS Domain.
type APIDomain struct {
ID string `json:"id"`
Name string `json:"name"`
TenantID string `json:"tenantId"`
SiteID string `json:"siteId"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}

// NewAPIDomain converts an owned REST DB projection to its public representation.
func NewAPIDomain(domain *cdbm.Domain) *APIDomain {
if domain == nil {
return nil
}

siteID := ""
if domain.SiteID != nil {
siteID = domain.SiteID.String()
}
tenantID := ""
if domain.TenantID != nil {
tenantID = domain.TenantID.String()
}

return &APIDomain{
ID: domain.ID.String(),
Name: domain.Hostname,
TenantID: tenantID,
SiteID: siteID,
Created: domain.Created,
Updated: domain.Updated,
}
}
174 changes: 174 additions & 0 deletions rest-api/api/pkg/api/model/domain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package model

import (
"encoding/json"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

cdbm "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/model"
)

func TestAPIDomainCreateRequest_Validate(t *testing.T) {
tests := []struct {
name string
request APIDomainCreateRequest
wantErr bool
}{
{
name: "valid",
request: APIDomainCreateRequest{Name: "tenant.example.com", SiteID: uuid.NewString()},
},
{
name: "missing name",
request: APIDomainCreateRequest{SiteID: uuid.NewString()},
wantErr: true,
},
{
name: "missing site ID",
request: APIDomainCreateRequest{Name: "tenant.example.com"},
wantErr: true,
},
{
name: "invalid site ID",
request: APIDomainCreateRequest{Name: "tenant.example.com", SiteID: "not-a-uuid"},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.request.Validate()
assert.Equal(t, tt.wantErr, err != nil)
})
}
}

func TestAPIDomainCreateRequest_ToProto(t *testing.T) {
request := APIDomainCreateRequest{Name: "tenant.example.com", SiteID: uuid.NewString()}

assert.Equal(t, request.Name, request.ToProto().GetName())
}

func TestAPIDomainGetAllRequest_Validate(t *testing.T) {
tests := []struct {
name string
request APIDomainGetAllRequest
wantErr bool
}{
{name: "empty filters"},
{name: "valid filters", request: APIDomainGetAllRequest{TenantID: uuid.NewString(), SiteID: uuid.NewString()}},
{name: "invalid tenant ID", request: APIDomainGetAllRequest{TenantID: "invalid"}, wantErr: true},
{name: "invalid site ID", request: APIDomainGetAllRequest{SiteID: "invalid"}, wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.request.Validate()
assert.Equal(t, tt.wantErr, err != nil)
})
}
}

func TestAPIDomainUpdateRequest_Validate(t *testing.T) {
tests := []struct {
name string
request APIDomainUpdateRequest
wantErr bool
}{
{name: "valid", request: APIDomainUpdateRequest{Name: "renamed.example.com"}},
{name: "missing name", wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.request.Validate()
assert.Equal(t, tt.wantErr, err != nil)
})
}
}

func TestAPIDomainUpdateRequest_ToProto(t *testing.T) {
controllerDomainID := uuid.New()
request := APIDomainUpdateRequest{Name: "renamed.example.com", ControllerDomainID: controllerDomainID}

got := request.ToProto().GetDomain()
require.NotNil(t, got)
assert.Equal(t, controllerDomainID.String(), got.GetId().GetValue())
assert.Equal(t, request.Name, got.GetName())
}

func TestNewAPIDomain(t *testing.T) {
localID := uuid.New()
controllerID := uuid.New()
tenantID := uuid.New()
siteID := uuid.New()
created := time.Now().UTC().Round(time.Microsecond)
tests := []struct {
name string
domain *cdbm.Domain
check func(*testing.T, *APIDomain)
}{
{
name: "nil input",
check: func(t *testing.T, got *APIDomain) {
assert.Nil(t, got)
},
},
{
name: "owned projection uses REST local identity",
domain: &cdbm.Domain{
ID: localID,
Hostname: "tenant.example.com",
TenantID: &tenantID,
SiteID: &siteID,
ControllerDomainID: &controllerID,
Created: created,
Updated: created,
},
check: func(t *testing.T, got *APIDomain) {
require.NotNil(t, got)
assert.Equal(t, localID.String(), got.ID)
assert.NotEqual(t, controllerID.String(), got.ID)
assert.Equal(t, tenantID.String(), got.TenantID)
assert.Equal(t, siteID.String(), got.SiteID)
assert.Equal(t, "tenant.example.com", got.Name)
assert.Equal(t, created, got.Created)
assert.Equal(t, created, got.Updated)
},
},
{
name: "missing ownership remains explicit",
domain: &cdbm.Domain{ID: localID},
check: func(t *testing.T, got *APIDomain) {
require.NotNil(t, got)
assert.Empty(t, got.TenantID)
assert.Empty(t, got.SiteID)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.check(t, NewAPIDomain(tt.domain))
})
}
}

func TestAPIDomain_MarshalJSON(t *testing.T) {
encoded, err := json.Marshal(APIDomain{})
require.NoError(t, err)

var got map[string]any
require.NoError(t, json.Unmarshal(encoded, &got))
assert.Len(t, got, 6)
for _, key := range []string{"id", "name", "tenantId", "siteId", "created", "updated"} {
assert.Contains(t, got, key)
}
}
68 changes: 47 additions & 21 deletions rest-api/api/pkg/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,53 @@ func NewAPIRoutes(dbSession *cdb.Session, tc tClient.Client, tnc tClient.Namespa
Method: http.MethodDelete,
Handler: apiHandler.NewDeleteSubnetHandler(dbSession, tc, scp, cfg),
},
// NVLink Domain operation endpoints (Flow).
{
Path: apiPathPrefix + "/domain/nvlink/power",
Method: http.MethodPatch,
Handler: apiHandler.NewBatchUpdateNVLinkDomainPowerStateHandler(dbSession, scp),
},
{
Path: apiPathPrefix + "/domain/nvlink/firmware",
Method: http.MethodPatch,
Handler: apiHandler.NewBatchUpdateNVLinkDomainFirmwareHandler(dbSession, scp),
},
{
Path: apiPathPrefix + "/domain/nvlink/:id/power",
Method: http.MethodPatch,
Handler: apiHandler.NewUpdateNVLinkDomainPowerStateHandler(dbSession, scp),
},
{
Path: apiPathPrefix + "/domain/nvlink/:id/firmware",
Method: http.MethodPatch,
Handler: apiHandler.NewUpdateNVLinkDomainFirmwareHandler(dbSession, scp),
},
// DNS Domain endpoints
{
Path: apiPathPrefix + "/domain",
Method: http.MethodPost,
Handler: apiHandler.NewCreateDomainHandler(dbSession, scp),
},
{
Path: apiPathPrefix + "/domain",
Method: http.MethodGet,
Handler: apiHandler.NewGetAllDomainHandler(dbSession),
},
{
Path: apiPathPrefix + "/domain/:domainId",
Method: http.MethodGet,
Handler: apiHandler.NewGetDomainHandler(dbSession),
},
{
Path: apiPathPrefix + "/domain/:domainId",
Method: http.MethodPatch,
Handler: apiHandler.NewUpdateDomainHandler(dbSession, scp),
},
{
Path: apiPathPrefix + "/domain/:domainId",
Method: http.MethodDelete,
Handler: apiHandler.NewDeleteDomainHandler(dbSession, scp),
},
// OperatingSystem endpoints
{
Path: apiPathPrefix + "/operating-system",
Expand Down Expand Up @@ -1144,27 +1191,6 @@ func NewAPIRoutes(dbSession *cdb.Session, tc tClient.Client, tnc tClient.Namespa
Method: http.MethodPost,
Handler: apiHandler.NewCancelTaskRunHandler(dbSession, tc, scp, cfg),
},
// NVLink Domain operation endpoints (Flow).
{
Path: apiPathPrefix + "/domain/nvlink/power",
Method: http.MethodPatch,
Handler: apiHandler.NewBatchUpdateNVLinkDomainPowerStateHandler(dbSession, scp),
},
{
Path: apiPathPrefix + "/domain/nvlink/firmware",
Method: http.MethodPatch,
Handler: apiHandler.NewBatchUpdateNVLinkDomainFirmwareHandler(dbSession, scp),
},
{
Path: apiPathPrefix + "/domain/nvlink/:id/power",
Method: http.MethodPatch,
Handler: apiHandler.NewUpdateNVLinkDomainPowerStateHandler(dbSession, scp),
},
{
Path: apiPathPrefix + "/domain/nvlink/:id/firmware",
Method: http.MethodPatch,
Handler: apiHandler.NewUpdateNVLinkDomainFirmwareHandler(dbSession, scp),
},
{
Path: apiPathPrefix + "/rack",
Method: http.MethodGet,
Expand Down
Loading
Loading