// Package adapter maps each object kind to its PVE API endpoints and knows // which fields are computed by PVE, which are specific to a cluster and how a // canonical value is written back. Paths and schemas: see docs/api-pve.md. package adapter import ( "context" "fmt" "net/url" "sort" "strings" "pve-confsync/internal/model" "pve-confsync/internal/pve" ) // Policy of one field. type Policy string const ( Sync Policy = "sync" // the reference value is copied to the clients Ignore Policy = "ignore" // never read nor written Override Policy = "override" // value comes from the client mapping only ) // ReadMode chooses which SDN configuration is read: the applied one (reference) // or the current one including pending changes (clients, which are written). type ReadMode int const ( ReadCurrent ReadMode = iota ReadRunning ) // Adapter handles one object kind. type Adapter interface { Kind() model.Kind // SDN reports whether writes must go through the SDN lock and reload. SDN() bool // List reads and normalizes every object of this kind. known holds the objects // already read for previous kinds (subnets need the vnets). List(ctx context.Context, c *pve.Client, mode ReadMode, known []model.Object) ([]model.Object, []string, error) // DefaultPolicy is the built-in policy of a field when the configuration says nothing. DefaultPolicy(field string) Policy // Forbidden fields are never synchronized, whatever the configuration says. Forbidden(field string) bool // Required lists the fields an object must have to be created. Required(obj model.Object) []string // Immutable fields cannot be changed by an update (re-creation needed). Immutable(field string) bool // Canonicalize puts user supplied values (mapping, YAML) in the canonical form of List. Canonicalize(fields map[string]any) // Singleton kinds have exactly one object that is never created nor deleted. Singleton() bool Create(ctx context.Context, c *pve.Client, obj model.Object, lock string) error // Update changes fields of an existing object (target carries the id and current fields). Update(ctx context.Context, c *pve.Client, target model.Object, set map[string]any, unset []string, lock string) error Delete(ctx context.Context, c *pve.Client, obj model.Object, lock string) error } // All returns the adapters in dependency order. func All() []Adapter { return []Adapter{datacenter{}, zone{}, vnet{}, subnet{}} } // ByKind returns the adapter of a kind. func ByKind(k model.Kind) (Adapter, error) { for _, a := range All() { if a.Kind() == k { return a, nil } } return nil, fmt.Errorf("unknown kind %q", k) } // canon converts one decoded API value into its canonical form: // objects become property strings with sorted keys, lists sorted []string. func canon(v any) any { switch t := v.(type) { case map[string]any: return propertyString(t) case []any: l := make([]string, 0, len(t)) for _, e := range t { if m, ok := e.(map[string]any); ok { l = append(l, propertyString(m)) } else { l = append(l, pve.Str(e)) } } sort.Strings(l) return l default: return pve.Str(v) } } // propertyString prints a PVE property string ("k1=v1,k2=v2"), keys sorted. // PVE parses property strings independently of the key order. func propertyString(m map[string]any) string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, k := range keys { v := m[k] if l, ok := v.([]any); ok { s := make([]string, 0, len(l)) for _, e := range l { s = append(s, pve.Str(e)) } sort.Strings(s) parts = append(parts, k+"="+strings.Join(s, ";")) continue } parts = append(parts, k+"="+pve.Str(v)) } return strings.Join(parts, ",") } // normalizeAPI builds the canonical field map of an API object, without the given keys. func normalizeAPI(raw map[string]any, drop ...string) map[string]any { skip := map[string]bool{"digest": true, "state": true, "pending": true} for _, d := range drop { skip[d] = true } out := map[string]any{} for k, v := range raw { if skip[k] || v == nil { continue } out[k] = canon(v) } return out } // form encodes canonical values for a write: lists become repeated parameters. func form(set map[string]any, unset []string, lock string) url.Values { f := url.Values{} for k, v := range set { switch t := v.(type) { case []string: for _, e := range t { f.Add(k, e) } default: f.Set(k, fmt.Sprint(t)) } } if len(unset) > 0 { u := append([]string(nil), unset...) sort.Strings(u) f.Set("delete", strings.Join(u, ",")) } if lock != "" { f.Set("lock-token", lock) } return f } func modeQuery(mode ReadMode) url.Values { if mode == ReadRunning { return url.Values{"running": {"1"}} } return nil }