Full web : clusters gérés dans l'UI, primaire et cibles, comptes pve
This commit is contained in:
1 parent
3e9cb563be
commit
ec3b7c9a01
5 files changed
+617
-453
No files matched your search
+58
-83
@@ -1,81 +1,60 @@
|
||||
// Package config loads the YAML configuration of the CLI (phase 0): clusters,
|
||||
// field policies, per client mappings and object selection.
|
||||
// Token secrets are never written in this file: they are read from an
|
||||
// environment variable or a file (docker secret), see README.
|
||||
// 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"
|
||||
"os"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"pve-confsync/internal/adapter"
|
||||
"pve-confsync/internal/model"
|
||||
"pve-confsync/internal/pve"
|
||||
"pve-confsync/internal/sshx"
|
||||
)
|
||||
|
||||
// Cluster is the connection to one PVE cluster.
|
||||
// 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 `yaml:"name"`
|
||||
URL string `yaml:"url"`
|
||||
TokenID string `yaml:"token_id"`
|
||||
TokenSecretEnv string `yaml:"token_secret_env,omitempty"`
|
||||
TokenSecretFile string `yaml:"token_secret_file,omitempty"`
|
||||
CAFile string `yaml:"ca_file,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
Insecure bool `yaml:"insecure,omitempty"`
|
||||
Timeout string `yaml:"timeout,omitempty"`
|
||||
|
||||
// Set by the web UI from its database, never read from nor written to YAML.
|
||||
Secret string `yaml:"-"` // token secret, decrypted
|
||||
CAPEM string `yaml:"-"` // CA bundle, PEM
|
||||
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 `yaml:",inline"`
|
||||
Mapping map[model.Kind]map[string]map[string]any `yaml:"mapping,omitempty"`
|
||||
Policies map[model.Kind]map[string]adapter.Policy `yaml:"policies,omitempty"`
|
||||
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 `yaml:"include,omitempty"`
|
||||
Exclude []string `yaml:"exclude,omitempty"`
|
||||
Include []string `json:"include,omitempty"`
|
||||
Exclude []string `json:"exclude,omitempty"`
|
||||
}
|
||||
|
||||
// Config is the whole file.
|
||||
// Config is the whole synchronization: Reference is the primary cluster.
|
||||
type Config struct {
|
||||
Reference Cluster `yaml:"reference"`
|
||||
Clients []Client `yaml:"clients"`
|
||||
Kinds []model.Kind `yaml:"kinds,omitempty"`
|
||||
Objects map[model.Kind]Selection `yaml:"objects,omitempty"`
|
||||
Policies map[model.Kind]map[string]adapter.Policy `yaml:"policies,omitempty"`
|
||||
Prune bool `yaml:"prune,omitempty"`
|
||||
}
|
||||
|
||||
// Load reads and validates a configuration file.
|
||||
func Load(file string) (*Config, error) {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c Config
|
||||
dec := yaml.NewDecoder(strings.NewReader(string(raw)))
|
||||
dec.KnownFields(true) // typos must fail, not be silently ignored
|
||||
if err := dec.Decode(&c); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", file, err)
|
||||
}
|
||||
if len(c.Kinds) == 0 {
|
||||
c.Kinds = append(c.Kinds, model.Kinds...)
|
||||
}
|
||||
return &c, c.validate()
|
||||
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).
|
||||
@@ -93,14 +72,16 @@ func (c *Config) validate() error {
|
||||
errs = append(errs, fmt.Errorf("cluster %s: duplicate name", cl.Name))
|
||||
}
|
||||
seen[cl.Name] = true
|
||||
if cl.Secret == "" && cl.TokenSecretEnv == "" && cl.TokenSecretFile == "" {
|
||||
errs = append(errs, fmt.Errorf("cluster %s: token_secret_env or token_secret_file is required", cl.Name))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@@ -151,45 +132,39 @@ func (c *Config) Client(name string) (*Client, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// PVE builds the API client of a cluster, resolving its token secret.
|
||||
// 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) {
|
||||
secret, err := cl.secret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
return pve.New(pve.Options{
|
||||
Name: cl.Name, BaseURL: cl.URL, TokenID: cl.TokenID, TokenSecret: secret,
|
||||
CAFile: cl.CAFile, CAPEM: []byte(cl.CAPEM), Fingerprint: cl.Fingerprint, Insecure: cl.Insecure, Timeout: timeout,
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
// ResolveSecret reads the token secret from the environment variable or file of
|
||||
// the YAML configuration (used when a YAML file is imported in the web UI).
|
||||
func (cl Cluster) ResolveSecret() (string, error) { return cl.secret() }
|
||||
|
||||
func (cl Cluster) secret() (string, error) {
|
||||
if cl.Secret != "" {
|
||||
return cl.Secret, nil
|
||||
}
|
||||
if cl.TokenSecretEnv != "" {
|
||||
if v := os.Getenv(cl.TokenSecretEnv); v != "" {
|
||||
return v, nil
|
||||
}
|
||||
if cl.TokenSecretFile == "" {
|
||||
return "", fmt.Errorf("cluster %s: environment variable %s is empty", cl.Name, cl.TokenSecretEnv)
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
b, err := os.ReadFile(cl.TokenSecretFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cluster %s: token secret: %w", cl.Name, err)
|
||||
}
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user