// Package model holds the generic, normalized representation of synchronized objects. // Field values are canonical strings or sorted string lists, so that two clusters // can be compared field by field and the result serialized to YAML without loss. package model import ( "fmt" "sort" "strings" "time" ) // Kind identifies an object type handled by an adapter. type Kind string const ( KindDatacenter Kind = "datacenter-options" KindZone Kind = "sdn-zone" KindVNet Kind = "sdn-vnet" KindSubnet Kind = "sdn-subnet" ) // Kinds lists the kinds in dependency order (creation order). var Kinds = []Kind{KindDatacenter, KindZone, KindVNet, KindSubnet} // Object is one configuration object of a cluster. // Field values are either string or []string (see Normalize). type Object struct { Kind Kind `yaml:"kind" json:"kind"` ID string `yaml:"id" json:"id"` Fields map[string]any `yaml:"fields" json:"fields"` } // Key is the unique identity of an object inside a cluster. func (o Object) Key() string { return string(o.Kind) + "/" + o.ID } // Snapshot is the exported state of one cluster (YAML, versionable in Git). type Snapshot struct { Cluster string `yaml:"cluster"` PVEVersion string `yaml:"pve_version,omitempty"` ExportedAt time.Time `yaml:"exported_at"` Objects []Object `yaml:"objects"` Warnings []string `yaml:"warnings,omitempty"` } // Sort orders objects by kind (dependency order) then by id. func Sort(objs []Object) { rank := map[Kind]int{} for i, k := range Kinds { rank[k] = i } sort.SliceStable(objs, func(i, j int) bool { if objs[i].Kind != objs[j].Kind { return rank[objs[i].Kind] < rank[objs[j].Kind] } return objs[i].ID < objs[j].ID }) } // Normalize turns a YAML/JSON decoded field map into canonical values: // scalars become strings, lists become sorted []string. func Normalize(fields map[string]any) map[string]any { out := make(map[string]any, len(fields)) for k, v := range fields { switch t := v.(type) { case []any: l := make([]string, 0, len(t)) for _, e := range t { l = append(l, scalar(e)) } sort.Strings(l) out[k] = l case []string: l := append([]string(nil), t...) sort.Strings(l) out[k] = l default: out[k] = scalar(v) } } return out } func scalar(v any) string { switch t := v.(type) { case nil: return "" case string: return t case bool: if t { return "1" } return "0" default: return fmt.Sprint(t) } } // Equal compares two canonical values. func Equal(a, b any) bool { la, oka := a.([]string) lb, okb := b.([]string) if oka || okb { if !oka || !okb || len(la) != len(lb) { return false } for i := range la { if la[i] != lb[i] { return false } } return true } return a == b } // Format renders a canonical value for humans. func Format(v any) string { if l, ok := v.([]string); ok { return "[" + strings.Join(l, ", ") + "]" } if v == nil { return "" } return fmt.Sprintf("%q", v) }