This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package reconciliation
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/itworx/pulse/internal/datasource"
|
||||
)
|
||||
|
||||
type Observation struct {
|
||||
SourceID, ExternalType, ExternalID, EntityType, CanonicalName, DisplayName string
|
||||
ObservedAt time.Time
|
||||
}
|
||||
|
||||
type Existing struct {
|
||||
EntityID string
|
||||
SourceID, ExternalType, ExternalID string
|
||||
CanonicalName, DisplayName string
|
||||
Tombstoned bool
|
||||
// TombstonedAt records when the entity was tombstoned. It is the zero time
|
||||
// while the entity is live and maps onto entities.tombstoned_at.
|
||||
TombstonedAt time.Time
|
||||
}
|
||||
type Override struct{ EntityID, FieldName, Value string }
|
||||
type Result struct {
|
||||
Entities []Existing
|
||||
Added, Updated, Tombstoned []string
|
||||
Aliases map[string]string
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
func StableEntityID(sourceID, externalType, externalID string) string {
|
||||
hash := sha256.Sum256([]byte("itworx-pulse/entity/v1/" + sourceID + "\x00" + externalType + "\x00" + externalID))
|
||||
b := hash[:16]
|
||||
b[6] = (b[6] & 0x0f) | 0x50
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%s-%s-%s-%s-%s", hex.EncodeToString(b[0:4]), hex.EncodeToString(b[4:6]), hex.EncodeToString(b[6:8]), hex.EncodeToString(b[8:10]), hex.EncodeToString(b[10:16]))
|
||||
}
|
||||
|
||||
func Reconcile(sourceID string, health datasource.HealthState, observations []Observation, previous []Existing, overrides []Override, now time.Time) (Result, error) {
|
||||
if strings.TrimSpace(sourceID) == "" {
|
||||
return Result{}, errors.New("source id is required")
|
||||
}
|
||||
if health == datasource.HealthUnknown || health == datasource.HealthDisabled {
|
||||
return Result{Entities: previous, Warnings: []string{"SOURCE_NOT_HEALTHY_NO_TOMBSTONES"}}, nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(observations))
|
||||
result := Result{Aliases: make(map[string]string)}
|
||||
overrideMap := make(map[string]string)
|
||||
for _, o := range overrides {
|
||||
overrideMap[o.EntityID+"\x00"+o.FieldName] = o.Value
|
||||
}
|
||||
previousByAlias := make(map[string]Existing)
|
||||
for _, e := range previous {
|
||||
previousByAlias[e.SourceID+"\x00"+e.ExternalType+"\x00"+e.ExternalID] = e
|
||||
}
|
||||
for _, o := range observations {
|
||||
if o.SourceID != sourceID || o.ExternalType == "" || o.ExternalID == "" || o.EntityType == "" || o.CanonicalName == "" || o.ObservedAt.IsZero() {
|
||||
return Result{}, errors.New("invalid observation")
|
||||
}
|
||||
key := o.SourceID + "\x00" + o.ExternalType + "\x00" + o.ExternalID
|
||||
if _, ok := seen[key]; ok {
|
||||
return Result{}, fmt.Errorf("duplicate observation %q", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
id := StableEntityID(o.SourceID, o.ExternalType, o.ExternalID)
|
||||
if old, ok := previousByAlias[key]; ok {
|
||||
id = old.EntityID
|
||||
}
|
||||
display := o.DisplayName
|
||||
if value, ok := overrideMap[id+"\x00displayName"]; ok {
|
||||
display = value
|
||||
}
|
||||
entity := Existing{EntityID: id, SourceID: o.SourceID, ExternalType: o.ExternalType, ExternalID: o.ExternalID, CanonicalName: o.CanonicalName, DisplayName: display}
|
||||
result.Entities = append(result.Entities, entity)
|
||||
result.Aliases[key] = id
|
||||
if _, ok := previousByAlias[key]; ok {
|
||||
result.Updated = append(result.Updated, id)
|
||||
} else {
|
||||
result.Added = append(result.Added, id)
|
||||
}
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
now = now.UTC()
|
||||
for _, old := range previous {
|
||||
key := old.SourceID + "\x00" + old.ExternalType + "\x00" + old.ExternalID
|
||||
if old.SourceID == sourceID {
|
||||
if _, ok := seen[key]; !ok && !old.Tombstoned {
|
||||
old.Tombstoned = true
|
||||
old.TombstonedAt = now
|
||||
result.Entities = append(result.Entities, old)
|
||||
result.Tombstoned = append(result.Tombstoned, old.EntityID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user