diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-08-15 03:30:51 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-08-15 04:00:55 +0900 |
| commit | 9ed3ba85eaba4bc4bd12b290dc4daf00ffe159dc (patch) | |
| tree | 03301910c40c59eb94029f1a93b1e74e21b5226a /hst | |
| parent | 4433c993fae86634b813ee212803a84aaeedd374 (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.go | 9 | ||||
| -rw-r--r-- | hst/fs_test.go | 15 | ||||
| -rw-r--r-- | hst/fsoverlay.go | 98 | ||||
| -rw-r--r-- | hst/fsoverlay_test.go | 50 | ||||
| -rw-r--r-- | hst/template.go | 6 | ||||
| -rw-r--r-- | hst/template_test.go | 9 |
6 files changed, 187 insertions, 0 deletions
@@ -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" }, |
