homelabby/internal/inventory/catalog_updater.go
Mikkel Georgsen 1f9621fcaa feat(01-04): quality gate state machine, tag sync, catalog updater
- CatalogStatus type with forward-only state machine (draft→indexed→…→complete)
- Transition() enforces valid transitions, returns error with 'invalid transition' message
- ParseCatalogStatus() validates known status strings
- HardwareRecord domain type composing netbox.Device with quality gate state
- CatalogUpdater.UpdateCatalogStatus() validates transition then PatchCustomFields
- SyncTags() normalizes tags (slug form) and ensures they exist in NetBox
- normalizeTags deduplicates across case/whitespace/space-vs-hyphen variants
- ensureTag uses go-netbox v4 NewTagRequest(name, slug) / ExtrasTagsCreate
- All 12 state machine table-driven tests pass
2026-04-10 05:22:22 +00:00

38 lines
1.2 KiB
Go

package inventory
import (
"context"
"fmt"
"git.georgsen.dk/hwlab/internal/netbox"
)
// CatalogUpdater persists quality gate transitions to NetBox.
type CatalogUpdater struct {
client *netbox.Client
}
// NewCatalogUpdater creates a CatalogUpdater backed by the given NetBox client.
func NewCatalogUpdater(client *netbox.Client) *CatalogUpdater {
return &CatalogUpdater{client: client}
}
// UpdateCatalogStatus validates the transition from current to next status
// and persists the result to NetBox via PatchCustomFields.
// Returns the new status on success.
//
// All catalog_status writes MUST go through this method to ensure T-04-01 mitigation:
// the quality gate transition is always validated before any NetBox PATCH.
func (u *CatalogUpdater) UpdateCatalogStatus(ctx context.Context, deviceID int, current, next CatalogStatus) (CatalogStatus, error) {
newStatus, err := Transition(current, next)
if err != nil {
return "", err
}
patch := map[string]interface{}{
"catalog_status": string(newStatus),
}
if err := u.client.PatchCustomFields(ctx, deviceID, patch); err != nil {
return "", fmt.Errorf("persist catalog_status to NetBox: %w", err)
}
return newStatus, nil
}