proxmox: config block, IDs, marks, state mapping, classify, registration
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>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
// 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"))
|
||||
}
|
||||
Reference in New Issue
Block a user