aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/pkg/exec.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-01-06 04:36:01 +0900
committerOphestra <cat@gensokyo.uk>2026-01-06 05:05:34 +0900
commit79adf217f4ca94fdfeb8a190fc7dc49fd26254dd (patch)
treebc9cfb855369bb6539fb11634fa0d132a6ef6a4e /internal/pkg/exec.go
parent8efffd72f4c5a0f241075fd13e4d07d71fd7fde8 (diff)
internal/pkg: implement exec artifact
This runs a program in a container environment. Artifacts can be made available to the container, they are cured concurrently and mounted in order. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/pkg/exec.go')
-rw-r--r--internal/pkg/exec.go223
1 files changed, 223 insertions, 0 deletions
diff --git a/internal/pkg/exec.go b/internal/pkg/exec.go
new file mode 100644
index 00000000..c51cd701
--- /dev/null
+++ b/internal/pkg/exec.go
@@ -0,0 +1,223 @@
+package pkg
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "os"
+ "runtime"
+
+ "hakurei.app/container"
+ "hakurei.app/container/check"
+ "hakurei.app/container/fhs"
+ "hakurei.app/container/std"
+ "hakurei.app/message"
+)
+
+// ExecContainerPath is an [Artifact] and the [check.Absolute] pathname to make
+// it available under in the container.
+type ExecContainerPath struct {
+ P *check.Absolute
+ A Artifact
+}
+
+// MustPath returns [ExecContainerPath] for pathname and [Artifact] and panics
+// if pathname is not absolute.
+func MustPath(pathname string, a Artifact) ExecContainerPath {
+ return ExecContainerPath{check.MustAbs(pathname), a}
+}
+
+// An execArtifact is an [Artifact] that produces output by running a program
+// part of another [Artifact] in a [container] to produce its output.
+type execArtifact struct {
+ // Caller-supplied context.
+ ctx context.Context
+ // Caller-supplied inner read-only bind mounts.
+ paths []ExecContainerPath
+ // Caller-supplied logging facility, passed through to [container] and used
+ // internally to produce verbose output.
+ msg message.Msg
+
+ // Number of [Artifact] to concurrently cure. A value of 0 or lower is
+ // equivalent to the value returned by [runtime.NumCPU].
+ cures int
+
+ // Passed through to [container.Params].
+ dir *check.Absolute
+ // Passed through to [container.Params].
+ env []string
+ // Passed through to [container.Params].
+ path *check.Absolute
+ // Passed through to [container.Params].
+ args []string
+}
+
+// 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.
+// A private instance of /proc and /dev is made available to the container.
+//
+// The working and temporary directories are both created and mounted writable
+// on /work and /tmp respectively.
+//
+// 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,
+
+ dir *check.Absolute,
+ env []string,
+ path *check.Absolute,
+ args []string,
+
+ paths ...ExecContainerPath,
+) Artifact {
+ return &execArtifact{ctx, paths, msg, cures, dir, env, path, args}
+}
+
+// Kind returns the hardcoded [Kind] constant.
+func (a *execArtifact) Kind() Kind { return KindExec }
+
+// Params returns paths, executable pathname and args concatenated together.
+func (a *execArtifact) Params() []byte {
+ var buf bytes.Buffer
+ for _, p := range a.paths {
+ buf.WriteString(p.P.String())
+ id := Ident(p.A)
+ buf.Write(id[:])
+ }
+ buf.WriteByte(0)
+ buf.WriteString(a.dir.String())
+ buf.WriteByte(0)
+ for _, e := range a.env {
+ buf.WriteString(e)
+ }
+ buf.WriteByte(0)
+ buf.WriteString(a.path.String())
+ buf.WriteByte(0)
+ for _, arg := range a.args {
+ buf.WriteString(arg)
+ }
+ return buf.Bytes()
+}
+
+// Dependencies returns a slice of all artifacts collected from caller-supplied
+// [ExecContainerPath].
+func (a *execArtifact) Dependencies() []Artifact {
+ artifacts := make([]Artifact, len(a.paths))
+ for i, p := range a.paths {
+ artifacts[i] = p.A
+ }
+ return artifacts
+}
+
+// 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) {
+ cures := a.cures
+ if cures < 1 {
+ cures = runtime.NumCPU()
+ }
+
+ paths := make([][2]*check.Absolute, len(a.paths))
+ for i, p := range a.paths {
+ paths[i][1] = p.P
+ }
+
+ if len(paths) > 0 {
+ type cureArtifact struct {
+ // Index of pending Artifact in paths.
+ index int
+ // Pending artifact.
+ a Artifact
+ }
+ ac := make(chan cureArtifact, len(paths))
+ for i, p := range a.paths {
+ ac <- cureArtifact{i, p.A}
+ }
+
+ type cureRes struct {
+ // Index of result in paths.
+ index int
+ // Cured pathname.
+ pathname *check.Absolute
+ // Error returned by c.
+ err error
+ }
+ res := make(chan cureRes)
+
+ for i := 0; i < cures; i++ {
+ go func() {
+ for d := range ac {
+ // computing and encoding identifier is expensive
+ if a.msg.IsVerbose() {
+ a.msg.Verbosef("curing %s...", Encode(Ident(d.a)))
+ }
+
+ var cr cureRes
+ cr.index = d.index
+ cr.pathname, _, cr.err = c.Cure(d.a)
+ res <- cr
+ }
+ }()
+ }
+
+ var count int
+ errs := make([]error, 0, len(paths))
+ for cr := range res {
+ count++
+
+ if cr.err != nil {
+ errs = append(errs, cr.err)
+ } else {
+ paths[cr.index][0] = cr.pathname
+ }
+
+ if count == len(paths) {
+ break
+ }
+ }
+ close(ac)
+ if err = errors.Join(errs...); err != nil {
+ return
+ }
+ }
+
+ ctx, cancel := context.WithCancel(a.ctx)
+ defer cancel()
+
+ z := container.New(ctx, a.msg)
+ z.ForwardCancel = true
+ z.SeccompPresets |= std.PresetStrict
+ z.ParentPerm = 0700
+ z.Hostname = "cure"
+ 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
+ }
+
+ z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args
+ z.Grow(len(paths) + 4)
+ for _, b := range paths {
+ z.Bind(b[0], b[1], 0)
+ }
+ z.Bind(
+ c.GetWorkDir(),
+ fhs.AbsRoot.Append("work"),
+ std.BindWritable|std.BindEnsure,
+ ).Bind(
+ c.GetTempDir(),
+ fhs.AbsTmp,
+ std.BindWritable|std.BindEnsure,
+ ).Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
+
+ if err = z.Start(); err != nil {
+ return
+ }
+ if err = z.Serve(); err != nil {
+ return
+ }
+ return z.Wait()
+}