155 lines
5.4 KiB
Go
155 lines
5.4 KiB
Go
package pve
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"net/url"
|
||
"sync"
|
||
|
||
"git.totmin.ru/en2zmax/forge-toolkit/configreload"
|
||
)
|
||
|
||
// Manager — одиночный, ПОТОКО-БЕЗОПАСНЫЙ диспетчер процесса (столп 2 §2):
|
||
// агенты запускаются в отдельных goroutine и конкурентно дёргают один
|
||
// Manager. В pooled-режиме (isolation=pooled) ядро на каждый вызов
|
||
// инжектит серверный аргумент _tenant_config = <agentDir>/forge-tools/
|
||
// proxmox.json — поэтому менеджер кэширует per-путь загрузчики конфига
|
||
// (configreload, live-reload по контент-хэшу) и per-путь Tenant'ы (свои
|
||
// клиенты/секреты). Тенанты НЕ делят клиентов между агентами.
|
||
type Manager struct {
|
||
mu sync.Mutex
|
||
// configPath — статический -config (легаси-одиночный режим). Пусто =
|
||
// pooled: конфиг приходит на каждый вызов через _tenant_config.
|
||
configPath string
|
||
// loaders кэширует configreload.Loader[*Config] по пути конфига.
|
||
loaders map[string]*configreload.Loader[*Config]
|
||
// tenants кэширует Tenant (свои клиенты) по ключу-пути конфига.
|
||
tenants map[string]*Tenant
|
||
}
|
||
|
||
// NewManager создаёт менеджер. configPath — опциональный постоянный конфиг
|
||
// (-config); пусто = pooled (тенант из _tenant_config на каждый вызов).
|
||
func NewManager(configPath string) *Manager {
|
||
return &Manager{
|
||
configPath: configPath,
|
||
loaders: make(map[string]*configreload.Loader[*Config]),
|
||
tenants: make(map[string]*Tenant),
|
||
}
|
||
}
|
||
|
||
// Tenant возвращает per-agent тенант. path — _tenant_config (пусто при
|
||
// -config-режиме). Fail-closed: нет конфига — ошибка (модуль отказывает,
|
||
// а не работает «с общими» кредами).
|
||
func (m *Manager) Tenant(ctx context.Context, path string) (*Tenant, error) {
|
||
cfgPath := m.resolvePath(path)
|
||
if cfgPath == "" {
|
||
return nil, errors.New("proxmox: no config (set -config or provide _tenant_config)")
|
||
}
|
||
|
||
loader := m.loaderFor(cfgPath)
|
||
cfg, err := loader.Get()
|
||
if err != nil {
|
||
// ErrNotFound / parse-error без last-good — fail-closed, а не фолбэк.
|
||
if cfg == nil || !cfg.HasHosts() {
|
||
return nil, fmt.Errorf("proxmox: tenant config %s: %w", cfgPath, err)
|
||
}
|
||
// Есть last-good — работаем со старым (правка была битой), но
|
||
// сигналим, чтобы не молчать.
|
||
// (здесь последний рабочий конфиг уже возвращён в cfg)
|
||
}
|
||
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if t, ok := m.tenants[cfgPath]; ok {
|
||
return t, nil
|
||
}
|
||
t := &Tenant{cfg: cfg, clients: make(map[string]*Client)}
|
||
m.tenants[cfgPath] = t
|
||
return t, nil
|
||
}
|
||
|
||
// resolvePath выбирает путь конфига: _tenant_config в приоритете, иначе
|
||
// статический -config.
|
||
func (m *Manager) resolvePath(tenantPath string) string {
|
||
if tenantPath != "" {
|
||
return tenantPath
|
||
}
|
||
return m.configPath
|
||
}
|
||
|
||
// loaderFor возвращает (и кэширует) загрузчик конфига по пути.
|
||
func (m *Manager) loaderFor(path string) *configreload.Loader[*Config] {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if l, ok := m.loaders[path]; ok {
|
||
return l
|
||
}
|
||
l := configreload.New(path, ParseConfig)
|
||
m.loaders[path] = l
|
||
return l
|
||
}
|
||
|
||
// Close закрывает все тенанты (и их клиенты).
|
||
func (m *Manager) Close() {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
for _, t := range m.tenants {
|
||
t.closeLocked()
|
||
}
|
||
m.tenants = nil
|
||
}
|
||
|
||
// Tenant — per-agent конфигурация + свои HTTP-клиенты к хостам. Секреты и
|
||
// разрешения (allowlist) — строго в рамках одного тенанта.
|
||
type Tenant struct {
|
||
cfg *Config
|
||
mu sync.Mutex
|
||
clients map[string]*Client // alias -> client
|
||
}
|
||
|
||
// Config возвращает политику тенанта.
|
||
func (t *Tenant) Config() *Config { return t.cfg }
|
||
|
||
// Client возвращает HTTP-клиент для хоста по алиасу (или default).
|
||
// Строится лениво и кэшируется; потокобезопасно.
|
||
func (t *Tenant) Client(ctx context.Context, alias string) (*Client, error) {
|
||
host, ok := t.cfg.Host(alias)
|
||
if !ok {
|
||
return nil, fmt.Errorf("proxmox: host %q not configured", alias)
|
||
}
|
||
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
if c, ok := t.clients[host.Alias]; ok {
|
||
return c, nil
|
||
}
|
||
c, err := NewClient(host)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
t.clients[host.Alias] = c
|
||
return c, nil
|
||
}
|
||
|
||
// Close закрывает клиенты тенанта. Не используй вне RWMutex Manager.
|
||
func (t *Tenant) Close() {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
t.closeLocked()
|
||
}
|
||
|
||
func (t *Tenant) closeLocked() {
|
||
// net/http.Client не имеет Close; здесь точка для будущего пула/окружения.
|
||
t.clients = nil
|
||
}
|
||
|
||
// joinURL собирает корректный путь API (PathEscape против path-traversal).
|
||
func joinURL(path string, ids ...string) string {
|
||
p := path
|
||
for _, id := range ids {
|
||
p += "/" + url.PathEscape(id)
|
||
}
|
||
return p
|
||
}
|