146 lines
3.9 KiB
Go
146 lines
3.9 KiB
Go
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
|
||
|
|
}
|