diff options
Diffstat (limited to 'internal/pkg/exec.go')
| -rw-r--r-- | internal/pkg/exec.go | 223 |
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() +} |
