package kvm2 import ( "errors" "fmt" "strings" "github.com/digitalocean/go-libvirt" "golang.org/x/crypto/ssh/knownhosts" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider" ) // permanentf and transientf classify an error at the source, so classify // keeps that class no matter what wraps it later. func permanentf(format string, args ...any) error { return fmt.Errorf("%w: %s", provider.ErrPermanent, fmt.Sprintf(format, args...)) } func transientf(format string, args ...any) error { return fmt.Errorf("%w: %s", provider.ErrTransient, fmt.Sprintf(format, args...)) } // sshAuthFailed is the substring x/crypto/ssh puts in every authentication // failure ("ssh: handshake failed: ssh: unable to authenticate, attempted // methods [...]"). const sshAuthFailed = "unable to authenticate" // classify maps any error reaching the provider's boundary to one of the // four contract sentinels, per spec entry 13. op is the contract operation: // a vanished domain or volume inside Create is Transient, because the // contract forbids NotFound from Create. func classify(op string, err error) error { if err == nil { return provider.ErrTransient } for _, class := range []error{provider.ErrNotFound, provider.ErrQuotaExceeded, provider.ErrPermanent, provider.ErrTransient} { if errors.Is(err, class) { return class } } if code, ok := libvirtCode(err); ok { return classifyLibvirt(op, code) } var keyErr *knownhosts.KeyError if errors.As(err, &keyErr) { return provider.ErrPermanent } if strings.Contains(err.Error(), sshAuthFailed) { return provider.ErrPermanent } // Everything else retries: a missing key or known_hosts file (the // optional Secret has not landed yet), context cancellation and // deadlines, net.OpError, io.EOF, and anything unrecognised. return provider.ErrTransient } func classifyLibvirt(op string, code libvirt.ErrorNumber) error { switch code { case libvirt.ErrNoDomain, libvirt.ErrNoStorageVol: if op == "create" { return provider.ErrTransient } return provider.ErrNotFound case libvirt.ErrNoStoragePool, libvirt.ErrXMLError, libvirt.ErrNoSupport, libvirt.ErrInvalidArg, libvirt.ErrAuthFailed, libvirt.ErrAccessDenied: return provider.ErrPermanent default: return provider.ErrTransient } } // libvirtCode extracts the libvirt error number from err, if it is or wraps // a libvirt.Error. That type has a value receiver, so errors.As needs a // value target. func libvirtCode(err error) (libvirt.ErrorNumber, bool) { var le libvirt.Error if !errors.As(err, &le) { return 0, false } return libvirt.ErrorNumber(le.Code), true } func hasCode(err error, code libvirt.ErrorNumber) bool { got, ok := libvirtCode(err) return ok && got == code } // libvirtErrorFields returns the key/value pairs a log line carries for a // libvirt error, or nil when err is not one. func libvirtErrorFields(err error) []any { code, ok := libvirtCode(err) if !ok { return nil } return []any{"libvirtCode", int(code), "libvirtError", code.String()} }