mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 12:15:42 +00:00
e8455e84b7
Canonical IDs for merged-source hosts (PVE node + pulse-agent) were minted from whichever identity keys the creating record happened to carry: the agent record knows the machine ID, the Proxmox node record only knows cluster+hostname. The registry rebuilds from scratch every tick, so a boot window where the agent had not checked in yet minted a cluster-keyed ID (agent-7a62... for delly) while steady state minted a machine-keyed one (agent-bdd4...). Every restart re-ran the race, fragmenting the resource_changes journal into per-boot eras (9.4k vs 6.1k rows for the same host) and silently truncating report availability and UI timelines. Fix, in the layer that owns identity: - Persist identity pins (canonical_id <-> machine_id/dmi/cluster/hostname) in the previously schema-only resource_identities table, written by the store-backed registry after monitor-adapter rebuilds, diff-aware so steady-state ticks cost no writes. - Complete weak incoming identities from the pins before matching and ID derivation, so a node-only boot window derives the same machine-keyed canonical ID as steady state. Derivation itself is unchanged; ephemeral nil-store registries behave exactly as before. - Expand change-journal reads (Get/Count families, SQLite and memory) to the full era set recomputed from the pinned identity keys, healing historical journals at query time with no row migration. Reads keyed by a stale era ID resolve to the same merged timeline. Regression tests cover both ingest orders, restart simulation via the monitor adapter, era ID derivation, and era-merged journal reads on both store implementations. Contracts updated: unified-resources obligation 25 (durable identity pins), monitoring obligation 10 (adapter rebuild persistence).
115 lines
4.2 KiB
Go
115 lines
4.2 KiB
Go
package unifiedresources
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
func normalizeSourceID(sourceID string) string {
|
|
return strings.TrimSpace(sourceID)
|
|
}
|
|
|
|
// SourceSpecificID returns the deterministic ID used for non-host resources when the registry
|
|
// does not have a canonical identity to key off of.
|
|
//
|
|
// This matches the ResourceRegistry's internal ID derivation:
|
|
// stable := fmt.Sprintf("%s:%s", source, sourceID)
|
|
// id := fmt.Sprintf("%s-%s", resourceType, hex(sha256(stable)[:8]))
|
|
func SourceSpecificID(resourceType ResourceType, source DataSource, sourceID string) string {
|
|
resourceType = CanonicalResourceType(resourceType)
|
|
stable := fmt.Sprintf("%s:%s", source, normalizeSourceID(sourceID))
|
|
hash := sha256.Sum256([]byte(stable))
|
|
return fmt.Sprintf("%s-%s", resourceType, hex.EncodeToString(hash[:8]))
|
|
}
|
|
|
|
// CanonicalResourceID returns the canonical v6 resource identifier.
|
|
func CanonicalResourceID(id string) string {
|
|
trimmed := strings.TrimSpace(id)
|
|
if trimmed == "" {
|
|
return ""
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
// ResourceIdentityPin is the durable identity→canonical-ID record persisted in
|
|
// the resource store (resource_identities table). It exists because canonical
|
|
// ID derivation keys off the strongest identity field known at mint time, and
|
|
// merged-source hosts (PVE node + pulse-agent) expose different identity
|
|
// subsets depending on which records are present when the registry rebuilds:
|
|
// the agent record knows the machine ID, the Proxmox node record only knows
|
|
// cluster+hostname. Pins let a rebuild that only sees the weak subset recover
|
|
// the strong keys, so the same physical host derives the same canonical ID in
|
|
// every boot, in every registry instance.
|
|
type ResourceIdentityPin struct {
|
|
CanonicalID string
|
|
ResourceType ResourceType
|
|
MachineID string
|
|
DMIUUID string
|
|
ClusterName string
|
|
// Hostname is the normalized primary hostname (NormalizeHostname).
|
|
Hostname string
|
|
}
|
|
|
|
func (p ResourceIdentityPin) normalized() ResourceIdentityPin {
|
|
p.CanonicalID = CanonicalResourceID(p.CanonicalID)
|
|
p.ResourceType = CanonicalResourceType(p.ResourceType)
|
|
p.MachineID = strings.TrimSpace(p.MachineID)
|
|
p.DMIUUID = strings.TrimSpace(p.DMIUUID)
|
|
p.ClusterName = strings.TrimSpace(p.ClusterName)
|
|
p.Hostname = NormalizeHostname(p.Hostname)
|
|
return p
|
|
}
|
|
|
|
func (p ResourceIdentityPin) hasStrongKey() bool {
|
|
return p.MachineID != "" || p.DMIUUID != "" || (p.ClusterName != "" && p.Hostname != "")
|
|
}
|
|
|
|
// EraIDs returns every canonical ID this pin's identity keys derive under the
|
|
// historical chooseNewID ladder (machine > dmi > cluster+hostname > hostname).
|
|
// Change-journal rows written in boots that only knew a weaker key sit under
|
|
// the weaker key's hash; expanding a read to the full era set merges those
|
|
// journal eras without rewriting history.
|
|
func (p ResourceIdentityPin) EraIDs() []string {
|
|
p = p.normalized()
|
|
ids := make([]string, 0, 5)
|
|
if p.CanonicalID != "" {
|
|
ids = append(ids, p.CanonicalID)
|
|
}
|
|
if p.MachineID != "" {
|
|
ids = append(ids, buildHashID(p.ResourceType, "machine:"+p.MachineID))
|
|
}
|
|
if p.DMIUUID != "" {
|
|
ids = append(ids, buildHashID(p.ResourceType, "dmi:"+p.DMIUUID))
|
|
}
|
|
if p.Hostname != "" {
|
|
if p.ClusterName != "" {
|
|
ids = append(ids, buildHashID(p.ResourceType, fmt.Sprintf("cluster:%s:%s", p.ClusterName, p.Hostname)))
|
|
}
|
|
ids = append(ids, buildHashID(p.ResourceType, "hostname:"+p.Hostname))
|
|
}
|
|
return uniqueTrimmed(ids...)
|
|
}
|
|
|
|
// identityPinForResource derives the persistable pin for a canonical resource.
|
|
// Only host resources with at least one strong identity key are pinned; a
|
|
// hostname-only host has a single possible derivation and needs no pin.
|
|
func identityPinForResource(resource *Resource) (ResourceIdentityPin, bool) {
|
|
if resource == nil || CanonicalResourceType(resource.Type) != ResourceTypeAgent {
|
|
return ResourceIdentityPin{}, false
|
|
}
|
|
pin := ResourceIdentityPin{
|
|
CanonicalID: resource.ID,
|
|
ResourceType: ResourceTypeAgent,
|
|
MachineID: resource.Identity.MachineID,
|
|
DMIUUID: resource.Identity.DMIUUID,
|
|
ClusterName: resource.Identity.ClusterName,
|
|
Hostname: firstIdentityHostname(resource.Identity),
|
|
}.normalized()
|
|
if pin.CanonicalID == "" || !pin.hasStrongKey() {
|
|
return ResourceIdentityPin{}, false
|
|
}
|
|
return pin, true
|
|
}
|