aboutsummaryrefslogtreecommitdiffhomepage
path: root/hst
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-10-23 22:51:10 +0900
committerOphestra <cat@gensokyo.uk>2025-10-23 22:59:02 +0900
commitdd94818f20c7a37bdd7c6129551759db1b09b65e (patch)
tree1ea7c94b3f6aec92f037a3e58dc343c3b00cd5a2 /hst
parent0fd357e7f64ec029489bbbcc7935005a26cb9a13 (diff)
hst/instance: define instance state
This is now part of the hst API. This change also improves identifier generation and serialisation. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'hst')
-rw-r--r--hst/hst.go2
-rw-r--r--hst/instance.go84
-rw-r--r--hst/instance_test.go113
3 files changed, 198 insertions, 1 deletions
diff --git a/hst/hst.go b/hst/hst.go
index 7fd7619c..bdff19f8 100644
--- a/hst/hst.go
+++ b/hst/hst.go
@@ -16,7 +16,7 @@ type AppError struct {
// A user-facing description of where the error occurred.
Step string `json:"step"`
// The underlying error value.
- Err error
+ Err error `json:"err"`
// An arbitrary error message, overriding the return value of Message if not empty.
Msg string `json:"message,omitempty"`
}
diff --git a/hst/instance.go b/hst/instance.go
new file mode 100644
index 00000000..5d15df6b
--- /dev/null
+++ b/hst/instance.go
@@ -0,0 +1,84 @@
+package hst
+
+import (
+ "crypto/rand"
+ "encoding/binary"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "time"
+)
+
+// An ID is a unique identifier held by a running hakurei container.
+type ID [16]byte
+
+// ErrIdentifierLength is returned when encountering a [hex] representation of [ID] with unexpected length.
+var ErrIdentifierLength = errors.New("identifier string has unexpected length")
+
+// IdentifierDecodeError is returned by [ID.UnmarshalText] to provide relevant error descriptions.
+type IdentifierDecodeError struct{ Err error }
+
+func (e IdentifierDecodeError) Unwrap() error { return e.Err }
+func (e IdentifierDecodeError) Error() string {
+ var invalidByteError hex.InvalidByteError
+ switch {
+ case errors.As(e.Err, &invalidByteError):
+ return fmt.Sprintf("got invalid byte %#U in identifier", rune(invalidByteError))
+ case errors.Is(e.Err, hex.ErrLength):
+ return "odd length identifier hex string"
+
+ default:
+ return e.Err.Error()
+ }
+}
+
+// String returns the [hex] string representation of [ID].
+func (a *ID) String() string { return hex.EncodeToString(a[:]) }
+
+// CreationTime returns the point in time [ID] was created.
+func (a *ID) CreationTime() time.Time {
+ return time.Unix(0, int64(binary.BigEndian.Uint64(a[:8]))).UTC()
+}
+
+// NewInstanceID creates a new unique [ID].
+func NewInstanceID(id *ID) error { return newInstanceID(id, uint64(time.Now().UnixNano())) }
+
+// newInstanceID creates a new unique [ID] with the specified timestamp.
+func newInstanceID(id *ID, p uint64) error {
+ binary.BigEndian.PutUint64(id[:8], p)
+ _, err := rand.Read(id[8:])
+ return err
+}
+
+// MarshalText encodes the [hex] representation of [ID].
+func (a *ID) MarshalText() (text []byte, err error) {
+ text = make([]byte, hex.EncodedLen(len(a)))
+ hex.Encode(text, a[:])
+ return
+}
+
+// UnmarshalText decodes a [hex] representation of [ID].
+func (a *ID) UnmarshalText(text []byte) error {
+ dl := hex.DecodedLen(len(text))
+ if dl != len(a) {
+ return IdentifierDecodeError{ErrIdentifierLength}
+ }
+ _, err := hex.Decode(a[:], text)
+ if err == nil {
+ return nil
+ }
+ return IdentifierDecodeError{err}
+}
+
+// A State describes a running hakurei container.
+type State struct {
+ // Unique instance id, created by [NewInstanceID].
+ ID ID `json:"instance"`
+ // Shim process pid. Runs as the target user.
+ PID int `json:"pid"`
+ // Configuration used to start the container.
+ Config *Config `json:"config"`
+
+ // Point in time the shim process was created.
+ Time time.Time `json:"time"`
+}
diff --git a/hst/instance_test.go b/hst/instance_test.go
new file mode 100644
index 00000000..5be9fc8b
--- /dev/null
+++ b/hst/instance_test.go
@@ -0,0 +1,113 @@
+package hst_test
+
+import (
+ "encoding/hex"
+ "errors"
+ "reflect"
+ "testing"
+ "time"
+ _ "unsafe"
+
+ "hakurei.app/hst"
+)
+
+//go:linkname newInstanceID hakurei.app/hst.newInstanceID
+func newInstanceID(id *hst.ID, p uint64) error
+
+func TestIdentifierDecodeError(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ err error
+ want string
+ }{
+ {"invalid byte", hst.IdentifierDecodeError{Err: hex.InvalidByteError(0)},
+ "got invalid byte U+0000 in identifier"},
+ {"odd length", hst.IdentifierDecodeError{Err: hex.ErrLength},
+ "odd length identifier hex string"},
+ {"passthrough", hst.IdentifierDecodeError{Err: hst.ErrIdentifierLength},
+ hst.ErrIdentifierLength.Error()},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ if got := tc.err.Error(); got != tc.want {
+ t.Errorf("Error: %q, want %q", got, tc.want)
+ }
+ })
+ }
+
+ t.Run("unwrap", func(t *testing.T) {
+ t.Parallel()
+
+ err := hst.IdentifierDecodeError{Err: hst.ErrIdentifierLength}
+ if !errors.Is(err, hst.ErrIdentifierLength) {
+ t.Errorf("Is unexpected false")
+ }
+ })
+}
+
+func TestID(t *testing.T) {
+ t.Parallel()
+
+ var randomID hst.ID
+ if err := hst.NewInstanceID(&randomID); err != nil {
+ t.Fatalf("NewInstanceID: error = %v", err)
+ }
+
+ testCases := []struct {
+ name string
+ data string
+ want hst.ID
+ err error
+ }{
+ {"bad length", "meow", hst.ID{},
+ hst.IdentifierDecodeError{Err: hst.ErrIdentifierLength}},
+ {"invalid byte", "02bc7f8936b2af6\x00\x00e2535cd71ef0bb7", hst.ID{},
+ hst.IdentifierDecodeError{Err: hex.InvalidByteError(0)}},
+
+ {"zero", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", hst.ID{}, nil},
+ {"random", randomID.String(), randomID, nil},
+ {"sample", "ba21c9bd33d9d37917288281a2a0d239", hst.ID{
+ 0xba, 0x21, 0xc9, 0xbd,
+ 0x33, 0xd9, 0xd3, 0x79,
+ 0x17, 0x28, 0x82, 0x81,
+ 0xa2, 0xa0, 0xd2, 0x39}, nil},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ var got hst.ID
+ if err := got.UnmarshalText([]byte(tc.data)); !reflect.DeepEqual(err, tc.err) {
+ t.Errorf("UnmarshalText: error = %#v, want %#v", err, tc.err)
+ }
+
+ if tc.err == nil {
+ if gotString := got.String(); gotString != tc.data {
+ t.Errorf("String: %q, want %q", gotString, tc.data)
+ }
+ if gotData, _ := got.MarshalText(); string(gotData) != tc.data {
+ t.Errorf("MarshalText: %q, want %q", string(gotData), tc.data)
+ }
+ }
+ })
+ }
+
+ t.Run("time", func(t *testing.T) {
+ t.Parallel()
+ var id hst.ID
+
+ now := time.Now()
+ if err := newInstanceID(&id, uint64(now.UnixNano())); err != nil {
+ t.Fatalf("newInstanceID: error = %v", err)
+ }
+
+ got := id.CreationTime()
+ if !got.Equal(now) {
+ t.Fatalf("CreationTime(%q): %s, want %s", id.String(), got, now)
+ }
+ })
+}