aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-08-31 11:57:59 +0900
committerOphestra <cat@gensokyo.uk>2025-08-31 11:57:59 +0900
commit780e3e546581dd4a46b887310a665eea06f3279a (patch)
treef7186c712fb4e0f62052e147968196bc45a17996
parent712cfc06d7e541c6e28b4b127a4760b4cfccffa8 (diff)
container/msg: optionally provide error messages
This makes handling of fatal errors a lot less squirmy. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--container/container.go18
-rw-r--r--container/container_test.go60
-rw-r--r--container/msg.go18
-rw-r--r--container/msg_test.go29
4 files changed, 117 insertions, 8 deletions
diff --git a/container/container.go b/container/container.go
index 2301ce75..dee4a44d 100644
--- a/container/container.go
+++ b/container/container.go
@@ -132,6 +132,24 @@ func (e *StartError) Error() string {
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.
func (p *Container) Start() error {
if p.cmd != nil {
diff --git a/container/container_test.go b/container/container_test.go
index 0d408bf1..c7f78aac 100644
--- a/container/container_test.go
+++ b/container/container_test.go
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"log"
+ "net"
"os"
"os/exec"
"os/signal"
@@ -34,6 +35,7 @@ func TestStartError(t *testing.T) {
s string
is error
isF error
+ msg string
}{
{"params env", &container.StartError{
Fatal: true,
@@ -41,7 +43,8 @@ func TestStartError(t *testing.T) {
Err: container.ErrReceiveEnv,
},
"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{
Fatal: true,
@@ -49,7 +52,8 @@ func TestStartError(t *testing.T) {
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
},
"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{
Fatal: true,
@@ -57,14 +61,16 @@ func TestStartError(t *testing.T) {
Err: syscall.EPERM,
},
"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{
Step: "get landlock ABI",
Err: syscall.ENOSYS,
},
"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{
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
@@ -72,7 +78,8 @@ func TestStartError(t *testing.T) {
Origin: true,
},
"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{
Fatal: true,
@@ -80,7 +87,8 @@ func TestStartError(t *testing.T) {
Err: syscall.EBADFD,
},
"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{
Fatal: true,
@@ -88,7 +96,8 @@ func TestStartError(t *testing.T) {
Err: syscall.ENOTRECOVERABLE,
},
"enforce landlock ruleset: state not recoverable",
- syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT},
+ syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
+ "cannot enforce landlock ruleset: state not recoverable"},
{"start", &container.StartError{
Step: "start container init",
@@ -99,7 +108,31 @@ func TestStartError(t *testing.T) {
}, Passthrough: true,
},
"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 {
t.Run(tc.name, func(t *testing.T) {
@@ -117,6 +150,17 @@ func TestStartError(t *testing.T) {
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)
+ }
+ })
})
}
}
diff --git a/container/msg.go b/container/msg.go
index 40f0bd4a..4be0fd2d 100644
--- a/container/msg.go
+++ b/container/msg.go
@@ -1,10 +1,28 @@
package container
import (
+ "errors"
"log"
"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 {
IsVerbose() bool
Verbose(v ...any)
diff --git a/container/msg_test.go b/container/msg_test.go
index 3c68a50e..ba62e9d3 100644
--- a/container/msg_test.go
+++ b/container/msg_test.go
@@ -1,14 +1,43 @@
package container_test
import (
+ "errors"
"log"
"strings"
"sync/atomic"
+ "syscall"
"testing"
"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) {
{
w := log.Writer()