118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
package history
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha1"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type RegisterManualComponentFailureCommand struct {
|
|
ComponentID string
|
|
MachineID *string
|
|
FailureTime time.Time
|
|
Description *string
|
|
FailureType string
|
|
FailureSource string
|
|
FailureExternal string
|
|
SourceRef *string
|
|
IdempotencyKey *string
|
|
}
|
|
|
|
type RegisterManualComponentFailureResult struct {
|
|
HistoryApply ApplyPatchResult `json:"history_apply"`
|
|
FailureEventID string `json:"failure_event_id"`
|
|
FailureExternal string `json:"failure_external_id"`
|
|
}
|
|
|
|
func (s *Service) RegisterManualComponentFailure(ctx context.Context, cmd RegisterManualComponentFailureCommand) (RegisterManualComponentFailureResult, error) {
|
|
if s == nil || s.db == nil {
|
|
return RegisterManualComponentFailureResult{}, fmt.Errorf("history service unavailable")
|
|
}
|
|
componentID := strings.TrimSpace(cmd.ComponentID)
|
|
if componentID == "" {
|
|
return RegisterManualComponentFailureResult{}, fmt.Errorf("%w: component_id is required", ErrInvalidPatch)
|
|
}
|
|
failureAt := cmd.FailureTime.UTC()
|
|
if failureAt.IsZero() {
|
|
return RegisterManualComponentFailureResult{}, fmt.Errorf("%w: failure_time is required", ErrInvalidPatch)
|
|
}
|
|
failureType := strings.TrimSpace(cmd.FailureType)
|
|
if failureType == "" {
|
|
failureType = "component_failed_manual"
|
|
}
|
|
failureSource := strings.TrimSpace(cmd.FailureSource)
|
|
if failureSource == "" {
|
|
failureSource = "manual_ui"
|
|
}
|
|
failureExternal := strings.TrimSpace(cmd.FailureExternal)
|
|
if failureExternal == "" {
|
|
failureExternal = buildManualFailureExternalID(componentID, failureAt, cmd.Description)
|
|
}
|
|
|
|
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
|
|
if err != nil {
|
|
return RegisterManualComponentFailureResult{}, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
patch := []PatchOp{
|
|
{Op: "replace", Path: "/runtime/health_status", Value: "FAILED"},
|
|
{Op: "replace", Path: "/runtime/health_status_at", Value: failureAt.Format(time.RFC3339Nano)},
|
|
}
|
|
applyRes, err := s.ApplyComponentPatchWithTx(ctx, tx, ApplyPatchCommand{
|
|
EntityID: componentID,
|
|
ChangeType: "COMPONENT_STATUS_SET",
|
|
EffectiveAt: &failureAt,
|
|
SourceType: "user",
|
|
SourceRef: cmd.SourceRef,
|
|
ActorType: "user",
|
|
IdempotencyKey: cmd.IdempotencyKey,
|
|
Patch: patch,
|
|
})
|
|
if err != nil {
|
|
return RegisterManualComponentFailureResult{}, err
|
|
}
|
|
|
|
if err := s.UpsertFailureProjectionWithTx(ctx, tx, FailureProjectionInput{
|
|
Source: failureSource,
|
|
ExternalID: failureExternal,
|
|
PartID: componentID,
|
|
MachineID: normalizeStringPtr(cmd.MachineID),
|
|
FailureType: failureType,
|
|
FailureTime: failureAt,
|
|
Details: normalizeStringPtr(cmd.Description),
|
|
}); err != nil {
|
|
return RegisterManualComponentFailureResult{}, err
|
|
}
|
|
|
|
var failureEventID string
|
|
if err := tx.QueryRowContext(ctx,
|
|
`SELECT id FROM failure_events WHERE source = ? AND external_id = ?`,
|
|
failureSource, failureExternal,
|
|
).Scan(&failureEventID); err != nil {
|
|
return RegisterManualComponentFailureResult{}, err
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return RegisterManualComponentFailureResult{}, err
|
|
}
|
|
return RegisterManualComponentFailureResult{
|
|
HistoryApply: applyRes,
|
|
FailureEventID: failureEventID,
|
|
FailureExternal: failureExternal,
|
|
}, nil
|
|
}
|
|
|
|
func buildManualFailureExternalID(componentID string, failureAt time.Time, description *string) string {
|
|
normDesc := ""
|
|
if description != nil {
|
|
normDesc = strings.TrimSpace(*description)
|
|
}
|
|
sum := sha1.Sum([]byte(strings.ToLower(componentID) + "|" + failureAt.UTC().Format(time.RFC3339Nano) + "|" + strings.ToLower(normDesc)))
|
|
return "manual_ui:" + componentID + ":" + failureAt.UTC().Format(time.RFC3339Nano) + ":" + hex.EncodeToString(sum[:4])
|
|
}
|