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
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:
parent
712cfc06d7
commit
780e3e5465
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user