Files
pve-confsync/internal/engine/select.go
T
2026-10-23 17:43:20 +02:00

154 lines
4.2 KiB
Go

package engine
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"sort"
"strings"
"pve-confsync/internal/adapter"
"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)
}
for _, p := range adapter.Parents(Desired(c)) {
if byKey[p].Op == OpCreate {
add(p)
}
}
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
}
for _, p := range adapter.Parents(Desired(c)) {
blocking[p] = 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])
}