Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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
319 changes: 319 additions & 0 deletions api/pkg/api/handler/rack.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"slices"
"strings"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
"go.opentelemetry.io/otel/attribute"
temporalEnums "go.temporal.io/api/enums/v1"
Expand Down Expand Up @@ -433,6 +434,324 @@ func (garh GetAllRackHandler) Handle(c echo.Context) error {
return c.JSON(http.StatusOK, apiRacks)
}

// ~~~~~ Create Rack Handler ~~~~~ //

// CreateRackHandler is the API Handler for creating a new expected Rack
type CreateRackHandler struct {
dbSession *cdb.Session
tc tClient.Client
scp *sc.ClientPool
cfg *config.Config
tracerSpan *cutil.TracerSpan
}

// NewCreateRackHandler initializes and returns a new handler for creating a Rack
func NewCreateRackHandler(dbSession *cdb.Session, tc tClient.Client, scp *sc.ClientPool, cfg *config.Config) CreateRackHandler {
return CreateRackHandler{
dbSession: dbSession,
tc: tc,
scp: scp,
cfg: cfg,
tracerSpan: cutil.NewTracerSpan(),
}
}

// Handle godoc
// @Summary Create a Rack
// @Description Create a new expected Rack definition in RLA
// @Tags rack
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param org path string true "Name of NGC organization"
// @Param body body model.APICreateRackRequest true "Create rack request"
// @Success 201 {object} model.APICreateRackResponse
// @Router /v2/org/{org}/carbide/rack [post]
func (crh CreateRackHandler) Handle(c echo.Context) error {
org, dbUser, ctx, logger, handlerSpan := common.SetupHandler("Rack", "Create", c, crh.tracerSpan)
if handlerSpan != nil {
defer handlerSpan.End()
}

// Parse and validate request body
var apiRequest model.APICreateRackRequest
if err := c.Bind(&apiRequest); err != nil {
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to parse request data", nil)
}
if err := apiRequest.Validate(); err != nil {
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, err.Error(), nil)
}

// Is DB user missing?
if dbUser == nil {
logger.Error().Msg("invalid User object found in request context")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve current user", nil)
}

// Validate org membership
ok, err := auth.ValidateOrgMembership(dbUser, org)
if !ok {
if err != nil {
logger.Error().Err(err).Msg("error validating org membership for User in request")
} else {
logger.Warn().Msg("could not validate org membership for user, access denied")
}
return cutil.NewAPIErrorResponse(c, http.StatusForbidden, fmt.Sprintf("Failed to validate membership for org: %s", org), nil)
}

// Validate role, only Provider Admins are allowed to create Rack
ok = auth.ValidateUserRoles(dbUser, org, nil, auth.ProviderAdminRole)
if !ok {
logger.Warn().Msg("user does not have Provider Admin role, access denied")
return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "User does not have Provider Admin role with org", nil)
}

// Get Infrastructure Provider for org
infrastructureProvider, err := common.GetInfrastructureProviderForOrg(ctx, nil, crh.dbSession, org)
if err != nil {
logger.Warn().Err(err).Msg("error getting infrastructure provider for org")
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to retrieve Infrastructure Provider for org", nil)
}

// Validate the site
site, err := common.GetSiteFromIDString(ctx, nil, apiRequest.SiteID, crh.dbSession)
if err != nil {
if errors.Is(err, common.ErrInvalidID) {
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to validate Site specified in request: invalid ID", nil)
}
if errors.Is(err, cdb.ErrDoesNotExist) {
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Site specified in request does not exist", nil)
}
logger.Error().Err(err).Msg("error retrieving Site from DB")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Site specified in request due to DB error", nil)
}

// Verify site belongs to the org's Infrastructure Provider
if site.InfrastructureProviderID != infrastructureProvider.ID {
return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Site specified in request doesn't belong to current org's Provider", nil)
}

siteConfig := &cdbm.SiteConfig{}
if site.Config != nil {
siteConfig = site.Config
}

if !siteConfig.RackLevelAdministration {
logger.Warn().Msg("site does not have Rack Level Administration enabled")
return cutil.NewAPIErrorResponse(c, http.StatusPreconditionFailed, "Site does not have Rack Level Administration enabled", nil)
}

// Get the temporal client for the site
stc, err := crh.scp.GetClientByID(site.ID)
if err != nil {
logger.Error().Err(err).Msg("failed to retrieve Temporal client for Site")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve client for Site", nil)
}

// Build RLA request
rlaRequest := &rlav1.CreateExpectedRackRequest{
Rack: apiRequest.ToProtoRack(),
}

// Execute workflow
workflowOptions := tClient.StartWorkflowOptions{
ID: fmt.Sprintf("rack-create-%s", apiRequest.SerialNumber),
WorkflowIDReusePolicy: temporalEnums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE,
WorkflowIDConflictPolicy: temporalEnums.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING,
WorkflowExecutionTimeout: cutil.WorkflowExecutionTimeout,
TaskQueue: queue.SiteTaskQueue,
}

ctx, cancel := context.WithTimeout(ctx, cutil.WorkflowContextTimeout)
defer cancel()

we, err := stc.ExecuteWorkflow(ctx, workflowOptions, "CreateExpectedRack", rlaRequest)
if err != nil {
logger.Error().Err(err).Msg("failed to execute CreateExpectedRack workflow")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to create Rack", nil)
}

// Get workflow result
var rlaResponse rlav1.CreateExpectedRackResponse
err = we.Get(ctx, &rlaResponse)
if err != nil {
var timeoutErr *tp.TimeoutError
if errors.As(err, &timeoutErr) || err == context.DeadlineExceeded || ctx.Err() != nil {
return common.TerminateWorkflowOnTimeOut(c, logger, stc, fmt.Sprintf("rack-create-%s", apiRequest.SerialNumber), err, "Rack", "CreateExpectedRack")
}
logger.Error().Err(err).Msg("failed to get result from CreateExpectedRack workflow")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to create Rack", nil)
}

logger.Info().Str("RackID", rlaResponse.GetId().GetId()).Msg("finishing API handler")

apiResponse := model.NewAPICreateRackResponse(&rlaResponse)
return c.JSON(http.StatusCreated, apiResponse)
}

// ~~~~~ Update Rack Handler ~~~~~ //

// UpdateRackHandler is the API Handler for updating an existing Rack
type UpdateRackHandler struct {
dbSession *cdb.Session
tc tClient.Client
scp *sc.ClientPool
cfg *config.Config
tracerSpan *cutil.TracerSpan
}

// NewUpdateRackHandler initializes and returns a new handler for updating a Rack
func NewUpdateRackHandler(dbSession *cdb.Session, tc tClient.Client, scp *sc.ClientPool, cfg *config.Config) UpdateRackHandler {
return UpdateRackHandler{
dbSession: dbSession,
tc: tc,
scp: scp,
cfg: cfg,
tracerSpan: cutil.NewTracerSpan(),
}
}

// Handle godoc
// @Summary Update a Rack
// @Description Update an existing Rack's fields by ID via RLA
// @Tags rack
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param org path string true "Name of NGC organization"
// @Param id path string true "ID of Rack"
// @Param body body model.APIRackUpdateRequest true "Update rack request"
// @Success 200 {object} model.APIRackUpdateResponse
// @Router /v2/org/{org}/carbide/rack/{id} [patch]
func (urh UpdateRackHandler) Handle(c echo.Context) error {
org, dbUser, ctx, logger, handlerSpan := common.SetupHandler("Rack", "Update", c, urh.tracerSpan)
if handlerSpan != nil {
defer handlerSpan.End()
}

// Parse and validate request body
var apiRequest model.APIRackUpdateRequest
if err := c.Bind(&apiRequest); err != nil {
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to parse request data", nil)
}
if err := apiRequest.Validate(); err != nil {
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, err.Error(), nil)
}

// Is DB user missing?
if dbUser == nil {
logger.Error().Msg("invalid User object found in request context")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve current user", nil)
}

// Validate org membership
ok, err := auth.ValidateOrgMembership(dbUser, org)
if !ok {
if err != nil {
logger.Error().Err(err).Msg("error validating org membership for User in request")
} else {
logger.Warn().Msg("could not validate org membership for user, access denied")
}
return cutil.NewAPIErrorResponse(c, http.StatusForbidden, fmt.Sprintf("Failed to validate membership for org: %s", org), nil)
}

// Validate role, only Provider Admins are allowed to update Rack
ok = auth.ValidateUserRoles(dbUser, org, nil, auth.ProviderAdminRole)
if !ok {
logger.Warn().Msg("user does not have Provider Admin role, access denied")
return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "User does not have Provider Admin role with org", nil)
}

// Get Infrastructure Provider for org
infrastructureProvider, err := common.GetInfrastructureProviderForOrg(ctx, nil, urh.dbSession, org)
if err != nil {
logger.Warn().Err(err).Msg("error getting infrastructure provider for org")
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to retrieve Infrastructure Provider for org", nil)
}

// Get rack ID from URL param
rackStrID := c.Param("id")
if _, err := uuid.Parse(rackStrID); err != nil {
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid rack ID: must be a valid UUID", nil)
}
urh.tracerSpan.SetAttribute(handlerSpan, attribute.String("rack_id", rackStrID), logger)

// Validate the site
site, err := common.GetSiteFromIDString(ctx, nil, apiRequest.SiteID, urh.dbSession)
if err != nil {
if errors.Is(err, common.ErrInvalidID) {
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to validate Site specified in request: invalid ID", nil)
}
if errors.Is(err, cdb.ErrDoesNotExist) {
return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Site specified in request does not exist", nil)
}
logger.Error().Err(err).Msg("error retrieving Site from DB")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Site specified in request due to DB error", nil)
}

// Verify site belongs to the org's Infrastructure Provider
if site.InfrastructureProviderID != infrastructureProvider.ID {
return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Site specified in request doesn't belong to current org's Provider", nil)
}

siteConfig := &cdbm.SiteConfig{}
if site.Config != nil {
siteConfig = site.Config
}

if !siteConfig.RackLevelAdministration {
logger.Warn().Msg("site does not have Rack Level Administration enabled")
return cutil.NewAPIErrorResponse(c, http.StatusPreconditionFailed, "Site does not have Rack Level Administration enabled", nil)
}

// Get the temporal client for the site
stc, err := urh.scp.GetClientByID(site.ID)
if err != nil {
logger.Error().Err(err).Msg("failed to retrieve Temporal client for Site")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve client for Site", nil)
}

// Build RLA request
rlaRequest := &rlav1.PatchRackRequest{
Rack: apiRequest.ToProtoRack(rackStrID),
}

// Execute workflow
workflowOptions := tClient.StartWorkflowOptions{
ID: fmt.Sprintf("rack-update-%s", rackStrID),
WorkflowIDReusePolicy: temporalEnums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE,
WorkflowIDConflictPolicy: temporalEnums.WORKFLOW_ID_CONFLICT_POLICY_FAIL,
WorkflowExecutionTimeout: cutil.WorkflowExecutionTimeout,
TaskQueue: queue.SiteTaskQueue,
}

ctx, cancel := context.WithTimeout(ctx, cutil.WorkflowContextTimeout)
defer cancel()

we, err := stc.ExecuteWorkflow(ctx, workflowOptions, "UpdateRack", rlaRequest)
if err != nil {
logger.Error().Err(err).Msg("failed to execute UpdateRack workflow")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to update Rack", nil)
}

// Get workflow result
var rlaResponse rlav1.PatchRackResponse
err = we.Get(ctx, &rlaResponse)
if err != nil {
var timeoutErr *tp.TimeoutError
if errors.As(err, &timeoutErr) || err == context.DeadlineExceeded || ctx.Err() != nil {
return common.TerminateWorkflowOnTimeOut(c, logger, stc, fmt.Sprintf("rack-update-%s", rackStrID), err, "Rack", "UpdateRack")
}
logger.Error().Err(err).Msg("failed to get result from UpdateRack workflow")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to update Rack", nil)
}

logger.Info().Str("Report", rlaResponse.GetReport()).Msg("finishing API handler")

apiResponse := model.NewAPIRackUpdateResponse(&rlaResponse)
return c.JSON(http.StatusOK, apiResponse)
}

// ~~~~~ Validate Rack Handler ~~~~~ //

// ValidateRackHandler is the API Handler for validating a Rack's components
Expand Down
Loading
Loading