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() | ||||
| } | ||||
| 
 | ||||
| // 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 { | ||||
|  | ||||
| @ -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) | ||||
| 				} | ||||
| 			}) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user