package client import "sort" // Action is one step of a sync plan. type Action string // The six sync actions. skip warns about managed items with local // modifications; leave warns about self-created items agent-lib never // managed; remove only ever touches unmodified managed copies. const ( ActDeploy Action = "deploy" ActUpdate Action = "update" ActSkip Action = "skip+warn" ActLeave Action = "leave+warn" ActRemove Action = "remove" ActKeep Action = "keep" ) // PlannedItem is one deployable inventory item with its repo-side hash. type PlannedItem struct { Type string Name string Origin string RelPath string Hash string } // DiskFact is the observed on-disk state of one deployed path. type DiskFact struct { Exists bool Hash string } // PlanEntry is one decided action with everything the executor needs. type PlanEntry struct { Action Action Type string Name string Origin string RelPath string DstPath string NewHash string Reason string } // Plan is a pure function from (inventory, manifest, on-disk facts) to an // action plan. pathFor injects the target path mapping so the planner never // touches the filesystem or the environment. The plan is sorted by // (type, name) for deterministic execution and reporting. func Plan(items []PlannedItem, manifest *Manifest, disk map[string]DiskFact, pathFor func(typ, name string) string) []PlanEntry { invByKey := map[string]PlannedItem{} for _, it := range items { invByKey[manifestKey(it.Type, it.Name)] = it } var plan []PlanEntry emit := func(a Action, it PlannedItem, reason string) { e := PlanEntry{Action: a, Reason: reason, NewHash: it.Hash} e.Type, e.Name, e.Origin, e.RelPath = it.Type, it.Name, it.Origin, it.RelPath e.DstPath = pathFor(e.Type, e.Name) plan = append(plan, e) } keys := map[string]bool{} for key := range invByKey { keys[key] = true } for key := range manifest.Items { keys[key] = true } for key, fact := range disk { if fact.Exists { keys[key] = true } } sorted := make([]string, 0, len(keys)) for key := range keys { sorted = append(sorted, key) } sort.Strings(sorted) for _, key := range sorted { it, inInv := invByKey[key] rec := manifest.Items[key] fact := disk[key] switch { case inInv && rec == nil: if fact.Exists { emit(ActSkip, it, "path occupied by a local item agent-lib never deployed; deploy blocked") } else { emit(ActDeploy, it, "") } case inInv && it.Hash == rec.Hash: decideAgainstDisk(it, rec, fact, emit, "unchanged") case inInv: decideAgainstDisk(it, rec, fact, emit, "changed") case rec != nil && !fact.Exists: emit(ActRemove, PlannedItem{Type: rec.Type, Name: rec.Name, Origin: rec.Origin}, "already gone from disk; dropping manifest record") case rec != nil && fact.Hash == rec.Hash: emit(ActRemove, PlannedItem{Type: rec.Type, Name: rec.Name, Origin: rec.Origin}, "upstream deleted") case rec != nil: emit(ActSkip, PlannedItem{Type: rec.Type, Name: rec.Name, Origin: rec.Origin}, "upstream deleted but the local copy was modified; keeping it") default: emit(ActLeave, PlannedItem{Type: typeOfKey(key), Name: nameOfKey(key)}, "unmanaged local item; left untouched") } } return plan } func decideAgainstDisk(it PlannedItem, rec *ManifestItem, fact DiskFact, emit func(Action, PlannedItem, string), upstreamState string) { switch { case !fact.Exists: emit(ActUpdate, it, "managed item missing on disk; restoring") case fact.Hash == rec.Hash: if upstreamState == "unchanged" { emit(ActKeep, it, "") } else { emit(ActUpdate, it, "") } default: emit(ActSkip, it, "locally modified; keeping the local version (upstream "+upstreamState+")") } } func typeOfKey(key string) string { for i := len(key) - 1; i >= 0; i-- { if key[i] == '/' { return key[:i] } } return key } func nameOfKey(key string) string { for i := len(key) - 1; i >= 0; i-- { if key[i] == '/' { return key[i+1:] } } return key }