Skip to content

Commit 9ec707b

Browse files
committed
feat: add ExpectedRack APIs (Create, Update, Get)
This adds `ExpectedRack` REST APIs to create, update, and get `ExpectedRack` instances, which is similar to the work for adding expected switch and power shelf API calls as well in #220. Tried to pattern match as much as possible again. Tests included! Signed-off-by: Chet Nichols III <chetn@nvidia.com>
1 parent c81ce28 commit 9ec707b

266 files changed

Lines changed: 12749 additions & 2414 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/pkg/api/handler/expectedrack.go

Lines changed: 1140 additions & 0 deletions
Large diffs are not rendered by default.

api/pkg/api/handler/expectedrack_test.go

Lines changed: 1617 additions & 0 deletions
Large diffs are not rendered by default.

api/pkg/api/model/expectedrack.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package model
19+
20+
import (
21+
"errors"
22+
"fmt"
23+
"time"
24+
25+
"github.com/google/uuid"
26+
27+
validation "github.com/go-ozzo/ozzo-validation/v4"
28+
validationis "github.com/go-ozzo/ozzo-validation/v4/is"
29+
30+
"github.com/NVIDIA/ncx-infra-controller-rest/api/pkg/api/model/util"
31+
cdbm "github.com/NVIDIA/ncx-infra-controller-rest/db/pkg/db/model"
32+
)
33+
34+
// APIExpectedRackCreateRequest is the data structure to capture request to create a new ExpectedRack
35+
type APIExpectedRackCreateRequest struct {
36+
// SiteID is the ID of the Site the rack belongs to
37+
SiteID string `json:"siteId"`
38+
// RackID is the operator-supplied identifier for the rack (string, not UUID).
39+
// Unique per Site.
40+
RackID string `json:"rackId"`
41+
// RackProfileID identifies the rack profile this rack conforms to
42+
RackProfileID string `json:"rackProfileId"`
43+
// Name is the optional human-readable name of the expected rack
44+
Name *string `json:"name"`
45+
// Description is the optional human-readable description of the expected rack
46+
Description *string `json:"description"`
47+
// Labels carries arbitrary key/value pairs. Well-known keys (chassis.*,
48+
// location.*) are used to convey chassis identity and physical location.
49+
Labels map[string]string `json:"labels"`
50+
}
51+
52+
// Validate ensure the values passed in request are acceptable
53+
func (ercr *APIExpectedRackCreateRequest) Validate() error {
54+
err := validation.ValidateStruct(ercr,
55+
validation.Field(&ercr.SiteID,
56+
validation.Required.Error(validationErrorValueRequired),
57+
validationis.UUID.Error(validationErrorInvalidUUID)),
58+
validation.Field(&ercr.RackID,
59+
validation.Required.Error(validationErrorValueRequired),
60+
validation.Match(util.NotAllWhitespaceRegexp).Error("RackID consists only of whitespace")),
61+
validation.Field(&ercr.RackProfileID,
62+
validation.Required.Error(validationErrorValueRequired),
63+
validation.Match(util.NotAllWhitespaceRegexp).Error("RackProfileID consists only of whitespace")),
64+
validation.Field(&ercr.Name,
65+
validation.NilOrNotEmpty.Error("Name cannot be empty")),
66+
validation.Field(&ercr.Description,
67+
validation.NilOrNotEmpty.Error("Description cannot be empty")),
68+
)
69+
70+
if err != nil {
71+
return err
72+
}
73+
74+
if err := util.ValidateLabels(ercr.Labels); err != nil {
75+
return err
76+
}
77+
78+
return nil
79+
}
80+
81+
// APIExpectedRackUpdateRequest is the data structure to capture user request to update an ExpectedRack
82+
type APIExpectedRackUpdateRequest struct {
83+
// ID is required for batch updates (must be empty or match path value for single update).
84+
ID *string `json:"id"`
85+
// RackID is the optional new operator-supplied rack identifier
86+
RackID *string `json:"rackId"`
87+
// RackProfileID is the optional new rack profile ID
88+
RackProfileID *string `json:"rackProfileId"`
89+
// Name is the optional new human-readable name of the expected rack
90+
Name *string `json:"name"`
91+
// Description is the optional new human-readable description of the expected rack
92+
Description *string `json:"description"`
93+
// Labels carries arbitrary key/value pairs. Well-known keys (chassis.*,
94+
// location.*) are used to convey chassis identity and physical location.
95+
Labels map[string]string `json:"labels"`
96+
}
97+
98+
// Validate ensure the values passed in request are acceptable
99+
func (erur *APIExpectedRackUpdateRequest) Validate() error {
100+
if erur.ID != nil {
101+
if *erur.ID == "" {
102+
return validation.Errors{
103+
"id": errors.New("ID cannot be empty"),
104+
}
105+
}
106+
if _, err := uuid.Parse(*erur.ID); err != nil {
107+
return validation.Errors{
108+
"id": errors.New("ID must be a valid UUID"),
109+
}
110+
}
111+
}
112+
113+
err := validation.ValidateStruct(erur,
114+
validation.Field(&erur.RackID,
115+
validation.NilOrNotEmpty.Error("RackID cannot be empty"),
116+
validation.When(erur.RackID != nil && *erur.RackID != "",
117+
validation.Match(util.NotAllWhitespaceRegexp).Error("RackID consists only of whitespace"))),
118+
validation.Field(&erur.RackProfileID,
119+
validation.NilOrNotEmpty.Error("RackProfileID cannot be empty"),
120+
validation.When(erur.RackProfileID != nil && *erur.RackProfileID != "",
121+
validation.Match(util.NotAllWhitespaceRegexp).Error("RackProfileID consists only of whitespace"))),
122+
validation.Field(&erur.Name,
123+
validation.NilOrNotEmpty.Error("Name cannot be empty")),
124+
validation.Field(&erur.Description,
125+
validation.NilOrNotEmpty.Error("Description cannot be empty")),
126+
)
127+
128+
if err != nil {
129+
return err
130+
}
131+
132+
if err := util.ValidateLabels(erur.Labels); err != nil {
133+
return err
134+
}
135+
136+
return nil
137+
}
138+
139+
// APIExpectedRack is the data structure to capture API representation of an ExpectedRack
140+
type APIExpectedRack struct {
141+
// ID is the unique identifier (UUID) of the expected rack
142+
ID uuid.UUID `json:"id"`
143+
// SiteID is the ID of the Site this rack belongs to
144+
SiteID uuid.UUID `json:"siteId"`
145+
// Site is the site information
146+
Site *APISite `json:"site,omitempty"`
147+
// RackID is the operator-supplied identifier for the rack
148+
RackID string `json:"rackId"`
149+
// RackProfileID identifies the rack profile this rack conforms to
150+
RackProfileID string `json:"rackProfileId"`
151+
// Name is the optional human-readable name of the expected rack
152+
Name string `json:"name"`
153+
// Description is the optional human-readable description of the expected rack
154+
Description string `json:"description"`
155+
// Labels carries arbitrary key/value pairs. Well-known keys (chassis.*,
156+
// location.*) are used to convey chassis identity and physical location.
157+
Labels map[string]string `json:"labels"`
158+
// Created indicates the ISO datetime string for when the ExpectedRack was created
159+
Created time.Time `json:"created"`
160+
// Updated indicates the ISO datetime string for when the ExpectedRack was last updated
161+
Updated time.Time `json:"updated"`
162+
}
163+
164+
// NewAPIExpectedRack accepts a DB layer ExpectedRack object and returns an API object
165+
func NewAPIExpectedRack(dbModel *cdbm.ExpectedRack) *APIExpectedRack {
166+
if dbModel == nil {
167+
return nil
168+
}
169+
170+
apier := &APIExpectedRack{
171+
ID: dbModel.ID,
172+
SiteID: dbModel.SiteID,
173+
RackID: dbModel.RackID,
174+
RackProfileID: dbModel.RackProfileID,
175+
Name: dbModel.Name,
176+
Description: dbModel.Description,
177+
Labels: dbModel.Labels,
178+
Created: dbModel.Created,
179+
Updated: dbModel.Updated,
180+
}
181+
182+
if dbModel.Site != nil {
183+
site := NewAPISite(*dbModel.Site, []cdbm.StatusDetail{}, nil)
184+
apier.Site = &site
185+
}
186+
187+
return apier
188+
}
189+
190+
// APIReplaceAllExpectedRacksRequest is the data structure to capture user request
191+
// to replace the full set of ExpectedRacks for a Site with the provided list.
192+
type APIReplaceAllExpectedRacksRequest struct {
193+
// SiteID is the ID of the Site whose ExpectedRacks should be replaced
194+
SiteID string `json:"siteId"`
195+
// ExpectedRacks is the list of ExpectedRack create requests to use as the
196+
// replacement set for the Site. May be empty to clear all ExpectedRacks
197+
// for the Site.
198+
ExpectedRacks []*APIExpectedRackCreateRequest `json:"expectedRacks"`
199+
}
200+
201+
// Validate ensure the values passed in request are acceptable
202+
func (rar *APIReplaceAllExpectedRacksRequest) Validate() error {
203+
err := validation.ValidateStruct(rar,
204+
validation.Field(&rar.SiteID,
205+
validation.Required.Error(validationErrorValueRequired),
206+
validationis.UUID.Error(validationErrorInvalidUUID)),
207+
)
208+
if err != nil {
209+
return err
210+
}
211+
212+
// Validate every entry and ensure they all reference the same Site as the top-level SiteID
213+
for i, er := range rar.ExpectedRacks {
214+
if er == nil {
215+
return validation.Errors{
216+
"expectedRacks": errors.New("ExpectedRack entry cannot be null"),
217+
}
218+
}
219+
if err := er.Validate(); err != nil {
220+
return validation.Errors{
221+
"expectedRacks": fmt.Errorf("entry %d: %w", i, err),
222+
}
223+
}
224+
if er.SiteID != rar.SiteID {
225+
return validation.Errors{
226+
"expectedRacks": fmt.Errorf("entry %d: siteId does not match top-level siteId", i),
227+
}
228+
}
229+
}
230+
231+
// Ensure rack IDs are unique within the request
232+
seen := make(map[string]bool, len(rar.ExpectedRacks))
233+
for i, er := range rar.ExpectedRacks {
234+
if seen[er.RackID] {
235+
return validation.Errors{
236+
"expectedRacks": fmt.Errorf("entry %d: duplicate rackId %q", i, er.RackID),
237+
}
238+
}
239+
seen[er.RackID] = true
240+
}
241+
242+
return nil
243+
}

0 commit comments

Comments
 (0)