Phase 2 of the proxmox provider plan. Adds the proxmox: config block with its validation, registers the type in the config allow-list and the constructor map, and starts internal/provider/proxmox: the api seam and its data types, providerIDs (vms/<vmid>/<name>) with the UID-derived VMID, the description mark, state mapping, and error classification. Tests read the responses captured from pv1-1 in Phase 0 rather than repeating Proxmox's wording, and every matcher is run against every captured error so none fires on the wrong message. The contract methods are Transient stubs until Phases 5 to 7. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
174 lines
5.6 KiB
Go
174 lines
5.6 KiB
Go
// Package proxmox implements the provider contract on a Proxmox VE cluster
|
|
// through its REST API: VMs are full clones of prepared templates, seeded
|
|
// with a NoCloud ISO uploaded to a storage and addressed through the guest
|
|
// agent. The design is in docs/proxmox-provider-spec.md.
|
|
package proxmox
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/go-logr/logr"
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
|
)
|
|
|
|
const (
|
|
defaultVMIDMin = 100000
|
|
defaultVMIDMax = 199999
|
|
defaultRequestTimeoutSeconds = 30
|
|
)
|
|
|
|
// budgets bounds every wait of the provider (spec entry 18); tests shrink
|
|
// them.
|
|
type budgets struct {
|
|
clone time.Duration // the clone task, and waiting for a VM's lock
|
|
upload time.Duration
|
|
resize time.Duration
|
|
start time.Duration
|
|
stop time.Duration
|
|
destroy time.Duration
|
|
seedDelete time.Duration
|
|
running time.Duration // from the start task's end to status running
|
|
poll time.Duration // task and status poll interval
|
|
}
|
|
|
|
func defaultBudgets() budgets {
|
|
return budgets{
|
|
clone: 10 * time.Minute,
|
|
upload: 2 * time.Minute,
|
|
resize: 2 * time.Minute,
|
|
start: 2 * time.Minute,
|
|
stop: 2 * time.Minute,
|
|
destroy: 5 * time.Minute,
|
|
seedDelete: time.Minute,
|
|
running: 60 * time.Second,
|
|
poll: time.Second,
|
|
}
|
|
}
|
|
|
|
// Provider is one configured proxmox entry: one cluster reached through one
|
|
// API URL with one token.
|
|
type Provider struct {
|
|
name string
|
|
cfg provider.ProxmoxConfig
|
|
api api
|
|
now func() time.Time
|
|
budgets budgets
|
|
}
|
|
|
|
var _ provider.Provider = (*Provider)(nil)
|
|
|
|
// New builds a Provider from pc.Proxmox. It never makes a request: the token
|
|
// file is only checked for existence, and a missing one is a warning, not an
|
|
// error, because the Secret mount is optional and may land later.
|
|
func New(ctx context.Context, pc provider.ProviderConfig) (provider.Provider, error) {
|
|
cfg := provider.ProxmoxConfig{}
|
|
if pc.Proxmox != nil {
|
|
cfg = *pc.Proxmox
|
|
}
|
|
// The HTTP client behind the api seam arrives in Phase 4 of the plan;
|
|
// until then every method is a stub that makes no request.
|
|
p := newProvider(pc.Name, cfg, nil)
|
|
log := p.logger(ctx)
|
|
if _, err := os.Stat(p.cfg.TokenFile); err != nil {
|
|
log.Info("WARNING: token file is not readable; every call fails until it is", "path", p.cfg.TokenFile, "reason", err.Error())
|
|
}
|
|
if p.cfg.InsecureSkipTLSVerify {
|
|
log.Info("WARNING: insecureSkipTLSVerify is set; the API's certificate is not verified")
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// newProvider is the constructor tests use, with a in-memory api.
|
|
func newProvider(name string, cfg provider.ProxmoxConfig, a api) *Provider {
|
|
return &Provider{
|
|
name: name,
|
|
cfg: withDefaults(cfg),
|
|
api: a,
|
|
now: time.Now,
|
|
budgets: defaultBudgets(),
|
|
}
|
|
}
|
|
|
|
// withDefaults returns cfg with every optional field filled in, so the rest
|
|
// of the package never re-checks for an empty value. Slices, maps and
|
|
// pointers are copied, not shared with the caller.
|
|
func withDefaults(cfg provider.ProxmoxConfig) provider.ProxmoxConfig {
|
|
cfg.Nodes = append([]string(nil), cfg.Nodes...)
|
|
if cfg.VMIDRange == nil {
|
|
cfg.VMIDRange = &provider.ProxmoxVMIDRange{Min: defaultVMIDMin, Max: defaultVMIDMax}
|
|
} else {
|
|
r := *cfg.VMIDRange
|
|
cfg.VMIDRange = &r
|
|
}
|
|
if cfg.RequestTimeoutSeconds == 0 {
|
|
cfg.RequestTimeoutSeconds = defaultRequestTimeoutSeconds
|
|
}
|
|
install := true
|
|
if cfg.InstallGuestAgent != nil {
|
|
install = *cfg.InstallGuestAgent
|
|
}
|
|
cfg.InstallGuestAgent = &install
|
|
templates := make(map[string]provider.ProxmoxTemplate, len(cfg.Templates))
|
|
for k, v := range cfg.Templates {
|
|
templates[k] = v
|
|
}
|
|
cfg.Templates = templates
|
|
machineTypes := make(map[string]provider.ProxmoxMachineType, len(cfg.MachineTypes))
|
|
for k, v := range cfg.MachineTypes {
|
|
machineTypes[k] = v
|
|
}
|
|
cfg.MachineTypes = machineTypes
|
|
return cfg
|
|
}
|
|
|
|
func (p *Provider) logger(ctx context.Context) logr.Logger {
|
|
return logf.FromContext(ctx).WithName("proxmox").WithValues("provider", p.name)
|
|
}
|
|
|
|
// node resolves placement.zone to a configured node. An empty zone is
|
|
// allowed only when exactly one node is configured.
|
|
func (p *Provider) node(zone string) (string, error) {
|
|
if zone == "" {
|
|
if len(p.cfg.Nodes) == 1 {
|
|
return p.cfg.Nodes[0], nil
|
|
}
|
|
return "", permanentf("placement.zone is required when proxmox.nodes lists more than one node")
|
|
}
|
|
for _, n := range p.cfg.Nodes {
|
|
if n == zone {
|
|
return n, nil
|
|
}
|
|
}
|
|
return "", permanentf("placement.zone %q is not in proxmox.nodes", zone)
|
|
}
|
|
|
|
// wrapErr is the single exit for errors: it classifies err and wraps it with
|
|
// the configured provider name, op and id.
|
|
func (p *Provider) wrapErr(op, id string, err error) error {
|
|
return provider.Wrap(classify(op, err), op, p.name, id, err)
|
|
}
|
|
|
|
// The four contract methods are stubs until the phases that implement them
|
|
// (Get and ListByTag in Phase 5, Create in 6, Delete in 7). Each answers
|
|
// Transient, so a Proxy on this provider retries rather than latching.
|
|
|
|
func (p *Provider) Create(_ context.Context, req provider.CreateRequest) (string, error) {
|
|
return "", p.wrapErr("create", "", transientf("proxmox Create is not implemented yet (request %s)", req.Name))
|
|
}
|
|
|
|
func (p *Provider) Get(_ context.Context, id string) (*provider.Instance, error) {
|
|
return nil, p.wrapErr("get", id, transientf("proxmox Get is not implemented yet"))
|
|
}
|
|
|
|
func (p *Provider) Delete(_ context.Context, id string) error {
|
|
return p.wrapErr("delete", id, transientf("proxmox Delete is not implemented yet"))
|
|
}
|
|
|
|
func (p *Provider) ListByTag(_ context.Context) ([]provider.Instance, error) {
|
|
return nil, p.wrapErr("list", "", transientf("proxmox ListByTag is not implemented yet"))
|
|
}
|