128 lines
3.0 KiB
Go
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)
|
|
}
|