hst/fs: implement overlay fstype
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m27s

This finally exposes overlay mounts in the high level hakurei API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-08-15 03:30:51 +09:00
parent 4433c993fa
commit 9ed3ba85ea
12 changed files with 253 additions and 3 deletions

View File

@@ -81,6 +81,12 @@ func (f *FilesystemConfigJSON) MarshalJSON() ([]byte, error) {
*FSEphemeral
}{fsType{FilesystemEphemeral}, cv}
case *FSOverlay:
v = &struct {
fsType
*FSOverlay
}{fsType{FilesystemOverlay}, cv}
default:
return nil, FSImplError{f.FilesystemConfig}
}
@@ -103,6 +109,9 @@ func (f *FilesystemConfigJSON) UnmarshalJSON(data []byte) error {
case FilesystemEphemeral:
*f = FilesystemConfigJSON{new(FSEphemeral)}
case FilesystemOverlay:
*f = FilesystemConfigJSON{new(FSOverlay)}
default:
return FSTypeError(t.Type)
}

View File

@@ -35,6 +35,10 @@ func TestFilesystemConfigJSON(t *testing.T) {
hst.FSImplError{Value: stubFS{"ephemeral"}},
"\x00", "\x00"},
{"bad impl overlay", hst.FilesystemConfigJSON{FilesystemConfig: stubFS{"overlay"}},
hst.FSImplError{Value: stubFS{"overlay"}},
"\x00", "\x00"},
{"bind", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSBind{
Dst: m("/etc"),
@@ -55,6 +59,17 @@ func TestFilesystemConfigJSON(t *testing.T) {
}, nil,
`{"type":"ephemeral","dst":"/run/user/65534","write":true,"size":1024,"perm":448}`,
`{"fs":{"type":"ephemeral","dst":"/run/user/65534","write":true,"size":1024,"perm":448},"magic":3236757504}`},
{"overlay", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSOverlay{
Dst: m("/nix/store"),
Lower: ms("/mnt-root/nix/.ro-store"),
Upper: m("/mnt-root/nix/.rw-store/upper"),
Work: m("/mnt-root/nix/.rw-store/work"),
},
}, nil,
`{"type":"overlay","dst":"/nix/store","lower":["/mnt-root/nix/.ro-store"],"upper":"/mnt-root/nix/.rw-store/upper","work":"/mnt-root/nix/.rw-store/work"}`,
`{"fs":{"type":"overlay","dst":"/nix/store","lower":["/mnt-root/nix/.ro-store"],"upper":"/mnt-root/nix/.rw-store/upper","work":"/mnt-root/nix/.rw-store/work"},"magic":3236757504}`},
}
for _, tc := range testCases {

98
hst/fsoverlay.go Normal file
View File

@@ -0,0 +1,98 @@
package hst
import (
"encoding/gob"
"strings"
"hakurei.app/container"
)
func init() { gob.Register(new(FSOverlay)) }
// FilesystemOverlay is the [FilesystemConfig.Type] name of an overlay mount point.
const FilesystemOverlay = "overlay"
// FSOverlay represents an overlay mount point.
type FSOverlay struct {
// mount point in container
Dst *container.Absolute `json:"dst"`
// any filesystem, does not need to be on a writable filesystem, must not be nil
Lower []*container.Absolute `json:"lower"`
// the upperdir is normally on a writable filesystem, leave as nil to mount Lower readonly
Upper *container.Absolute `json:"upper,omitempty"`
// the workdir needs to be an empty directory on the same filesystem as Upper, must not be nil if Upper is populated
Work *container.Absolute `json:"work,omitempty"`
}
func (o *FSOverlay) Valid() bool {
if o == nil || o.Dst == nil {
return false
}
for _, a := range o.Lower {
if a == nil {
return false
}
}
if o.Upper != nil { // rw
return o.Work != nil && len(o.Lower) > 0
} else { // ro
return len(o.Lower) >= 2
}
}
func (o *FSOverlay) Target() *container.Absolute {
if !o.Valid() {
return nil
}
return o.Dst
}
func (o *FSOverlay) Host() []*container.Absolute {
if !o.Valid() {
return nil
}
p := make([]*container.Absolute, 0, 2+len(o.Lower))
if o.Upper != nil && o.Work != nil {
p = append(p, o.Upper, o.Work)
}
p = append(p, o.Lower...)
return p
}
func (o *FSOverlay) Apply(op *container.Ops) {
if !o.Valid() {
return
}
if o.Upper != nil && o.Work != nil { // rw
op.Overlay(o.Dst, o.Upper, o.Work, o.Lower...)
} else { // ro
op.OverlayReadonly(o.Dst, o.Lower...)
}
}
func (o *FSOverlay) String() string {
if !o.Valid() {
return "<invalid>"
}
lower := make([]string, len(o.Lower))
for i, a := range o.Lower {
lower[i] = container.EscapeOverlayDataSegment(a.String())
}
if o.Upper != nil && o.Work != nil {
return "w*" + strings.Join(append([]string{
container.EscapeOverlayDataSegment(o.Dst.String()),
container.EscapeOverlayDataSegment(o.Upper.String()),
container.EscapeOverlayDataSegment(o.Work.String())},
lower...), container.SpecialOverlayPath)
} else {
return "*" + strings.Join(append([]string{
container.EscapeOverlayDataSegment(o.Dst.String())},
lower...), container.SpecialOverlayPath)
}
}

50
hst/fsoverlay_test.go Normal file
View File

@@ -0,0 +1,50 @@
package hst_test
import (
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFSOverlay(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"},
{"nil lower", &hst.FSOverlay{Dst: m("/etc"), Lower: []*container.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},
{"zero lower", &hst.FSOverlay{Dst: m("/etc"), Upper: m("/"), Work: m("/")}, false, nil, nil, nil, "<invalid>"},
{"zero lower ro", &hst.FSOverlay{Dst: m("/etc")}, false, nil, nil, nil, "<invalid>"},
{"short lower", &hst.FSOverlay{Dst: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, "<invalid>"},
{"full", &hst.FSOverlay{
Dst: m("/nix/store"),
Lower: ms("/mnt-root/nix/.ro-store"),
Upper: m("/mnt-root/nix/.rw-store/upper"),
Work: m("/mnt-root/nix/.rw-store/work"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/nix/store"),
Lower: ms("/mnt-root/nix/.ro-store"),
Upper: m("/mnt-root/nix/.rw-store/upper"),
Work: m("/mnt-root/nix/.rw-store/work"),
}}, m("/nix/store"), ms("/mnt-root/nix/.rw-store/upper", "/mnt-root/nix/.rw-store/work", "/mnt-root/nix/.ro-store"),
"w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store"},
{"ro", &hst.FSOverlay{
Dst: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
{"ro work", &hst.FSOverlay{
Dst: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Work: m("/tmp"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
})
}

View File

@@ -79,6 +79,12 @@ func Template() *Config {
},
Filesystem: []FilesystemConfigJSON{
{&FSEphemeral{Dst: container.AbsFHSTmp, Write: true, Perm: 0755}},
{&FSOverlay{
Dst: container.MustAbs("/nix/store"),
Lower: []*container.Absolute{container.MustAbs("/mnt-root/nix/.ro-store")},
Upper: container.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: container.MustAbs("/mnt-root/nix/.rw-store/work"),
}},
{&FSBind{Src: container.MustAbs("/nix/store")}},
{&FSBind{Src: container.AbsFHSRun.Append("current-system")}},
{&FSBind{Src: container.AbsFHSRun.Append("opengl-driver")}},

View File

@@ -103,6 +103,15 @@ func TestTemplate(t *testing.T) {
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"