container/msg: optionally provide error messages
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m38s

This makes handling of fatal errors a lot less squirmy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-08-31 11:57:59 +09:00
parent 712cfc06d7
commit 780e3e5465
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
4 changed files with 117 additions and 8 deletions

View File

@ -132,6 +132,24 @@ func (e *StartError) Error() string {
return e.Step + ": " + e.Err.Error() return e.Step + ": " + e.Err.Error()
} }
// Message returns a user-facing error message.
func (e *StartError) Message() string {
if e.Passthrough {
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.SyscallError)):
return "cannot " + e.Err.Error()
default:
return e.Err.Error()
}
}
if e.Origin {
return e.Step
}
return "cannot " + e.Error()
}
// Start starts the container init. The init process blocks until Serve is called. // Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error { func (p *Container) Start() error {
if p.cmd != nil { if p.cmd != nil {

View File

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@ -34,6 +35,7 @@ func TestStartError(t *testing.T) {
s string s string
is error is error
isF error isF error
msg string
}{ }{
{"params env", &container.StartError{ {"params env", &container.StartError{
Fatal: true, Fatal: true,
@ -41,7 +43,8 @@ func TestStartError(t *testing.T) {
Err: container.ErrReceiveEnv, Err: container.ErrReceiveEnv,
}, },
"set up params stream: environment variable not set", "set up params stream: environment variable not set",
container.ErrReceiveEnv, syscall.EBADF}, container.ErrReceiveEnv, syscall.EBADF,
"cannot set up params stream: environment variable not set"},
{"params", &container.StartError{ {"params", &container.StartError{
Fatal: true, Fatal: true,
@ -49,7 +52,8 @@ func TestStartError(t *testing.T) {
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF}, Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
}, },
"set up params stream pipe2: bad file descriptor", "set up params stream pipe2: bad file descriptor",
syscall.EBADF, os.ErrInvalid}, syscall.EBADF, os.ErrInvalid,
"cannot set up params stream pipe2: bad file descriptor"},
{"PR_SET_NO_NEW_PRIVS", &container.StartError{ {"PR_SET_NO_NEW_PRIVS", &container.StartError{
Fatal: true, Fatal: true,
@ -57,14 +61,16 @@ func TestStartError(t *testing.T) {
Err: syscall.EPERM, Err: syscall.EPERM,
}, },
"prctl(PR_SET_NO_NEW_PRIVS): operation not permitted", "prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
syscall.EPERM, syscall.EACCES}, syscall.EPERM, syscall.EACCES,
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
{"landlock abi", &container.StartError{ {"landlock abi", &container.StartError{
Step: "get landlock ABI", Step: "get landlock ABI",
Err: syscall.ENOSYS, Err: syscall.ENOSYS,
}, },
"get landlock ABI: function not implemented", "get landlock ABI: function not implemented",
syscall.ENOSYS, syscall.ENOEXEC}, syscall.ENOSYS, syscall.ENOEXEC,
"cannot get landlock ABI: function not implemented"},
{"landlock old", &container.StartError{ {"landlock old", &container.StartError{
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
@ -72,7 +78,8 @@ func TestStartError(t *testing.T) {
Origin: true, Origin: true,
}, },
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
syscall.ENOSYS, syscall.ENOSPC}, syscall.ENOSYS, syscall.ENOSPC,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
{"landlock create", &container.StartError{ {"landlock create", &container.StartError{
Fatal: true, Fatal: true,
@ -80,7 +87,8 @@ func TestStartError(t *testing.T) {
Err: syscall.EBADFD, Err: syscall.EBADFD,
}, },
"create landlock ruleset: file descriptor in bad state", "create landlock ruleset: file descriptor in bad state",
syscall.EBADFD, syscall.EBADF}, syscall.EBADFD, syscall.EBADF,
"cannot create landlock ruleset: file descriptor in bad state"},
{"landlock enforce", &container.StartError{ {"landlock enforce", &container.StartError{
Fatal: true, Fatal: true,
@ -88,7 +96,8 @@ func TestStartError(t *testing.T) {
Err: syscall.ENOTRECOVERABLE, Err: syscall.ENOTRECOVERABLE,
}, },
"enforce landlock ruleset: state not recoverable", "enforce landlock ruleset: state not recoverable",
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT}, syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
"cannot enforce landlock ruleset: state not recoverable"},
{"start", &container.StartError{ {"start", &container.StartError{
Step: "start container init", Step: "start container init",
@ -99,7 +108,31 @@ func TestStartError(t *testing.T) {
}, Passthrough: true, }, Passthrough: true,
}, },
"fork/exec /proc/nonexistent: no such file or directory", "fork/exec /proc/nonexistent: no such file or directory",
syscall.ENOENT, syscall.ENOSYS}, syscall.ENOENT, syscall.ENOSYS,
"cannot fork/exec /proc/nonexistent: no such file or directory"},
{"start syscall", &container.StartError{
Step: "start container init",
Err: &os.SyscallError{
Syscall: "open",
Err: syscall.ENOSYS,
}, Passthrough: true,
},
"open: function not implemented",
syscall.ENOSYS, syscall.ENOENT,
"cannot open: function not implemented"},
{"start other", &container.StartError{
Step: "start container init",
Err: &net.OpError{
Op: "dial",
Net: "unix",
Err: syscall.ECONNREFUSED,
}, Passthrough: true,
},
"dial unix: connection refused",
syscall.ECONNREFUSED, syscall.ECONNABORTED,
"dial unix: connection refused"},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@ -117,6 +150,17 @@ func TestStartError(t *testing.T) {
t.Errorf("Is: unexpected true") t.Errorf("Is: unexpected true")
} }
}) })
t.Run("msg", func(t *testing.T) {
if got, ok := container.GetErrorMessage(tc.err); !ok {
if tc.msg != "" {
t.Errorf("GetErrorMessage: err does not implement MessageError")
}
return
} else if got != tc.msg {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg)
}
})
}) })
} }
} }

View File

@ -1,10 +1,28 @@
package container package container
import ( import (
"errors"
"log" "log"
"sync/atomic" "sync/atomic"
) )
// MessageError is an error with a user-facing message.
type MessageError interface {
// Message returns a user-facing error message.
Message() string
error
}
// GetErrorMessage returns whether an error implements [MessageError], and the message if it does.
func GetErrorMessage(err error) (string, bool) {
var e MessageError
if !errors.As(err, &e) || e == nil {
return zeroString, false
}
return e.Message(), true
}
type Msg interface { type Msg interface {
IsVerbose() bool IsVerbose() bool
Verbose(v ...any) Verbose(v ...any)

View File

@ -1,14 +1,43 @@
package container_test package container_test
import ( import (
"errors"
"log" "log"
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall"
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
) )
func TestMessageError(t *testing.T) {
testCases := []struct {
name string
err error
want string
wantOk bool
}{
{"nil", nil, "", false},
{"new", errors.New(":3"), "", false},
{"start", &container.StartError{
Step: "meow",
Err: syscall.ENOTRECOVERABLE,
}, "cannot meow: state not recoverable", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, ok := container.GetErrorMessage(tc.err)
if got != tc.want {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.want)
}
if ok != tc.wantOk {
t.Errorf("GetErrorMessage: ok = %v, want %v", ok, tc.wantOk)
}
})
}
}
func TestDefaultMsg(t *testing.T) { func TestDefaultMsg(t *testing.T) {
{ {
w := log.Writer() w := log.Writer()