Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

210 lines
7.2 KiB
Go

package reconciliation
import (
"errors"
"fmt"
"sort"
"strings"
"time"
)
const (
containerServiceExternalType = "container-service"
containerInstanceExternalType = "container-instance"
)
type ContainerObservation struct {
SourceID string
RuntimeID string
Name string
Project string
Service string
ImageDigest string
ObservedAt time.Time
}
type ContainerAlias struct {
EntityID string
SourceID string
RuntimeID string
Name string
Project string
Service string
ImageDigest string
FirstSeenAt time.Time
LastSeenAt time.Time
Active bool
// TombstonedAt records when the runtime alias stopped being observed. It is
// the zero time while the alias is active and is stamped once, so repeated
// snapshots keep the original tombstone moment.
TombstonedAt time.Time
}
type ContainerRecreation struct {
EntityID string
PreviousRuntimeID string
CurrentRuntimeID string
EventType string
EventEntityID string
OccurredAt time.Time
}
type ContainerIdentityResult struct {
Aliases []ContainerAlias
Current []ContainerAlias
Added []string
Updated []string
Recreated []ContainerRecreation
Warnings []string
}
func ReconcileContainers(observations []ContainerObservation, previous []ContainerAlias, now time.Time) (ContainerIdentityResult, error) {
if now.IsZero() {
now = time.Now().UTC()
}
now = now.UTC()
records := append([]ContainerAlias(nil), previous...)
for i := range records {
if records[i].SourceID == "" || records[i].RuntimeID == "" || records[i].EntityID == "" {
return ContainerIdentityResult{}, errors.New("invalid previous container alias")
}
records[i].FirstSeenAt = records[i].FirstSeenAt.UTC()
records[i].LastSeenAt = records[i].LastSeenAt.UTC()
records[i].TombstonedAt = records[i].TombstonedAt.UTC()
records[i].Active = false
}
exact := make(map[string][]int)
stable := make(map[string][]int)
for i, record := range records {
exact[containerAliasKey(record.SourceID, record.RuntimeID)] = append(exact[containerAliasKey(record.SourceID, record.RuntimeID)], i)
if key := containerStableKey(record.SourceID, record.Project, record.Service); key != "" {
stable[key] = append(stable[key], i)
}
}
seen := make(map[string]struct{}, len(observations))
result := ContainerIdentityResult{}
for _, observation := range observations {
if err := validateContainerObservation(observation); err != nil {
return ContainerIdentityResult{}, err
}
runtimeKey := containerAliasKey(observation.SourceID, observation.RuntimeID)
if _, ok := seen[runtimeKey]; ok {
return ContainerIdentityResult{}, fmt.Errorf("duplicate container observation %q", runtimeKey)
}
seen[runtimeKey] = struct{}{}
var record ContainerAlias
if candidates := exact[runtimeKey]; len(candidates) == 1 {
record = records[candidates[0]]
record.Active = true
record.Name = observation.Name
record.Project = observation.Project
record.Service = observation.Service
record.ImageDigest = observation.ImageDigest
record.LastSeenAt = observation.ObservedAt.UTC()
result.Updated = append(result.Updated, record.EntityID)
} else if key := containerStableKey(observation.SourceID, observation.Project, observation.Service); key != "" && len(stable[key]) == 1 {
old := records[stable[key][0]]
old.Active = false
record = ContainerAlias{
EntityID: old.EntityID, SourceID: observation.SourceID, RuntimeID: observation.RuntimeID,
Name: observation.Name, Project: observation.Project, Service: observation.Service,
ImageDigest: observation.ImageDigest, FirstSeenAt: old.FirstSeenAt, LastSeenAt: observation.ObservedAt.UTC(), Active: true,
}
records = append(records, record)
recreation := ContainerRecreation{
EntityID: record.EntityID, PreviousRuntimeID: old.RuntimeID, CurrentRuntimeID: record.RuntimeID,
EventType: "container.recreated", EventEntityID: record.EntityID, OccurredAt: observation.ObservedAt.UTC(),
}
result.Recreated = append(result.Recreated, recreation)
result.Updated = append(result.Updated, record.EntityID)
continue
} else {
externalType := containerInstanceExternalType
externalID := observation.RuntimeID
if key := containerStableKey(observation.SourceID, observation.Project, observation.Service); key != "" {
if len(stable[key]) > 1 {
result.Warnings = append(result.Warnings, "AMBIGUOUS_COMPOSE_IDENTITY_NO_MERGE")
} else {
externalType = containerServiceExternalType
externalID = observation.Project + "\x00" + observation.Service
}
}
entityID := StableEntityID(observation.SourceID, externalType, externalID)
record = ContainerAlias{
EntityID: entityID, SourceID: observation.SourceID, RuntimeID: observation.RuntimeID,
Name: observation.Name, Project: observation.Project, Service: observation.Service,
ImageDigest: observation.ImageDigest, FirstSeenAt: observation.ObservedAt.UTC(),
LastSeenAt: observation.ObservedAt.UTC(), Active: true,
}
records = append(records, record)
result.Added = append(result.Added, entityID)
continue
}
records = replaceAlias(records, record)
}
// A runtime alias that is no longer observed is tombstoned rather than
// deleted, and keeps the moment it was first found missing.
for i := range records {
if records[i].Active {
records[i].TombstonedAt = time.Time{}
} else if records[i].TombstonedAt.IsZero() {
records[i].TombstonedAt = now
}
}
sort.Strings(result.Added)
sort.Strings(result.Updated)
sort.Slice(result.Recreated, func(i, j int) bool {
if result.Recreated[i].EntityID != result.Recreated[j].EntityID {
return result.Recreated[i].EntityID < result.Recreated[j].EntityID
}
return result.Recreated[i].CurrentRuntimeID < result.Recreated[j].CurrentRuntimeID
})
sort.Strings(result.Warnings)
sort.Slice(records, func(i, j int) bool {
if records[i].EntityID != records[j].EntityID {
return records[i].EntityID < records[j].EntityID
}
return records[i].RuntimeID < records[j].RuntimeID
})
result.Aliases = records
for _, record := range records {
if record.Active {
result.Current = append(result.Current, record)
}
}
return result, nil
}
func validateContainerObservation(observation ContainerObservation) error {
if strings.TrimSpace(observation.SourceID) == "" || strings.TrimSpace(observation.RuntimeID) == "" || strings.TrimSpace(observation.Name) == "" || observation.ObservedAt.IsZero() {
return errors.New("invalid container observation")
}
if (strings.TrimSpace(observation.Project) == "") != (strings.TrimSpace(observation.Service) == "") {
return errors.New("compose project and service must be supplied together")
}
return nil
}
func containerAliasKey(sourceID, runtimeID string) string {
return sourceID + "\x00" + runtimeID
}
func containerStableKey(sourceID, project, service string) string {
if strings.TrimSpace(project) == "" || strings.TrimSpace(service) == "" {
return ""
}
return sourceID + "\x00" + project + "\x00" + service
}
func replaceAlias(records []ContainerAlias, updated ContainerAlias) []ContainerAlias {
for i := range records {
if records[i].EntityID == updated.EntityID && records[i].RuntimeID == updated.RuntimeID {
records[i] = updated
return records
}
}
return append(records, updated)
}