Files

128 lines
3.0 KiB
Go

// 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 "<absent>"
}
return fmt.Sprintf("%q", v)
}