217 lines
5.8 KiB
Go
217 lines
5.8 KiB
Go
// Package config is the synchronization configuration given to the engine:
|
|
// the primary cluster, the targets with their mappings and policies, the
|
|
// replicated kinds and the object selection. It is built from the database of
|
|
// the web UI (internal/store); credentials are decrypted only in memory.
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"path"
|
|
"time"
|
|
|
|
"pve-confsync/internal/adapter"
|
|
"pve-confsync/internal/model"
|
|
"pve-confsync/internal/pve"
|
|
"pve-confsync/internal/sshx"
|
|
)
|
|
|
|
// Cluster is the connection to one PVE cluster: the API with the root@pam
|
|
// credentials and SSH as root on one node for what the API does not cover.
|
|
type Cluster struct {
|
|
Name string
|
|
URL string
|
|
User string // API user, root@pam
|
|
Password string // decrypted, never logged
|
|
Fingerprint string // pinned TLS certificate (SHA-256)
|
|
CAPEM string // CA bundle, PEM
|
|
Insecure bool
|
|
Timeout string
|
|
SSHHost string // empty: the host of URL
|
|
SSHPort int
|
|
SSHHostKey string // pinned host key (authorized_keys format)
|
|
}
|
|
|
|
// Client is a target cluster with its mapping (kind -> id -> field -> value)
|
|
// and its own policy overrides.
|
|
type Client struct {
|
|
Cluster
|
|
Mapping map[model.Kind]map[string]map[string]any
|
|
Policies map[model.Kind]map[string]adapter.Policy
|
|
}
|
|
|
|
// Selection filters the objects of a kind with glob patterns on their id.
|
|
type Selection struct {
|
|
Include []string `json:"include,omitempty"`
|
|
Exclude []string `json:"exclude,omitempty"`
|
|
}
|
|
|
|
// Config is the whole synchronization: Reference is the primary cluster.
|
|
type Config struct {
|
|
Reference Cluster
|
|
Clients []Client
|
|
Kinds []model.Kind
|
|
Objects map[model.Kind]Selection
|
|
Policies map[model.Kind]map[string]adapter.Policy
|
|
Prune bool
|
|
}
|
|
|
|
// Validate checks a configuration built elsewhere (web UI database, YAML import).
|
|
func (c *Config) Validate() error { return c.validate() }
|
|
|
|
func (c *Config) validate() error {
|
|
var errs []error
|
|
seen := map[string]bool{}
|
|
all := append([]Cluster{c.Reference}, clusters(c.Clients)...)
|
|
for i, cl := range all {
|
|
if cl.Name == "" {
|
|
errs = append(errs, fmt.Errorf("cluster #%d: name is required", i))
|
|
}
|
|
if seen[cl.Name] {
|
|
errs = append(errs, fmt.Errorf("cluster %s: duplicate name", cl.Name))
|
|
}
|
|
seen[cl.Name] = true
|
|
}
|
|
for _, k := range c.Kinds {
|
|
if _, err := adapter.ByKind(k); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
for _, d := range adapter.KindDeps[k] {
|
|
if !c.HasKind(d) {
|
|
errs = append(errs, fmt.Errorf("%s needs %s", k, d))
|
|
}
|
|
}
|
|
}
|
|
check := func(where string, p map[model.Kind]map[string]adapter.Policy) {
|
|
for k, fields := range p {
|
|
a, err := adapter.ByKind(k)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("%s: %w", where, err))
|
|
continue
|
|
}
|
|
for f, pol := range fields {
|
|
switch pol {
|
|
case adapter.Sync, adapter.Ignore, adapter.Override:
|
|
default:
|
|
errs = append(errs, fmt.Errorf("%s: %s.%s: unknown policy %q", where, k, f, pol))
|
|
}
|
|
if pol == adapter.Sync && a.Forbidden(f) {
|
|
errs = append(errs, fmt.Errorf("%s: %s.%s cannot be synchronized", where, k, f))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
check("policies", c.Policies)
|
|
for _, cl := range c.Clients {
|
|
check("client "+cl.Name, cl.Policies)
|
|
for k := range cl.Mapping {
|
|
if _, err := adapter.ByKind(k); err != nil {
|
|
errs = append(errs, fmt.Errorf("client %s mapping: %w", cl.Name, err))
|
|
}
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func clusters(cs []Client) []Cluster {
|
|
out := make([]Cluster, len(cs))
|
|
for i, c := range cs {
|
|
out[i] = c.Cluster
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Client returns the client with this name.
|
|
func (c *Config) Client(name string) (*Client, bool) {
|
|
for i := range c.Clients {
|
|
if c.Clients[i].Name == name {
|
|
return &c.Clients[i], true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// PVE builds the API client of a cluster, with its SSH access when the host
|
|
// key is recorded.
|
|
func (cl Cluster) PVE() (*pve.Client, error) {
|
|
var timeout time.Duration
|
|
var err error
|
|
if cl.Timeout != "" {
|
|
if timeout, err = time.ParseDuration(cl.Timeout); err != nil {
|
|
return nil, fmt.Errorf("cluster %s: timeout: %w", cl.Name, err)
|
|
}
|
|
}
|
|
o := pve.Options{
|
|
Name: cl.Name, BaseURL: cl.URL, User: cl.User, Password: cl.Password,
|
|
CAPEM: []byte(cl.CAPEM), Fingerprint: cl.Fingerprint, Insecure: cl.Insecure, Timeout: timeout,
|
|
}
|
|
if cl.SSHHostKey != "" {
|
|
sh, err := sshx.New(cl.SSHOptions())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cluster %s: %w", cl.Name, err)
|
|
}
|
|
o.Shell = sh
|
|
}
|
|
return pve.New(o)
|
|
}
|
|
|
|
// SSHOptions is the SSH access of the cluster: root on SSHHost, or on the host of the API URL.
|
|
func (cl Cluster) SSHOptions() sshx.Options {
|
|
host := cl.SSHHost
|
|
if host == "" {
|
|
if u, err := url.Parse(cl.URL); err == nil {
|
|
host = u.Hostname()
|
|
}
|
|
}
|
|
return sshx.Options{Host: host, Port: cl.SSHPort, User: "root", Password: cl.Password, HostKey: cl.SSHHostKey}
|
|
}
|
|
|
|
// Policy resolves the policy of a field for a client: client policy, then
|
|
// global policy, then the adapter default. Forbidden fields are always ignored.
|
|
func (c *Config) Policy(cl *Client, a adapter.Adapter, field string) adapter.Policy {
|
|
if a.Forbidden(field) {
|
|
return adapter.Ignore
|
|
}
|
|
if cl != nil {
|
|
if p, ok := cl.Policies[a.Kind()][field]; ok {
|
|
return p
|
|
}
|
|
}
|
|
if p, ok := c.Policies[a.Kind()][field]; ok {
|
|
return p
|
|
}
|
|
return a.DefaultPolicy(field)
|
|
}
|
|
|
|
// Selected tells whether an object id is part of the synchronization.
|
|
func (c *Config) Selected(k model.Kind, id string) bool {
|
|
s, ok := c.Objects[k]
|
|
if !ok {
|
|
return true
|
|
}
|
|
in := len(s.Include) == 0
|
|
for _, p := range s.Include {
|
|
if m, _ := path.Match(p, id); m {
|
|
in = true
|
|
break
|
|
}
|
|
}
|
|
for _, p := range s.Exclude {
|
|
if m, _ := path.Match(p, id); m {
|
|
return false
|
|
}
|
|
}
|
|
return in
|
|
}
|
|
|
|
// HasKind tells whether a kind is synchronized.
|
|
func (c *Config) HasKind(k model.Kind) bool {
|
|
for _, x := range c.Kinds {
|
|
if x == k {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|