internal/pkg: implement exec artifact
All checks were successful
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m35s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m46s
Test / Hpkg (push) Successful in 4m30s
Test / Sandbox (race detector) (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 5m57s
Test / Flake checks (push) Successful in 1m43s
All checks were successful
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m35s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m46s
Test / Hpkg (push) Successful in 4m30s
Test / Sandbox (race detector) (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 5m57s
Test / Flake checks (push) Successful in 1m43s
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>
This commit is contained in:
223
internal/pkg/exec.go
Normal file
223
internal/pkg/exec.go
Normal file
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user