From 92b61889a6db7e5e351cade9c871a9d670917ee9 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Wed, 17 Jun 2026 02:16:57 +0900 Subject: [PATCH] hst: support ephemeral overlay mounts This is useful for reusing a readonly template without autoroot. Signed-off-by: Ophestra --- check/absolute.go | 6 ++++++ hst/fs.go | 2 ++ hst/fs_test.go | 9 +++++++-- hst/fsoverlay.go | 36 ++++++++++++++++++++++++--------- hst/fsoverlay_test.go | 14 ++++++++++++- internal/outcome/spcontainer.go | 4 ++++ 6 files changed, 59 insertions(+), 12 deletions(-) diff --git a/check/absolute.go b/check/absolute.go index 1a050d06..c5970abb 100644 --- a/check/absolute.go +++ b/check/absolute.go @@ -31,6 +31,8 @@ func (e AbsoluteError) Is(target error) bool { type Absolute struct{ pathname unique.Handle[string] } var ( + _ fmt.GoStringer = new(Absolute) + _ encoding.TextAppender = new(Absolute) _ encoding.TextMarshaler = new(Absolute) _ encoding.TextUnmarshaler = new(Absolute) @@ -40,6 +42,10 @@ var ( _ encoding.BinaryUnmarshaler = new(Absolute) ) +func (a *Absolute) GoString() string { + return fmt.Sprintf("check.MustAbs(%q)", a.String()) +} + // ok returns whether [Absolute] is not the zero value. func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) } diff --git a/hst/fs.go b/hst/fs.go index cb35f925..2f2c377c 100644 --- a/hst/fs.go +++ b/hst/fs.go @@ -37,6 +37,8 @@ type Ops interface { Bind(source, target *check.Absolute, flags int) Ops // Overlay appends an op that mounts the overlay pseudo filesystem. Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops + // OverlayEphemeral appends a MountOverlayOp with an ephemeral upperdir and workdir. + OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) Ops // OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly. OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops diff --git a/hst/fs_test.go b/hst/fs_test.go index a88970b7..8ef268fc 100644 --- a/hst/fs_test.go +++ b/hst/fs_test.go @@ -3,6 +3,7 @@ package hst_test import ( "encoding/json" "errors" + "fmt" "os" "reflect" "strings" @@ -283,11 +284,11 @@ func checkFs(t *testing.T, testCases []fsTestCase) { if !reflect.DeepEqual(ops, &tc.ops) { gotString := new(strings.Builder) for _, op := range *ops { - gotString.WriteString("\n" + op.String()) + gotString.WriteString("\n" + fmt.Sprintf("%#v", op)) } wantString := new(strings.Builder) for _, op := range tc.ops { - wantString.WriteString("\n" + op.String()) + wantString.WriteString("\n" + fmt.Sprintf("%#v", op)) } t.Errorf("Apply: %s, want %s", gotString, wantString) } @@ -339,6 +340,10 @@ func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*chec return opsAdapter{p.Ops.Overlay(target, state, work, layers...)} } +func (p opsAdapter) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) hst.Ops { + return opsAdapter{p.Ops.OverlayEphemeral(target, layers...)} +} + func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops { return opsAdapter{p.Ops.OverlayReadonly(target, layers...)} } diff --git a/hst/fsoverlay.go b/hst/fsoverlay.go index b06c1e7e..16a5e335 100644 --- a/hst/fsoverlay.go +++ b/hst/fsoverlay.go @@ -2,6 +2,7 @@ package hst import ( "encoding/gob" + "slices" "strings" "hakurei.app/check" @@ -40,7 +41,7 @@ func (o *FSOverlay) Valid() bool { } if o.Upper != nil { // rw - return o.Work != nil && len(o.Lower) > 0 + return o.Work != nil || len(o.Lower) > 0 } else { // ro return len(o.Lower) >= 2 } @@ -58,8 +59,11 @@ func (o *FSOverlay) Host() []*check.Absolute { return nil } p := make([]*check.Absolute, 0, 2+len(o.Lower)) - if o.Upper != nil && o.Work != nil { - p = append(p, o.Upper, o.Work) + if o.Upper != nil { + p = append(p, o.Upper) + if o.Work != nil { + p = append(p, o.Work) + } } p = append(p, o.Lower...) return p @@ -70,11 +74,18 @@ func (o *FSOverlay) Apply(z *ApplyState) { return } - if o.Upper != nil && o.Work != nil { - z.Overlay(o.Target, o.Upper, o.Work, o.Lower...) + if o.Upper != nil { if o.Target.Is(fhs.AbsRoot) { z.NoRemountRoot = true } + if o.Work != nil { + z.Overlay(o.Target, o.Upper, o.Work, o.Lower...) + } else { + z.OverlayEphemeral(o.Target, slices.Concat( + o.Lower, + []*check.Absolute{o.Upper})..., + ) + } } else { z.OverlayReadonly(o.Target, o.Lower...) } @@ -90,12 +101,19 @@ func (o *FSOverlay) String() string { lower[i] = check.EscapeOverlayDataSegment(a.String()) } - if o.Upper != nil && o.Work != nil { - return "w*" + strings.Join(append([]string{ + if o.Upper != nil { + if o.Work != nil { + return "w*" + strings.Join(append([]string{ + check.EscapeOverlayDataSegment(o.Target.String()), + check.EscapeOverlayDataSegment(o.Upper.String()), + check.EscapeOverlayDataSegment(o.Work.String())}, + lower...), check.SpecialOverlayPath) + } + return "e*" + strings.Join(append([]string{ check.EscapeOverlayDataSegment(o.Target.String()), - check.EscapeOverlayDataSegment(o.Upper.String()), - check.EscapeOverlayDataSegment(o.Work.String())}, + check.EscapeOverlayDataSegment(o.Upper.String())}, lower...), check.SpecialOverlayPath) + } else { return "*" + strings.Join(append([]string{ check.EscapeOverlayDataSegment(o.Target.String())}, diff --git a/hst/fsoverlay_test.go b/hst/fsoverlay_test.go index 1b14e7c9..e1ae21b5 100644 --- a/hst/fsoverlay_test.go +++ b/hst/fsoverlay_test.go @@ -5,6 +5,7 @@ import ( "hakurei.app/check" "hakurei.app/container" + "hakurei.app/fhs" "hakurei.app/hst" ) @@ -14,7 +15,7 @@ func TestFSOverlay(t *testing.T) { checkFs(t, []fsTestCase{ {"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, ""}, {"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*check.Absolute{nil}}, false, nil, nil, nil, ""}, - {"zero lower", &hst.FSOverlay{Target: m("/etc"), Upper: m("/"), Work: m("/")}, false, nil, nil, nil, ""}, + {"zero lower", &hst.FSOverlay{Target: m("/etc"), Work: m("/")}, false, nil, nil, nil, ""}, {"zero lower ro", &hst.FSOverlay{Target: m("/etc")}, false, nil, nil, nil, ""}, {"short lower", &hst.FSOverlay{Target: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, ""}, @@ -62,5 +63,16 @@ func TestFSOverlay(t *testing.T) { Work: m("/tmp/work"), }}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"), "w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"}, + + {"ephemeral", &hst.FSOverlay{ + Target: m("/"), + Lower: ms("/tmp/.src0", "/tmp/.src1"), + Upper: m("/tmp/upper"), + }, true, container.Ops{&container.MountOverlayOp{ + Target: m("/"), + Lower: ms("/tmp/.src0", "/tmp/.src1", "/tmp/upper"), + Upper: fhs.AbsRoot, + }}, m("/"), ms("/tmp/upper", "/tmp/.src0", "/tmp/.src1"), + "e*/:/tmp/upper:/tmp/.src0:/tmp/.src1"}, }) } diff --git a/internal/outcome/spcontainer.go b/internal/outcome/spcontainer.go index d9398d2c..97d98dd1 100644 --- a/internal/outcome/spcontainer.go +++ b/internal/outcome/spcontainer.go @@ -382,6 +382,10 @@ func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*chec return opsAdapter{p.Ops.Overlay(target, state, work, layers...)} } +func (p opsAdapter) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) hst.Ops { + return opsAdapter{p.Ops.OverlayEphemeral(target, layers...)} +} + func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops { return opsAdapter{p.Ops.OverlayReadonly(target, layers...)} }