Files
forge-tools-proxmox/internal/pve/manager.go
T

155 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}