aboutsummaryrefslogtreecommitdiffhomepage
path: root/hst
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-08-15 03:30:51 +0900
committerOphestra <cat@gensokyo.uk>2025-08-15 04:00:55 +0900
commit9ed3ba85eaba4bc4bd12b290dc4daf00ffe159dc (patch)
tree03301910c40c59eb94029f1a93b1e74e21b5226a /hst
parent4433c993fae86634b813ee212803a84aaeedd374 (diff)
hst/fs: implement overlay fstype
This finally exposes overlay mounts in the high level hakurei API. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'hst')
-rw-r--r--hst/fs.go9
-rw-r--r--hst/fs_test.go15
-rw-r--r--hst/fsoverlay.go98
-rw-r--r--hst/fsoverlay_test.go50
-rw-r--r--hst/template.go6
-rw-r--r--hst/template_test.go9
6 files changed, 187 insertions, 0 deletions
diff --git a/hst/fs.go b/hst/fs.go
index a9ced38f..0dc95a99 100644
--- a/hst/fs.go
+++ b/hst/fs.go
@@ -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)
}
diff --git a/hst/fs_test.go b/hst/fs_test.go
index 2cdcb1e0..c251d070 100644
--- a/hst/fs_test.go
+++ b/hst/fs_test.go
@@ -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 {
diff --git a/hst/fsoverlay.go b/hst/fsoverlay.go
new file mode 100644
index 00000000..cbf7cd6d
--- /dev/null
+++ b/hst/fsoverlay.go
@@ -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)
+ }
+}
diff --git a/hst/fsoverlay_test.go b/hst/fsoverlay_test.go
new file mode 100644
index 00000000..18ed0773
--- /dev/null
+++ b/hst/fsoverlay_test.go
@@ -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"},
+ })
+}
diff --git a/hst/template.go b/hst/template.go
index db6748ec..82e0b7d9 100644
--- a/hst/template.go
+++ b/hst/template.go
@@ -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")}},
diff --git a/hst/template_test.go b/hst/template_test.go
index e3c36f58..76933783 100644
--- a/hst/template_test.go
+++ b/hst/template_test.go
@@ -104,6 +104,15 @@ func TestTemplate(t *testing.T) {
"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"
},