Files
pve-confsync/internal/adapter/adapter.go
T

223 lines
6.2 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"
"crypto/sha256"
"encoding/hex"
"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{}, group{}, role{}, user{}, password{}, zone{}, vnet{}, subnet{}}
}
// KindDeps: replicating a kind needs these kinds too (a password needs its
// account on the target, an account its groups).
var KindDeps = map[model.Kind][]model.Kind{
model.KindPassword: {model.KindUser},
model.KindUser: {model.KindGroup},
}
// Sensitive is implemented by adapters whose fields must never be shown nor
// journaled (password hashes): they are replaced by Redact.
type Sensitive interface {
Sensitive(field string) bool
}
// Parented is implemented by adapters whose objects need other objects on the
// target ("kind/id" keys): an account needs its groups, a password its account.
type Parented interface {
Parents(o model.Object) []string
}
// IsSensitive tells whether a field of a kind is secret.
func IsSensitive(k model.Kind, field string) bool {
a, err := ByKind(k)
if err != nil {
return false
}
s, ok := a.(Sensitive)
return ok && s.Sensitive(field)
}
// Redact replaces a secret value by a short digest: two different hashes stay
// distinguishable in a diff, the value itself is never shown.
func Redact(v any) any {
s, ok := v.(string)
if !ok || s == "" {
return v
}
sum := sha256.Sum256([]byte(s))
return "<masqué " + hex.EncodeToString(sum[:3]) + ">"
}
// Parents returns the keys of the objects o needs on the target.
func Parents(o model.Object) []string {
a, err := ByKind(o.Kind)
if err != nil {
return nil
}
if p, ok := a.(Parented); ok {
return p.Parents(o)
}
return nil
}
// 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
}