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