Files
egress-proxies-operator/internal/provider/proxmox/list.go
T
kacerrandClaude Opus 5.5 803716459a proxmox: Get, ListByTag and task polling
Get follows spec entry 9's check order (lock and unknown status before
the name, suspended is Stopped after the name and mark checks), reads
status/current so paused is Stopped, and maps the three agent messages
of entry 7. ListByTag reads one config per candidate and fails whole
only on the resources call or an ended ctx. waitTask polls the UPID's
node every budgets.poll, logs failed tasks at V(1) and counts
"WARNINGS: <n>" as success; waitUnlocked serves Create's ownership check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:11:20 +00:00

82 lines
2.6 KiB
Go

package proxmox
import (
"context"
"strings"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
// proxyTag is the tag Create sets for the Proxmox UI; ListByTag uses it,
// with the name prefix provider.NameFromUID gives, only to pick candidates.
// The mark decides.
const (
proxyTag = "proxy_operator"
proxyNamePrefix = "proxy-"
)
// ListByTag is spec entry 12: one resources call covers every node, then
// one config read per candidate for the mark. GC reads only ID, UID and
// CreatedAt (contract §2.4), so no status or agent call is made. The
// resources call failing fails the list; one unreadable candidate is
// skipped, unless ctx ended, which fails the list rather than pass off a
// partial view as complete.
func (p *Provider) ListByTag(ctx context.Context) ([]provider.Instance, error) {
log := p.logger(ctx)
rs, err := p.api.ClusterResources(ctx)
if err != nil {
return nil, p.wrapErr("list", "", err)
}
var (
out []provider.Instance
candidates, skipped int
)
for _, r := range rs {
if !isCandidate(r) {
continue
}
candidates++
vlog := log.WithValues("vmid", r.VMID, "name", r.Name, "node", r.Node)
cfg, err := p.api.VMConfig(ctx, r.Node, r.VMID)
if err != nil {
if ctx.Err() != nil {
return nil, p.wrapErr("list", "", err)
}
skipped++
vlog.V(1).Info("skipping unreadable proxmox vm", "error", err.Error())
continue
}
mark := parseDescription(cfg["description"])
if !mark.Managed {
continue
}
inst := provider.Instance{ID: formatProviderID(r.VMID, r.Name), UID: mark.UID, CreatedAt: mark.CreatedAt}
vlog.V(2).Info("proxmox listed instance", "id", inst.ID, "uid", inst.UID, "createdAt", inst.CreatedAt)
out = append(out, inst)
}
log.V(1).Info("proxmox vms listed", "vms", len(rs), "candidates", candidates, "managed", len(out), "skipped", skipped)
return out, nil
}
// isCandidate picks the resource entries worth a config read: QEMU VMs, not
// templates, named, and either tagged or named like a proxy. A nameless
// entry is a clone in progress, whose Create is still running.
func isCandidate(r vmResource) bool {
if r.Type != "qemu" || r.Template != 0 || r.Name == "" {
return false
}
return strings.HasPrefix(r.Name, proxyNamePrefix) || hasTag(r.Tags, proxyTag)
}
// hasTag reports whether tags, as Proxmox lists them (";"-separated, though
// "," and spaces are accepted on input), contains tag. Proxmox folds tags to
// lower case.
func hasTag(tags, tag string) bool {
for _, t := range strings.FieldsFunc(tags, func(r rune) bool { return r == ';' || r == ',' || r == ' ' }) {
if strings.EqualFold(t, tag) {
return true
}
}
return false
}