mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
e74fea4bbc
A guest's unified canonical ID hashed its node-scoped source ID (instance:node:vmid), so a live migration to another cluster node re-minted the resource and orphaned every operator-owned row keyed by the old ID: explicit availability check links (fail-closed by design, the reported symptom in #1669), alert overrides, operator state, action audits, manual links, and recovery subjects. VMIDs are unique within a cluster, so guests now derive their canonical ID from instance+VMID ("proxmox-guest:<instance>:<vmid>") and keep it across migrations. The guest-metadata half of #1669 was fixed separately at the metadata-store layer. Existing installs converge through record-declared succession: ingest declares the retired node-scoped IDs superseded for every node the instance currently knows (current names plus native aliases), so rows orphaned by pre-upgrade migrations also re-key. Successions are now recorded durably in a canonical_id_successions table, which memoizes the re-key (steady-state rebuilds re-declare the same eras every tick without touching SQL) and lets change-journal reads merge retired guest eras the way pin EraIDs do for hosts. The succession re-key also covers manual link/exclusion rows. Availability links resolve retired canonical IDs and old-node source triples through a registry superseded index plus guest-triple parsing (persistence keys only, ambiguity fails closed), and the stored LinkedResourceID re-homes to the current canonical ID on the alert-migration cadence. Recovery subjects converge on the same derivation via CanonicalSubjectResourceID, the mapper's registry-miss fallback, node-independent external guest keys, and the store's startup backfill, which also sweeps posture rows stranded under retired subject keys. Metrics history and frontend row identity key off the node-scoped source ID and are deliberately unchanged.
135 lines
4.3 KiB
Go
135 lines
4.3 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// migrateAvailabilityLinksToCanonicalIDs loads the availability target
|
|
// configuration, re-homes explicit links whose resource reference was retired
|
|
// by a canonical-ID succession or a guest live migration, and persists the
|
|
// result. Runs on the same cadence as the alert override migration; once
|
|
// every link references a live canonical ID it is a cheap no-op.
|
|
func (m *Monitor) migrateAvailabilityLinksToCanonicalIDs(resources []unifiedresources.Resource) {
|
|
if m == nil || m.configPersist == nil || len(resources) == 0 {
|
|
return
|
|
}
|
|
targets, err := m.configPersist.LoadAvailabilityTargets()
|
|
if err != nil || len(targets) == 0 {
|
|
return
|
|
}
|
|
migrated, changed := migrateAvailabilityLinkedResources(targets, resources)
|
|
if !changed {
|
|
return
|
|
}
|
|
if err := m.configPersist.SaveAvailabilityTargets(migrated); err != nil {
|
|
log.Error().Err(err).Msg("failed to persist canonical availability link migration")
|
|
return
|
|
}
|
|
log.Info().Msg("migrated availability links to current canonical resource IDs")
|
|
}
|
|
|
|
// migrateAvailabilityLinkedResources re-homes explicit availability links
|
|
// written under a retired canonical resource ID (or under a node-scoped guest
|
|
// source ID) onto the resource's current canonical ID. The explicit link is
|
|
// documented as authoritative and fail-closed, so only provider-declared
|
|
// persistence keys participate: a superseded canonical ID declared by exactly
|
|
// one live resource, or a Proxmox guest source reference whose instance+VMID
|
|
// matches exactly one live guest. Address or hostname coincidences never
|
|
// migrate a link.
|
|
//
|
|
// The returned slice shares unmodified entries with the input; the boolean
|
|
// reports whether any link changed.
|
|
func migrateAvailabilityLinkedResources(targets []config.AvailabilityTarget, resources []unifiedresources.Resource) ([]config.AvailabilityTarget, bool) {
|
|
if len(targets) == 0 || len(resources) == 0 {
|
|
return targets, false
|
|
}
|
|
|
|
hasLink := false
|
|
for _, target := range targets {
|
|
if strings.TrimSpace(target.LinkedResourceID) != "" {
|
|
hasLink = true
|
|
break
|
|
}
|
|
}
|
|
if !hasLink {
|
|
return targets, false
|
|
}
|
|
|
|
liveIDs := make(map[string]struct{}, len(resources))
|
|
successors := make(map[string]string)
|
|
ambiguous := make(map[string]struct{})
|
|
guestIDsByKey := make(map[string][]string)
|
|
|
|
for _, resource := range resources {
|
|
currentID := unifiedresources.CanonicalResourceID(resource.ID)
|
|
if currentID == "" {
|
|
continue
|
|
}
|
|
liveIDs[currentID] = struct{}{}
|
|
|
|
for _, supersededID := range resource.SupersededCanonicalIDs {
|
|
oldID := unifiedresources.CanonicalResourceID(supersededID)
|
|
if oldID == "" || oldID == currentID {
|
|
continue
|
|
}
|
|
if existing, ok := successors[oldID]; ok && existing != currentID {
|
|
delete(successors, oldID)
|
|
ambiguous[oldID] = struct{}{}
|
|
continue
|
|
}
|
|
if _, conflict := ambiguous[oldID]; conflict {
|
|
continue
|
|
}
|
|
successors[oldID] = currentID
|
|
}
|
|
|
|
if resource.Proxmox != nil && resource.Proxmox.VMID > 0 {
|
|
switch unifiedresources.CanonicalResourceType(resource.Type) {
|
|
case unifiedresources.ResourceTypeVM, unifiedresources.ResourceTypeSystemContainer:
|
|
key := unifiedresources.ProxmoxGuestIdentityKey(resource.Proxmox.Instance, resource.Proxmox.VMID)
|
|
if key != "" {
|
|
guestIDsByKey[key] = append(guestIDsByKey[key], currentID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
changed := false
|
|
out := targets
|
|
for i, target := range targets {
|
|
linkedID := strings.TrimSpace(target.LinkedResourceID)
|
|
if linkedID == "" {
|
|
continue
|
|
}
|
|
if _, live := liveIDs[linkedID]; live {
|
|
continue
|
|
}
|
|
|
|
newID := successors[linkedID]
|
|
if newID == "" {
|
|
if instance, _, vmid, ok := unifiedresources.ParseProxmoxGuestSourceID(linkedID); ok {
|
|
// Node segment deliberately ignored: the link must follow the
|
|
// guest across live migrations. Ambiguity fails closed.
|
|
if candidates := guestIDsByKey[unifiedresources.ProxmoxGuestIdentityKey(instance, vmid)]; len(candidates) == 1 {
|
|
newID = candidates[0]
|
|
}
|
|
}
|
|
}
|
|
if newID == "" || newID == linkedID {
|
|
continue
|
|
}
|
|
|
|
if !changed {
|
|
out = make([]config.AvailabilityTarget, len(targets))
|
|
copy(out, targets)
|
|
changed = true
|
|
}
|
|
out[i].LinkedResourceID = newID
|
|
}
|
|
return out, changed
|
|
}
|