Public source validation / validate (push) Failing after 3m8s
235 lines
5.4 KiB
Go
235 lines
5.4 KiB
Go
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)
|
|
}
|