diff --git a/internal/pkg/dir_test.go b/internal/pkg/dir_test.go index 8e4e198..3038526 100644 --- a/internal/pkg/dir_test.go +++ b/internal/pkg/dir_test.go @@ -274,32 +274,82 @@ func TestFlatten(t *testing.T) { }, pkg.MustDecode("GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")}, {"sample exec container", fstest.MapFS{ - ".": {Mode: fs.ModeDir | 0700}, + ".": {Mode: fs.ModeDir | 0700}, + "checksum": {Mode: fs.ModeDir | 0700}, "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9": {Mode: fs.ModeDir | 0500}, "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check": {Mode: 0400, Data: []byte{0x0}}, "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500}, "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}}, + "identifier": {Mode: fs.ModeDir | 0700}, "identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")}, "identifier/nfeISfLeFDr1k-g3hpE1oZ440kTqDdfF8TDpoLdbTPqaMMIl95oiqcvqjRkMjubA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, "identifier/oHuqV7p0v1Vd8IdAzjyYM8sfCS0P2LR5tfv5cb6Gbf2ZWUm8Ec-7hYPJ_qr183m7": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")}, + "temp": {Mode: fs.ModeDir | 0700}, "work": {Mode: fs.ModeDir | 0700}, }, []pkg.FlatEntry{ {Mode: fs.ModeDir | 0700, Path: "."}, + {Mode: fs.ModeDir | 0700, Path: "checksum"}, {Mode: fs.ModeDir | 0500, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9"}, {Mode: 0400, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check", Data: []byte{0x0}}, {Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"}, {Mode: 0400, Path: "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb", Data: []byte{}}, + {Mode: fs.ModeDir | 0700, Path: "identifier"}, {Mode: fs.ModeSymlink | 0777, Path: "identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")}, {Mode: fs.ModeSymlink | 0777, Path: "identifier/nfeISfLeFDr1k-g3hpE1oZ440kTqDdfF8TDpoLdbTPqaMMIl95oiqcvqjRkMjubA", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, {Mode: fs.ModeSymlink | 0777, Path: "identifier/oHuqV7p0v1Vd8IdAzjyYM8sfCS0P2LR5tfv5cb6Gbf2ZWUm8Ec-7hYPJ_qr183m7", Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")}, + {Mode: fs.ModeDir | 0700, Path: "temp"}, {Mode: fs.ModeDir | 0700, Path: "work"}, }, pkg.MustDecode("7PoPpWLjFPXIymbuIYLZAzOpCYr-2PN4CZ11jFdO-mDlnZNgFO3JyOtK8HW8Jxvm")}, + + {"testtool net", fstest.MapFS{ + ".": {Mode: fs.ModeDir | 0500}, + + "check": {Mode: 0400, Data: []byte("net")}, + }, []pkg.FlatEntry{ + {Mode: fs.ModeDir | 0500, Path: "."}, + + {Mode: 0400, Path: "check", Data: []byte("net")}, + }, pkg.MustDecode("a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W")}, + + {"sample exec net container", fstest.MapFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500}, + "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}}, + "checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W": {Mode: fs.ModeDir | 0500}, + "checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W/check": {Mode: 0400, Data: []byte("net")}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")}, + "identifier/I3T53NtN6HPAyyodHtq2B0clcsoS1nPdvCEb-Zc5K-hoqFGL2od1mftHhwG7gX1S": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W")}, + "identifier/nfeISfLeFDr1k-g3hpE1oZ440kTqDdfF8TDpoLdbTPqaMMIl95oiqcvqjRkMjubA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + + "temp": {Mode: fs.ModeDir | 0700}, + "work": {Mode: fs.ModeDir | 0700}, + }, []pkg.FlatEntry{ + {Mode: fs.ModeDir | 0700, Path: "."}, + + {Mode: fs.ModeDir | 0700, Path: "checksum"}, + {Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"}, + {Mode: 0400, Path: "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb", Data: []byte{}}, + {Mode: fs.ModeDir | 0500, Path: "checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W"}, + {Mode: 0400, Path: "checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W/check", Data: []byte("net")}, + + {Mode: fs.ModeDir | 0700, Path: "identifier"}, + {Mode: fs.ModeSymlink | 0777, Path: "identifier/I3T53NtN6HPAyyodHtq2B0clcsoS1nPdvCEb-Zc5K-hoqFGL2od1mftHhwG7gX1S", Data: []byte("../checksum/a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W")}, + {Mode: fs.ModeSymlink | 0777, Path: "identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")}, + {Mode: fs.ModeSymlink | 0777, Path: "identifier/nfeISfLeFDr1k-g3hpE1oZ440kTqDdfF8TDpoLdbTPqaMMIl95oiqcvqjRkMjubA", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + + {Mode: fs.ModeDir | 0700, Path: "temp"}, + {Mode: fs.ModeDir | 0700, Path: "work"}, + }, pkg.MustDecode("bBQVFIt0FnOulljgpLnGtuzHSFgwiCMjc4pmc4rHRqXKQ60Q5aBVYp5f6aH9VdZi")}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/pkg/exec.go b/internal/pkg/exec.go index 539bd93..930624f 100644 --- a/internal/pkg/exec.go +++ b/internal/pkg/exec.go @@ -6,6 +6,7 @@ import ( "errors" "os" "runtime" + "slices" "hakurei.app/container" "hakurei.app/container/check" @@ -29,6 +30,9 @@ func MustPath(pathname string, a Artifact) ExecContainerPath { // An execArtifact is an [Artifact] that produces output by running a program // part of another [Artifact] in a [container] to produce its output. +// +// Methods of execArtifact does not modify any struct field or underlying arrays +// referred to by slices. type execArtifact struct { // Caller-supplied context. ctx context.Context @@ -52,6 +56,33 @@ type execArtifact struct { args []string } +// execNetArtifact is like execArtifact but implements [KnownChecksum] and has +// its resulting container keep the host net namespace. +type execNetArtifact struct { + checksum Checksum + + execArtifact +} + +var _ KnownChecksum = new(execNetArtifact) + +// Checksum returns the caller-supplied checksum. +func (a *execNetArtifact) Checksum() Checksum { return a.checksum } + +// Kind returns the hardcoded [Kind] constant. +func (a *execNetArtifact) Kind() Kind { return KindExecNet } + +// Params is [Checksum] concatenated with [KindExec] params. +func (a *execNetArtifact) Params() []byte { + return slices.Concat(a.checksum[:], a.execArtifact.Params()) +} + +// Cure cures the [Artifact] by curing all its dependencies then running the +// container described by the caller. The container retains host networking. +func (a *execNetArtifact) Cure(c *CureContext) error { + return a.cure(c, true) +} + // NewExec returns a new [Artifact] bounded by ctx, it cures all [Artifact] // in paths at the specified maximum concurrent cures limit. Specified paths are // bind mounted read-only in the specified order in the resulting container. @@ -60,12 +91,16 @@ type execArtifact struct { // The working and temporary directories are both created and mounted writable // on /work and /tmp respectively. // +// If checksum is non-nil, the resulting [Artifact] implements [KnownChecksum] +// and its container runs in the host net namespace. +// // A cures value of 0 or lower is equivalent to the value returned by // [runtime.NumCPU]. func NewExec( ctx context.Context, msg message.Msg, cures int, + checksum *Checksum, dir *check.Absolute, env []string, @@ -74,7 +109,11 @@ func NewExec( paths ...ExecContainerPath, ) Artifact { - return &execArtifact{ctx, paths, msg, cures, dir, env, path, args} + a := execArtifact{ctx, paths, msg, cures, dir, env, path, args} + if checksum == nil { + return &a + } + return &execNetArtifact{*checksum, a} } // Kind returns the hardcoded [Kind] constant. @@ -126,6 +165,12 @@ func (a *execArtifact) Dependencies() []Artifact { // Cure cures the [Artifact] by curing all its dependencies then running the // container described by the caller. func (a *execArtifact) Cure(c *CureContext) (err error) { + return a.cure(c, false) +} + +// cure is like Cure but allows optional host net namespace. This is used for +// the [KnownChecksum] variant where networking is allowed. +func (a *execArtifact) cure(c *CureContext, hostNet bool) (err error) { cures := a.cures if cures < 1 { cures = runtime.NumCPU() @@ -205,7 +250,11 @@ func (a *execArtifact) Cure(c *CureContext) (err error) { z.ForwardCancel = true z.SeccompPresets |= std.PresetStrict z.ParentPerm = 0700 + z.HostNet = hostNet z.Hostname = "cure" + if z.HostNet { + z.Hostname = "cure-net" + } z.Uid, z.Gid = (1<<10)-1, (1<<10)-1 if a.msg.IsVerbose() { z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr diff --git a/internal/pkg/exec_test.go b/internal/pkg/exec_test.go index 0ac0601..1b2f84a 100644 --- a/internal/pkg/exec_test.go +++ b/internal/pkg/exec_test.go @@ -39,6 +39,7 @@ func TestExec(t *testing.T) { t.Context(), msg, 0, + nil, check.MustAbs("/work"), []string{"HAKUREI_TEST=1"}, check.MustAbs("/opt/bin/testtool"), @@ -66,6 +67,7 @@ func TestExec(t *testing.T) { t.Context(), msg, 0, + nil, check.MustAbs("/work"), []string{"HAKUREI_TEST=1"}, check.MustAbs("/opt/bin/testtool"), @@ -84,6 +86,7 @@ func TestExec(t *testing.T) { t.Context(), msg, 0, + nil, check.MustAbs("/work"), []string{"HAKUREI_TEST=1"}, check.MustAbs("/opt/bin/testtool"), @@ -99,6 +102,7 @@ func TestExec(t *testing.T) { t.Context(), msg, 0, + nil, check.MustAbs("/work"), nil, check.MustAbs("/opt/bin/testtool"), @@ -110,6 +114,47 @@ func TestExec(t *testing.T) { testtoolDestroy(t, base, c) }, pkg.MustDecode("7PoPpWLjFPXIymbuIYLZAzOpCYr-2PN4CZ11jFdO-mDlnZNgFO3JyOtK8HW8Jxvm")}, + + {"net", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + c.SetStrict(true) + testtool, testtoolDestroy := newTesttool() + + msg := message.New(log.New(os.Stderr, "container: ", 0)) + msg.SwapVerbose(testing.Verbose()) + + wantChecksum := pkg.MustDecode( + "a1F_i9PVQI4qMcoHgTQkORuyWLkC1GLIxOhDt2JpU1NGAxWc5VJzdlfRK-PYBh3W", + ) + cureMany(t, c, []cureStep{ + {"container", pkg.NewExec( + t.Context(), + msg, + 0, + &wantChecksum, + check.MustAbs("/work"), + []string{"HAKUREI_TEST=1"}, + check.MustAbs("/opt/bin/testtool"), + []string{"testtool", "net"}, + + pkg.MustPath("/file", newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xfe, 0}, + nil, + nil, nil, + )), + pkg.MustPath("/.hakurei", stubArtifact{ + kind: pkg.KindTar, + params: []byte("empty directory"), + cure: func(c *pkg.CureContext) error { + return os.MkdirAll(c.GetWorkDir().String(), 0700) + }, + }), + pkg.MustPath("/opt", testtool), + ), ignorePathname, wantChecksum, nil}, + }) + + testtoolDestroy(t, base, c) + }, pkg.MustDecode("bBQVFIt0FnOulljgpLnGtuzHSFgwiCMjc4pmc4rHRqXKQ60Q5aBVYp5f6aH9VdZi")}, }) } diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go index 00d6cf7..423aea1 100644 --- a/internal/pkg/pkg.go +++ b/internal/pkg/pkg.go @@ -185,6 +185,9 @@ const ( KindTar // KindExec is the kind of [Artifact] returned by [NewExec]. KindExec + // KindExecNet is the kind of [Artifact] returned by [NewExec] but with a + // non-nil checksum. + KindExecNet ) // Ident returns a deterministic identifier for the supplied params and diff --git a/internal/pkg/testdata/main.go b/internal/pkg/testdata/main.go index d63acdb..2a0144f 100644 --- a/internal/pkg/testdata/main.go +++ b/internal/pkg/testdata/main.go @@ -16,7 +16,13 @@ func main() { log.SetFlags(0) log.SetPrefix("testtool: ") - if wantArgs := []string{"testtool"}; !slices.Equal(os.Args, wantArgs) { + var hostNet bool + wantArgs := []string{"testtool"} + if len(os.Args) == 2 { + hostNet = true + wantArgs = []string{"testtool", "net"} + } + if !slices.Equal(os.Args, wantArgs) { log.Fatalf("Args: %q, want %q", os.Args, wantArgs) } if wantEnv := []string{ @@ -31,6 +37,22 @@ func main() { log.Fatalf("Executable: %q, want %q", got, wantExec) } + wantHostname := "cure" + if hostNet { + wantHostname += "-net" + } + + if hostname, err := os.Hostname(); err != nil { + log.Fatalf("Hostname: error = %v", err) + } else if hostname != wantHostname { + log.Fatalf("Hostname: %q, want %q", hostname, wantHostname) + } + + ident := "oHuqV7p0v1Vd8IdAzjyYM8sfCS0P2LR5tfv5cb6Gbf2ZWUm8Ec-7hYPJ_qr183m7" + if hostNet { + ident = "I3T53NtN6HPAyyodHtq2B0clcsoS1nPdvCEb-Zc5K-hoqFGL2od1mftHhwG7gX1S" + } + var m *vfs.MountInfo if f, err := os.Open(fhs.Proc + "self/mountinfo"); err != nil { log.Fatalf("Open: error = %v", err) @@ -64,14 +86,12 @@ func main() { next() // testtool artifact next() - if path.Base(m.Root) != - "oHuqV7p0v1Vd8IdAzjyYM8sfCS0P2LR5tfv5cb6Gbf2ZWUm8Ec-7hYPJ_qr183m7" || m.Target != "/work" { + if path.Base(m.Root) != ident || m.Target != "/work" { log.Fatal("unexpected work mount entry") } next() - if path.Base(m.Root) != - "oHuqV7p0v1Vd8IdAzjyYM8sfCS0P2LR5tfv5cb6Gbf2ZWUm8Ec-7hYPJ_qr183m7" || m.Target != "/tmp" { + if path.Base(m.Root) != ident || m.Target != "/tmp" { log.Fatal("unexpected temp mount entry") } @@ -97,7 +117,11 @@ func main() { os.Exit(1) } - if err := os.WriteFile("check", []byte{0}, 0400); err != nil { + checkData := []byte{0} + if hostNet { + checkData = []byte("net") + } + if err := os.WriteFile("check", checkData, 0400); err != nil { log.Fatal(err) } }