package engine import ( "crypto/sha256" "encoding/hex" "encoding/json" "sort" "strings" "pve-confsync/internal/model" ) // Key is the identity of a planned change ("kind/id"), as model.Object.Key. func (c Change) Key() string { return string(c.Kind) + "/" + c.ID } // Filter keeps the changes of the given objects, plus the ones they depend on: // creating a vnet needs its zone when the zone is created too, a subnet needs its // vnet; deleting a zone needs the deletion of its vnets and their subnets first. // The execution order of the plan is kept. An empty key list keeps everything. func Filter(p Plan, keys []string) Plan { if len(keys) == 0 { return p } byKey := map[string]Change{} for _, c := range p.Changes { byKey[c.Key()] = c } want := map[string]bool{} for _, k := range keys { if _, ok := byKey[k]; ok { want[k] = true } } for changed := true; changed; { changed = false add := func(k string) { if _, ok := byKey[k]; ok && !want[k] { want[k] = true changed = true } } for k := range want { c := byKey[k] switch c.Op { case OpCreate, OpUpdate: // parents written in the same plan must exist first if z, ok := c.Set["zone"].(string); ok && byKey[string(model.KindZone)+"/"+z].Op == OpCreate { add(string(model.KindZone) + "/" + z) } if v, ok := c.Set["vnet"].(string); ok && byKey[string(model.KindVNet)+"/"+v].Op == OpCreate { add(string(model.KindVNet) + "/" + v) } case OpDelete: // children deleted in the same plan must go first for _, d := range p.Changes { if d.Op != OpDelete { continue } switch { case c.Kind == model.KindZone && d.Kind == model.KindVNet && d.Old["zone"] == c.ID, c.Kind == model.KindVNet && d.Kind == model.KindSubnet && d.Old["vnet"] == c.ID: add(d.Key()) } } } } } // an error on the parent of a selected object blocks it too (a vnet whose // zone cannot be created) blocking := map[string]bool{} for k := range want { c := byKey[k] if c.Op == OpDelete { continue } if z, ok := c.Set["zone"].(string); ok { blocking[string(model.KindZone)+"/"+z] = true } if v, ok := c.Set["vnet"].(string); ok { blocking[string(model.KindVNet)+"/"+v] = true } } out := Plan{Client: p.Client, Warnings: p.Warnings} // errors about other objects ("kind/id: ...") do not block this selection for _, e := range p.Errors { if k, ok := ErrorKey(e); !ok || want[k] || blocking[k] || contains(keys, k) { out.Errors = append(out.Errors, e) } } for _, c := range p.Changes { if want[c.Key()] { out.Changes = append(out.Changes, c) } } return out } // ErrorKey returns the object an error of MakePlan is about ("kind/id: ..."; // ": " and not ":" because IPv6 subnet ids hold colons). func ErrorKey(e string) (string, bool) { for _, k := range model.Kinds { if strings.HasPrefix(e, string(k)+"/") { if i := strings.Index(e, ": "); i > 0 { return e[:i], true } } } return "", false } func contains(l []string, s string) bool { for _, x := range l { if x == s { return true } } return false } // Digest identifies the content of a plan: the web UI shows a dry-run, then applies // only if the plan computed again just before the apply has the same digest, so // that what is written is exactly what the user reviewed. func (p Plan) Digest() string { type ch struct { Op Op `json:"op"` Key string `json:"key"` Set map[string]any `json:"set,omitempty"` Unset []string `json:"unset,omitempty"` } l := make([]ch, 0, len(p.Changes)) for _, c := range p.Changes { l = append(l, ch{c.Op, c.Key(), c.Set, c.Unset}) } errs := append([]string(nil), p.Errors...) sort.Strings(errs) // map keys are sorted by encoding/json: the digest is stable b, _ := json.Marshal(struct { Client string `json:"client"` Changes []ch `json:"changes"` Errors []string `json:"errors"` }{p.Client, l, errs}) sum := sha256.Sum256(b) return hex.EncodeToString(sum[:12]) }