package datasource import ( "context" "errors" "fmt" "strings" "time" ) const ContractVersion = "v1" type SourceType string const ( SourcePrometheus SourceType = "prometheus" SourceUnraid SourceType = "unraid" SourceAgent SourceType = "agent" SourceExporter SourceType = "exporter" ) func (s SourceType) Valid() bool { switch s { case SourcePrometheus, SourceUnraid, SourceAgent, SourceExporter: return true default: return false } } type HealthState string const ( HealthHealthy HealthState = "healthy" HealthDegraded HealthState = "degraded" HealthUnknown HealthState = "unknown" HealthDisabled HealthState = "disabled" ) type CapabilityState string const ( CapabilityEnabled CapabilityState = "enabled" CapabilityUnsupported CapabilityState = "unsupported" CapabilityUnavailable CapabilityState = "unavailable" CapabilityDisabled CapabilityState = "disabled" ) type FreshnessPolicy struct { MaxAge time.Duration } func (p FreshnessPolicy) Validate() error { if p.MaxAge <= 0 || p.MaxAge > 24*time.Hour { return errors.New("freshness max age must be between 1 second and 24 hours") } return nil } type SourceHealth struct { State HealthState ObservedAt time.Time ReceivedAt time.Time LastSuccess time.Time Policy FreshnessPolicy ReasonCode string } func (h SourceHealth) Validate(now time.Time) error { if h.State != HealthHealthy && h.State != HealthDegraded && h.State != HealthUnknown && h.State != HealthDisabled { return fmt.Errorf("invalid health state %q", h.State) } if err := h.Policy.Validate(); err != nil { return err } if h.ReceivedAt.IsZero() { return errors.New("received timestamp is required") } if h.ObservedAt.IsZero() && h.State == HealthHealthy { return errors.New("healthy source requires observed timestamp") } if now.IsZero() { now = time.Now().UTC() } if !h.ObservedAt.IsZero() && h.ObservedAt.After(now.Add(time.Minute)) { return errors.New("observed timestamp cannot be materially in the future") } return nil } func (h SourceHealth) Fresh(now time.Time) bool { if h.State == HealthDisabled || h.State == HealthUnknown || h.ObservedAt.IsZero() || h.Policy.MaxAge <= 0 { return false } if now.IsZero() { now = time.Now().UTC() } return !h.ObservedAt.Before(now.Add(-h.Policy.MaxAge)) } func (h SourceHealth) EffectiveState(now time.Time) HealthState { if h.State == HealthHealthy && !h.Fresh(now) { return HealthUnknown } return h.State } type Capability struct { ID string Version string State CapabilityState Description string ReasonCode string ObservedAt time.Time } func (c Capability) Validate() error { if strings.TrimSpace(c.ID) == "" || len(c.ID) > 120 { return errors.New("capability id must be 1-120 characters") } if strings.TrimSpace(c.Version) == "" || len(c.Version) > 32 { return errors.New("capability version must be 1-32 characters") } switch c.State { case CapabilityEnabled, CapabilityUnsupported, CapabilityUnavailable, CapabilityDisabled: default: return fmt.Errorf("invalid capability state %q", c.State) } if len(c.Description) > 500 || len(c.ReasonCode) > 80 { return errors.New("capability text exceeds bounds") } return nil } type CapabilitySet []Capability func (s CapabilitySet) Validate() error { if len(s) > 100 { return errors.New("capability set exceeds 100 entries") } seen := make(map[string]struct{}, len(s)) for _, capability := range s { if err := capability.Validate(); err != nil { return err } key := capability.ID + "@" + capability.Version if _, exists := seen[key]; exists { return fmt.Errorf("duplicate capability %q", key) } seen[key] = struct{}{} } return nil } func (s CapabilitySet) Find(id string) (Capability, bool) { for _, capability := range s { if capability.ID == id { return capability, true } } return Capability{}, false } type Source struct { ID string Name string Type SourceType Enabled bool Contract string Health SourceHealth Capabilities CapabilitySet } func (s Source) Validate(now time.Time) error { if strings.TrimSpace(s.ID) == "" || len(s.ID) > 120 || strings.TrimSpace(s.Name) == "" || len(s.Name) > 255 { return errors.New("source id and name are required and bounded") } if !s.Type.Valid() { return fmt.Errorf("invalid source type %q", s.Type) } if s.Contract != ContractVersion { return fmt.Errorf("unsupported datasource contract %q", s.Contract) } if err := s.Health.Validate(now); err != nil { return err } return s.Capabilities.Validate() } type DiscoveryResult struct { Source Source } type Entity struct { ExternalType string ExternalID string EntityType string CanonicalName string DisplayName string Status string ObservedAt time.Time } type InventoryResult struct { Entities []Entity } type MetricBinding struct { SemanticName string Version string CapabilityID string } type Event struct { Type string Summary string OccurredAt time.Time } type Snapshot struct { Source Source Inventory InventoryResult MetricBindings []MetricBinding Events []Event } type Adapter interface { Discover(context.Context) (DiscoveryResult, error) Health(context.Context) (SourceHealth, error) Inventory(context.Context) (InventoryResult, error) MetricsBindings(context.Context) ([]MetricBinding, error) Events(context.Context) ([]Event, error) Capabilities(context.Context) (CapabilitySet, error) }