// 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" "fmt" "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" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/keyedlock" ) 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 // nameLocks serialises Create and Delete per instance name (spec entry // 19), so the reconciler's Delete and GC's Delete never race on stop and // destroy, and a Delete cannot interleave with a Create's steps. nameLocks *keyedlock.Locks } 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 } client, err := newHTTPAPI(withDefaults(cfg)) if err != nil { return nil, fmt.Errorf("proxmox provider %s: %w", pc.Name, err) } p := newProvider(pc.Name, cfg, observe(client, pc.Name)) 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 an 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(), nameLocks: keyedlock.New(), } } // 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) } // lockName takes the per-name lock of spec entry 19, or gives up with ctx, // so a caller whose ctx ends while another caller's Create waits on a clone // returns promptly. func (p *Provider) lockName(ctx context.Context, name string) (func(), error) { release, err := p.nameLocks.Acquire(ctx, name) if err != nil { return nil, fmt.Errorf("%w: waiting for the lock on %s: %w", provider.ErrTransient, name, err) } return release, nil } // 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")) }