Files
egress-proxies-operator/internal/provider/proxmox/delete.go
T
kacerrandClaude Opus 5.5 2f89c1fffb proxmox: Delete with seed cleanup, health Checker, CRD type comment
Delete stops and destroys a VM of ours under the per-name lock, then
deletes its seed on the VM's node, or on every node when no VM of ours
was left. A hibernated VM of ours is Permanent, as in Create. The
Checker verifies each node concurrently (version, node status, ISO
storage, templates) and adds CertificateInvalid, CredentialsUnavailable,
StorageNotFound and TemplateNotFound to the shared reasons. The
ProviderHealth type comment gains proxmox.

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

150 lines
4.9 KiB
Go

package proxmox
import (
"context"
"fmt"
"time"
"github.com/go-logr/logr"
)
// Delete is spec entry 11: under the per-name lock, stop and destroy the VM
// when it is ours, then delete its seed. Everything already gone is
// success, and nothing of ours is left when nil is returned, so Get
// afterwards is NotFound.
func (p *Provider) Delete(ctx context.Context, id string) error {
vmid, name, err := parseProviderID(id)
if err != nil {
return p.wrapErr("delete", id, err)
}
log := p.logger(ctx).WithValues("vmid", vmid, "name", name)
unlock, err := p.lockName(ctx, name)
if err != nil {
return p.wrapErr("delete", id, err)
}
defer unlock()
node, err := p.destroy(ctx, log, vmid, name)
if err != nil {
return p.wrapErr("delete", id, err)
}
if node == "" {
err = p.deleteSeedEverywhere(ctx, log, name)
} else {
log = log.WithValues("node", node)
err = p.deleteSeed(ctx, log, node, name)
}
if err != nil {
return p.wrapErr("delete", id, err)
}
log.V(1).Info("proxmox vm deleted", "destroyed", node != "")
return nil
}
// destroy is steps 2 to 6. It returns the node of the VM it destroyed, or
// "" when there was no VM of ours to destroy: then the seed's node is not
// known, and the seed is looked for on every node.
func (p *Provider) destroy(ctx context.Context, log logr.Logger, vmid int, name string) (string, error) {
r, ok, err := p.resolve(ctx, vmid)
if err != nil {
return "", err
}
if !ok {
log.V(1).Info("proxmox vm already gone")
return "", nil
}
log = log.WithValues("node", r.Node)
if r.Lock != "" && r.Lock != lockSuspended {
return "", transientf("VM %d is locked (%s)", vmid, r.Lock)
}
if r.Status == "unknown" {
return "", transientf("VM %d has status unknown: its node %s is not answering", vmid, r.Node)
}
cfg, err := p.api.VMConfig(ctx, r.Node, vmid)
if isNotExist(err) {
log.V(1).Info("proxmox vm already gone")
return "", nil
}
if err != nil {
return "", fmt.Errorf("reading config of VM %d: %w", vmid, err)
}
if cfg["name"] != name {
log.V(1).Info("proxmox vm at this vmid is not ours, left alone", "vmName", cfg["name"])
return "", nil
}
if !parseDescription(cfg["description"]).Managed {
log.V(1).Info("proxmox vm carries no proxy-operator mark, left alone")
return "", nil
}
switch lock := cfg["lock"]; lock {
case "":
case lockSuspended:
// Destroy refuses a locked VM and the lock never clears on its own,
// so only an admin can move this on, as in Create's ownership check.
return "", permanentf("VM %d named %s is hibernated (lock suspended); resume or destroy it in Proxmox", vmid, name)
default:
return "", transientf("VM %d is locked (%s)", vmid, lock)
}
// The listing shows a paused VM as running, and Stop ends it too.
if r.Status != "stopped" {
gone, err := p.runTask(ctx, log, "stopping", vmid, p.budgets.stop, func() (string, error) { return p.api.Stop(ctx, r.Node, vmid) })
if err != nil || gone {
return r.Node, err
}
}
if _, err := p.runTask(ctx, log, "destroying", vmid, p.budgets.destroy, func() (string, error) { return p.api.Destroy(ctx, r.Node, vmid) }); err != nil {
return "", err
}
return r.Node, nil
}
// runTask submits a task on a VM and waits for it. A VM that is gone by the
// time the task is submitted reports gone: it is what the task was for.
func (p *Provider) runTask(ctx context.Context, log logr.Logger, what string, vmid int, budget time.Duration, submit func() (string, error)) (bool, error) {
upid, err := submit()
if isNotExist(err) {
log.V(1).Info("proxmox vm already gone")
return true, nil
}
if err != nil {
return false, fmt.Errorf("%s VM %d: %w", what, vmid, err)
}
if err := p.waitTask(ctx, log, upid, budget); err != nil {
return false, fmt.Errorf("%s VM %d: %w", what, vmid, err)
}
return false, nil
}
// deleteSeed is step 7 on one node. A missing volume still ends in a
// successful task (F13), so nothing is matched.
func (p *Provider) deleteSeed(ctx context.Context, log logr.Logger, node, name string) error {
volid := seedVolID(p.cfg.ISOStorage, name)
upid, err := p.api.DeleteVolume(ctx, node, p.cfg.ISOStorage, volid)
if err != nil {
return fmt.Errorf("deleting seed %s on %s: %w", volid, node, err)
}
if err := p.waitTask(ctx, log, upid, p.budgets.seedDelete); err != nil {
return fmt.Errorf("deleting seed %s on %s: %w", volid, node, err)
}
return nil
}
// deleteSeedEverywhere is step 7 when the VM's node is not known: every
// configured node is tried, and one that fails is logged and skipped, so a
// node that is down or lacks the storage cannot hold the Delete up. An
// ended ctx fails the call instead.
func (p *Provider) deleteSeedEverywhere(ctx context.Context, log logr.Logger, name string) error {
for _, node := range p.cfg.Nodes {
err := p.deleteSeed(ctx, log, node, name)
if err == nil {
continue
}
if ctx.Err() != nil {
return err
}
log.V(1).Info("proxmox seed delete failed on a node, skipped", "node", node, "reason", err.Error())
}
return nil
}