188 lines
5.1 KiB
Go
188 lines
5.1 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])
|
|
}
|
|
|
|
// Skip removes from a plan the changes of the kinds left out of a replication
|
|
// ("not synchronized" in the dry-run), then keeps what the remaining changes
|
|
// need as Filter does: a group to create for an account stays in the plan.
|
|
// keys narrows the plan to some objects first (empty: every object).
|
|
func Skip(p Plan, keys []string, skip []model.Kind) Plan {
|
|
if len(skip) == 0 {
|
|
return Filter(p, keys)
|
|
}
|
|
out := map[model.Kind]bool{}
|
|
for _, k := range skip {
|
|
out[k] = true
|
|
}
|
|
if len(keys) == 0 {
|
|
for _, c := range p.Changes {
|
|
keys = append(keys, c.Key())
|
|
}
|
|
for _, e := range p.Errors {
|
|
if k, ok := ErrorKey(e); ok {
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
}
|
|
var kept []string
|
|
for _, k := range keys {
|
|
if kind, _, _ := strings.Cut(k, "/"); !out[model.Kind(kind)] {
|
|
kept = append(kept, k)
|
|
}
|
|
}
|
|
if len(kept) == 0 {
|
|
return Plan{Client: p.Client, Warnings: p.Warnings}
|
|
}
|
|
return Filter(p, kept)
|
|
}
|