169 lines
4.8 KiB
Go
169 lines
4.8 KiB
Go
// 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
|
|
}
|