Publish ITWorx Pulse source
Public source validation / validate (push) Failing after 3m8s

This commit is contained in:
ITWorx Pulse release export
2026-09-03 02:09:19 +02:00
commit bd774932d5
614 changed files with 77116 additions and 0 deletions
+398
View File
@@ -0,0 +1,398 @@
package array
import (
"context"
"errors"
"math"
"reflect"
"sort"
"strings"
"time"
)
const ContractVersion = "v1"
const (
StateOperational = "operational"
StateDegraded = "degraded"
StateMissing = "missing"
StateUnknown = "unknown"
Fresh = "fresh"
Stale = "stale"
Unavailable = "unavailable"
)
type Limits struct {
MaxMembers int
MaxHistory int
MaxErrors int
}
func (l Limits) withDefaults() Limits {
if l.MaxMembers == 0 {
l.MaxMembers = 64
}
if l.MaxHistory == 0 {
l.MaxHistory = 64
}
if l.MaxErrors == 0 {
l.MaxErrors = 20
}
return l
}
func (l Limits) Validate() error {
if l.MaxMembers < 1 || l.MaxMembers > 256 || l.MaxHistory < 1 || l.MaxHistory > 256 || l.MaxErrors < 1 || l.MaxErrors > 100 {
return errors.New("array limits are outside safe bounds")
}
return nil
}
type Policy struct{ FreshnessMaxAge time.Duration }
func (p Policy) withDefaults() Policy {
if p.FreshnessMaxAge == 0 {
p.FreshnessMaxAge = 60 * time.Second
}
return p
}
func (p Policy) Validate() error {
if p.FreshnessMaxAge <= 0 || p.FreshnessMaxAge > 24*time.Hour {
return errors.New("array policy is outside safe bounds")
}
return nil
}
type Source struct {
ID string `json:"id"`
Type string `json:"type"`
CapabilityVersion string `json:"capabilityVersion"`
ObservedAt time.Time `json:"observedAt"`
ReceivedAt time.Time `json:"receivedAt"`
Freshness string `json:"freshness"`
State string `json:"state"`
Reason string `json:"reason,omitempty"`
}
type RawMember struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
State string `json:"state"`
CapacityBytes uint64 `json:"capacityBytes"`
ReadBytes uint64 `json:"readBytes"`
WriteBytes uint64 `json:"writeBytes"`
}
type Member struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
State string `json:"state"`
CapacityBytes uint64 `json:"capacityBytes"`
ReadBytes uint64 `json:"readBytes"`
WriteBytes uint64 `json:"writeBytes"`
}
type RawParity struct {
Present bool `json:"present"`
State string `json:"state"`
Errors uint64 `json:"errors"`
}
type Parity struct {
Present bool `json:"present"`
State string `json:"state"`
Errors uint64 `json:"errors"`
}
type RawCheck struct {
ID string `json:"id"`
State string `json:"state"`
ProgressPercent float64 `json:"progressPercent"`
SpeedBytesPerSecond uint64 `json:"speedBytesPerSecond"`
Errors uint64 `json:"errors"`
StartedAt *time.Time `json:"startedAt,omitempty"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
}
type Check struct {
ID string `json:"id"`
State string `json:"state"`
ProgressPercent float64 `json:"progressPercent"`
SpeedBytesPerSecond uint64 `json:"speedBytesPerSecond"`
Errors uint64 `json:"errors"`
StartedAt *time.Time `json:"startedAt,omitempty"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
}
type RawSnapshot struct {
Source Source `json:"source"`
State string `json:"state"`
Parity RawParity `json:"parity"`
Members []RawMember `json:"members"`
CurrentCheck *RawCheck `json:"currentCheck,omitempty"`
History []RawCheck `json:"history,omitempty"`
ObservedAt time.Time `json:"observedAt"`
ReceivedAt time.Time `json:"receivedAt"`
}
type Snapshot struct {
ContractVersion string `json:"contractVersion"`
Source Source `json:"source"`
State string `json:"state"`
Parity Parity `json:"parity"`
Members []Member `json:"members"`
CurrentCheck *Check `json:"currentCheck,omitempty"`
History []Check `json:"history"`
ObservedAt time.Time `json:"observedAt"`
ReceivedAt time.Time `json:"receivedAt"`
}
type Provider interface {
Snapshot(context.Context) (Snapshot, error)
}
type RawProvider interface {
Snapshot(context.Context) (RawSnapshot, error)
}
type Adapter struct {
Source RawProvider
Limits Limits
Policy Policy
Now func() time.Time
}
func (a Adapter) Snapshot(ctx context.Context) (Snapshot, error) {
if err := ctx.Err(); err != nil {
return Snapshot{}, err
}
if a.Source == nil {
return UnknownSnapshot(time.Now().UTC(), "array", "unraid", "source_unavailable"), nil
}
raw, err := a.Source.Snapshot(ctx)
if err != nil {
return Snapshot{}, err
}
now := time.Now().UTC()
if a.Now != nil {
now = a.Now()
}
return Normalize(raw, now, a.Limits, a.Policy)
}
func UnknownSnapshot(now time.Time, id, typ, reason string) Snapshot {
if now.IsZero() {
now = time.Now().UTC()
}
return Snapshot{ContractVersion: ContractVersion, Source: Source{ID: id, Type: typ, CapabilityVersion: ContractVersion, ReceivedAt: now.UTC(), Freshness: Unavailable, State: StateUnknown, Reason: reason}, State: StateUnknown, Members: []Member{}, History: []Check{}, ObservedAt: now.UTC(), ReceivedAt: now.UTC()}
}
func Normalize(raw RawSnapshot, now time.Time, limits Limits, policy Policy) (Snapshot, error) {
limits = limits.withDefaults()
policy = policy.withDefaults()
if err := limits.Validate(); err != nil {
return Snapshot{}, err
}
if err := policy.Validate(); err != nil {
return Snapshot{}, err
}
if now.IsZero() {
now = time.Now().UTC()
}
if raw.ReceivedAt.IsZero() {
raw.ReceivedAt = now
}
if raw.ObservedAt.IsZero() {
raw.ObservedAt = raw.ReceivedAt
}
if raw.ObservedAt.After(now.Add(time.Minute)) {
return Snapshot{}, errors.New("array observation is materially in the future")
}
if len(raw.Members) > limits.MaxMembers || len(raw.History) > limits.MaxHistory {
return Snapshot{}, errors.New("array payload exceeds bounds")
}
if raw.CurrentCheck != nil {
if err := validateCheck(*raw.CurrentCheck); err != nil {
return Snapshot{}, err
}
}
for _, item := range raw.History {
if err := validateCheck(item); err != nil {
return Snapshot{}, err
}
}
members := make([]Member, 0, len(raw.Members))
seenMembers := make(map[string]RawMember, len(raw.Members))
missing := 0
for _, item := range raw.Members {
item.ID = canonicalIdentity(item.ID)
if previous, ok := seenMembers[item.ID]; ok {
if reflect.DeepEqual(previous, item) {
continue
}
return Snapshot{}, errors.New("conflicting duplicate array member identity")
}
seenMembers[item.ID] = item
if strings.TrimSpace(item.ID) == "" || strings.TrimSpace(item.Name) == "" || len(item.ID) > 128 || len(item.Name) > 255 {
return Snapshot{}, errors.New("array member identity is invalid")
}
state := bounded(item.State, StateUnknown)
role := bounded(item.Role, "data")
if state == "missing" || state == "disabled" || state == "emulated" {
missing++
}
members = append(members, Member{ID: item.ID, Name: item.Name, Role: role, State: state, CapacityBytes: item.CapacityBytes, ReadBytes: item.ReadBytes, WriteBytes: item.WriteBytes})
}
sort.Slice(members, func(i, j int) bool {
if members[i].Role != members[j].Role {
return members[i].Role < members[j].Role
}
if members[i].Name != members[j].Name {
return members[i].Name < members[j].Name
}
return members[i].ID < members[j].ID
})
parity := Parity{Present: raw.Parity.Present, State: bounded(raw.Parity.State, StateUnknown), Errors: raw.Parity.Errors}
state := bounded(raw.State, StateUnknown)
if state != StateOperational && state != StateDegraded && state != StateMissing && state != StateUnknown {
state = StateUnknown
}
if missing > 0 && state == StateOperational {
state = StateDegraded
}
if parity.Errors > 0 && state == StateOperational {
state = StateDegraded
}
source := raw.Source
if source.ID == "" {
source.ID = "array"
}
if source.Type == "" {
source.Type = "unraid"
}
if source.CapabilityVersion == "" {
source.CapabilityVersion = ContractVersion
}
source.ObservedAt = raw.ObservedAt.UTC()
source.ReceivedAt = raw.ReceivedAt.UTC()
source.Freshness = Fresh
source.State = state
if now.Sub(raw.ObservedAt) > policy.FreshnessMaxAge {
source.Freshness = Stale
source.State = StateUnknown
source.Reason = "stale_source"
state = StateUnknown
for i := range members {
members[i].State = StateUnknown
}
}
result := Snapshot{ContractVersion: ContractVersion, Source: source, State: state, Parity: parity, Members: members, ObservedAt: raw.ObservedAt.UTC(), ReceivedAt: raw.ReceivedAt.UTC()}
if raw.CurrentCheck != nil {
current := normalizeCheck(*raw.CurrentCheck)
result.CurrentCheck = &current
}
result.History = make([]Check, 0, len(raw.History))
for _, item := range raw.History {
result.History = append(result.History, normalizeCheck(item))
}
sort.SliceStable(result.History, func(i, j int) bool { return checkTime(result.History[i]).After(checkTime(result.History[j])) })
return result, nil
}
func canonicalIdentity(value string) string { return strings.ToLower(strings.TrimSpace(value)) }
func validateCheck(item RawCheck) error {
if strings.TrimSpace(item.ID) == "" || len(item.ID) > 128 || item.ProgressPercent < 0 || item.ProgressPercent > 100 || math.IsNaN(item.ProgressPercent) || math.IsInf(item.ProgressPercent, 0) {
return errors.New("invalid parity check")
}
if item.CompletedAt != nil && item.StartedAt != nil && item.CompletedAt.Before(*item.StartedAt) {
return errors.New("parity check completed before start")
}
return nil
}
func normalizeCheck(item RawCheck) Check {
result := Check{ID: item.ID, State: bounded(item.State, StateUnknown), ProgressPercent: item.ProgressPercent, SpeedBytesPerSecond: item.SpeedBytesPerSecond, Errors: item.Errors}
if item.StartedAt != nil {
value := item.StartedAt.UTC()
result.StartedAt = &value
}
if item.CompletedAt != nil {
value := item.CompletedAt.UTC()
result.CompletedAt = &value
}
return result
}
func checkTime(item Check) time.Time {
if item.CompletedAt != nil {
return *item.CompletedAt
}
if item.StartedAt != nil {
return *item.StartedAt
}
return time.Time{}
}
func bounded(value, fallback string) string {
value = strings.TrimSpace(value)
if value == "" {
return fallback
}
if len(value) > 64 {
return value[:64]
}
return value
}
type Event struct {
ID string `json:"id"`
Type string `json:"type"`
Severity string `json:"severity"`
EntityID string `json:"entityId"`
OccurredAt time.Time `json:"occurredAt"`
Attributes map[string]string `json:"attributes,omitempty"`
}
func TransitionEvents(previous, current Snapshot) []Event {
var events []Event
at := current.ObservedAt
if at.IsZero() {
at = current.ReceivedAt
}
if at.IsZero() {
at = time.Now().UTC()
}
at = at.UTC()
add := func(kind, severity string, attrs map[string]string) {
events = append(events, Event{ID: eventID(kind, current, at), Type: kind, Severity: severity, EntityID: "array", OccurredAt: at, Attributes: attrs})
}
if previous.State != current.State {
switch current.State {
case StateDegraded:
add("array.degraded", "warning", map[string]string{"state": current.State})
case StateMissing:
add("array.missing", "critical", map[string]string{"state": current.State})
case StateOperational:
if previous.State == StateDegraded || previous.State == StateMissing {
add("array.recovered", "info", map[string]string{"state": current.State})
}
}
}
if previous.Parity.State != current.Parity.State || previous.Parity.Errors != current.Parity.Errors {
severity := "info"
if current.Parity.Errors > 0 || current.Parity.State == "failed" {
severity = "critical"
}
add("array.parity_changed", severity, map[string]string{"state": current.Parity.State, "errors": formatUint(current.Parity.Errors)})
}
return events
}
func eventID(kind string, snapshot Snapshot, at time.Time) string {
return kind + ":" + snapshot.Source.ID + ":" + at.Format(time.RFC3339Nano)
}
func formatUint(value uint64) string {
if value == 0 {
return "0"
}
digits := make([]byte, 0, 20)
for value > 0 {
digits = append([]byte{byte('0' + value%10)}, digits...)
value /= 10
}
return string(digits)
}
+126
View File
@@ -0,0 +1,126 @@
package array
import (
"context"
"errors"
"testing"
"time"
)
type rawProvider struct {
snapshot RawSnapshot
err error
}
func (p rawProvider) Snapshot(context.Context) (RawSnapshot, error) { return p.snapshot, p.err }
func baseRaw(now time.Time) RawSnapshot {
return RawSnapshot{Source: Source{ID: "fixture-array", Type: "fixture"}, State: StateOperational, Parity: RawParity{Present: true, State: "idle"}, Members: []RawMember{{ID: "disk1", Name: "Disk 1", Role: "data", State: "online", CapacityBytes: 100}, {ID: "parity", Name: "Parity", Role: "parity", State: "online", CapacityBytes: 100}}, ObservedAt: now, ReceivedAt: now}
}
func TestNormalizeOperationalDegradedAndMissingFixtures(t *testing.T) {
now := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
operational, err := Normalize(baseRaw(now), now, Limits{}, Policy{})
if err != nil || operational.State != StateOperational || operational.Source.State != StateOperational {
t.Fatalf("operational=%+v err=%v", operational, err)
}
degradedRaw := baseRaw(now)
degradedRaw.Members[0].State = "emulated"
degraded, err := Normalize(degradedRaw, now, Limits{}, Policy{})
if err != nil || degraded.State != StateDegraded {
t.Fatalf("degraded=%+v err=%v", degraded, err)
}
missingRaw := baseRaw(now)
missingRaw.State = StateMissing
missingRaw.Members[0].State = "missing"
missing, err := Normalize(missingRaw, now, Limits{}, Policy{})
if err != nil || missing.State != StateMissing {
t.Fatalf("missing=%+v err=%v", missing, err)
}
}
func TestNormalizeProgressSpeedHistoryAndDeterministicOrder(t *testing.T) {
now := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
start := now.Add(-10 * time.Minute)
older := now.Add(-2 * time.Hour)
raw := baseRaw(now)
raw.Members = append(raw.Members, RawMember{ID: "disk0", Name: "Disk 0", Role: "data", State: "online"})
raw.CurrentCheck = &RawCheck{ID: "check-current", State: "running", ProgressPercent: 42.5, SpeedBytesPerSecond: 123456, StartedAt: &start}
raw.History = []RawCheck{{ID: "old", State: "completed", ProgressPercent: 100, CompletedAt: &older}, {ID: "new", State: "failed", ProgressPercent: 87, Errors: 2, CompletedAt: &start}}
got, err := Normalize(raw, now, Limits{}, Policy{})
if err != nil {
t.Fatal(err)
}
if got.CurrentCheck == nil || got.CurrentCheck.ProgressPercent != 42.5 || got.CurrentCheck.SpeedBytesPerSecond != 123456 {
t.Fatalf("current check=%+v", got.CurrentCheck)
}
if len(got.History) != 2 || got.History[0].ID != "new" {
t.Fatalf("history=%+v", got.History)
}
if got.Members[0].ID != "disk0" || got.Members[1].Role != "data" {
t.Fatalf("members=%+v", got.Members)
}
}
func TestNormalizeStaleIsUnknownAndContextCancellation(t *testing.T) {
now := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
raw := baseRaw(now.Add(-2 * time.Minute))
got, err := Normalize(raw, now, Limits{}, Policy{FreshnessMaxAge: time.Minute})
if err != nil || got.State != StateUnknown || got.Source.State != StateUnknown || got.Source.Freshness != Stale || got.Members[0].State != StateUnknown {
t.Fatalf("stale=%+v err=%v", got, err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err = (Adapter{}).Snapshot(ctx)
if !errors.Is(err, context.Canceled) {
t.Fatalf("err=%v", err)
}
}
func TestTransitionEventsAreBoundedAndDeterministic(t *testing.T) {
at := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
previous := UnknownSnapshot(at, "fixture-array", "fixture", "initial")
current := previous
current.ObservedAt = at
current.Source.State = StateDegraded
current.State = StateDegraded
current.Parity.State = "failed"
current.Parity.Errors = 3
events := TransitionEvents(previous, current)
if len(events) != 2 {
t.Fatalf("events=%+v", events)
}
if events[0].Type != "array.degraded" || events[1].Type != "array.parity_changed" {
t.Fatalf("events=%+v", events)
}
if events[0].ID == "" || !events[1].OccurredAt.Equal(at) {
t.Fatalf("events=%+v", events)
}
}
func TestAdapterUnknownFallbackAndProviderError(t *testing.T) {
now := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
unknown, err := (Adapter{Now: func() time.Time { return now }}).Snapshot(context.Background())
if err != nil || unknown.State != StateUnknown || unknown.Source.Reason != "source_unavailable" {
t.Fatalf("unknown=%+v err=%v", unknown, err)
}
expected := errors.New("fixture")
_, err = (Adapter{Source: rawProvider{err: expected}}).Snapshot(context.Background())
if !errors.Is(err, expected) {
t.Fatalf("err=%v", err)
}
}
func TestArrayMembersDeduplicateByCanonicalPhysicalIdentity(t *testing.T) {
now := time.Date(2026, 8, 11, 23, 45, 0, 0, time.UTC)
raw := baseRaw(now)
raw.Members = []RawMember{{ID: " DISK-1 ", Name: "disk1", Role: "data", State: "online", CapacityBytes: 100}, {ID: " DISK-1 ", Name: "disk1", Role: "data", State: "online", CapacityBytes: 100}}
got, err := Normalize(raw, now, Limits{}, Policy{})
if err != nil || len(got.Members) != 1 || got.Members[0].ID != "disk-1" {
t.Fatalf("members=%+v err=%v", got.Members, err)
}
raw.Members[1].Role = "parity"
if _, err := Normalize(raw, now, Limits{}, Policy{}); err == nil {
t.Fatal("conflicting physical roles must fail closed")
}
}