Skip to content
Open
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
2 changes: 1 addition & 1 deletion pkg/dao/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (d *sqlResourceDao) Get(ctx context.Context, kind, id string) (*api.Resourc
func (d *sqlResourceDao) GetForUpdate(ctx context.Context, kind, id string) (*api.Resource, error) {
g2 := d.sessionFactory.New(ctx)
var resource api.Resource
if err := g2.Clauses(clause.Locking{Strength: "UPDATE"}).Take(
if err := g2.Preload("Conditions").Clauses(clause.Locking{Strength: "UPDATE"}).Take(
&resource, "kind = ? AND id = ?", kind, id).Error; err != nil {
return nil, err
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/dao/resource_condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type ResourceConditionDao interface {
// For new condition types, both are used as-is from the input — caller
// must populate them. Zero timestamps are defaulted to now.
UpdateConditions(ctx context.Context, resourceID string, conditions []api.ResourceCondition) error

// DeleteByResource removes all condition rows for a resource.
// Used during hard-delete to clean up before removing the resource row.
DeleteByResource(ctx context.Context, resourceID string) error
}

var _ ResourceConditionDao = &sqlResourceConditionDao{}
Expand Down Expand Up @@ -84,3 +88,12 @@ func (d *sqlResourceConditionDao) UpdateConditions(

return nil
}

func (d *sqlResourceConditionDao) DeleteByResource(ctx context.Context, resourceID string) error {
g2 := d.sessionFactory.New(ctx)
if err := g2.Where("resource_id = ?", resourceID).Delete(&api.ResourceCondition{}).Error; err != nil {
db.MarkForRollback(ctx, err)
return err
}
return nil
}
166 changes: 166 additions & 0 deletions pkg/handlers/resource_status_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package handlers

import (
"context"
"net/http"

"github.com/gorilla/mux"

"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api/openapi"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api/presenters"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/errors"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/registry"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/services"
)

// ResourceStatusHandler handles GET/PUT on /{plural}/{id}/statuses for
// config-driven generic resources. Each method branches on whether "parent_id"
// is present in mux.Vars(r) to handle both flat and nested routes — matching
// the pattern established by ResourceHandler in HYPERFLEET-1157.
type ResourceStatusHandler struct {
resourceService services.ResourceService
adapterStatusService services.AdapterStatusService
descriptor registry.EntityDescriptor
}

func NewResourceStatusHandler(
descriptor registry.EntityDescriptor,
resourceService services.ResourceService,
adapterStatusService services.AdapterStatusService,
) *ResourceStatusHandler {
return &ResourceStatusHandler{
descriptor: descriptor,
resourceService: resourceService,
adapterStatusService: adapterStatusService,
}
}

// List returns all adapter statuses for a resource with pagination.
// Verifies ownership when parent_id is present in the route.
func (h *ResourceStatusHandler) List(w http.ResponseWriter, r *http.Request) {
cfg := &handlerConfig{
Action: func() (interface{}, *errors.ServiceError) {
ctx := r.Context()
id := mux.Vars(r)["id"]
listArgs, err := services.NewListArguments(r.URL.Query())
if err != nil {
return nil, err
}

if svcErr := h.verifyResource(r, id); svcErr != nil {
return nil, svcErr
}

return h.listStatuses(ctx, id, listArgs)
},
ErrorHandler: handleError,
}

handleList(w, r, cfg)
}

// Create creates or updates an adapter status for a resource.
// Verifies ownership when parent_id is present in the route.
func (h *ResourceStatusHandler) Create(w http.ResponseWriter, r *http.Request) {
var req openapi.AdapterStatusCreateRequest

cfg := &handlerConfig{
MarshalInto: &req,
Validate: []validate{
validateNotEmpty(&req, "Adapter", "adapter"),
validateObservedGeneration(&req),
validateConditions(&req, "Conditions"),
validateObservedTimeRange(&req.ObservedTime),
},
Action: func() (interface{}, *errors.ServiceError) {
ctx := r.Context()
id := mux.Vars(r)["id"]

if svcErr := h.verifyResource(r, id); svcErr != nil {
return nil, svcErr
}

return h.processStatus(ctx, id, &req)
},
ErrorHandler: handleError,
}

handleCreateWithNoContent(w, r, cfg)
}

// verifyResource confirms the resource exists. For nested routes (parent_id
// present), also verifies the resource belongs to the parent. No-op ownership
// check for flat routes.
func (h *ResourceStatusHandler) verifyResource(r *http.Request, id string) *errors.ServiceError {
ctx := r.Context()
if parentID, hasParent := mux.Vars(r)["parent_id"]; hasParent {
if _, err := h.resourceService.GetByOwner(ctx, h.descriptor.Kind, id, parentID); err != nil {
return err
}
} else {
if _, err := h.resourceService.Get(ctx, h.descriptor.Kind, id); err != nil {
return err
}
}
return nil
}

// listStatuses fetches paginated adapter statuses and presents them as an OpenAPI response.
func (h *ResourceStatusHandler) listStatuses(
ctx context.Context, resourceID string, listArgs *services.ListArguments,
) (interface{}, *errors.ServiceError) {
adapterStatuses, total, err := h.adapterStatusService.FindByResourcePaginated(
ctx, h.descriptor.Kind, resourceID, listArgs,
)
if err != nil {
return nil, err
}

items := make([]openapi.AdapterStatus, 0, len(adapterStatuses))
for _, as := range adapterStatuses {
presented, presErr := presenters.PresentAdapterStatus(as)
if presErr != nil {
logger.WithError(ctx, presErr).Error("Failed to present adapter status")
return nil, errors.GeneralError("Failed to present adapter status")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
items = append(items, presented)
}

return openapi.AdapterStatusList{
Items: items,
Page: int32(listArgs.Page),
Size: int32(len(items)),
Total: int32(total),
}, nil
}

// processStatus converts the request, delegates to ProcessAdapterStatus, and presents the result.
// Returns (nil, nil) when the status was silently discarded (triggers 204 No Content).
func (h *ResourceStatusHandler) processStatus(
ctx context.Context, resourceID string, req *openapi.AdapterStatusCreateRequest,
) (interface{}, *errors.ServiceError) {
newStatus, convErr := presenters.ConvertAdapterStatus(h.descriptor.Kind, resourceID, req)
if convErr != nil {
logger.WithError(ctx, convErr).Error("Failed to convert adapter status")
return nil, errors.GeneralError("Failed to convert adapter status")
}

adapterStatus, err := h.resourceService.ProcessAdapterStatus(
ctx, h.descriptor.Kind, resourceID, newStatus,
)
if err != nil {
return nil, err
}

if adapterStatus == nil {
return nil, nil
}

status, presErr := presenters.PresentAdapterStatus(adapterStatus)
if presErr != nil {
logger.WithError(ctx, presErr).Error("Failed to present adapter status")
return nil, errors.GeneralError("Failed to present adapter status")
}
return &status, nil
}
Loading