All checks were successful
Test / Create distribution (push) Successful in 53s
Test / Sandbox (push) Successful in 3m9s
Test / ShareFS (push) Successful in 4m48s
Test / Sandbox (race detector) (push) Successful in 5m24s
Test / Hakurei (push) Successful in 5m37s
Test / Hpkg (push) Successful in 5m34s
Test / Hakurei (race detector) (push) Successful in 7m24s
Test / Flake checks (push) Successful in 1m46s
This makes it possible to use an Artifact as root without arranging for directory creation in the Artifact ahead of time. Signed-off-by: Ophestra <cat@gensokyo.uk>
297 lines
7.5 KiB
Go
297 lines
7.5 KiB
Go
package pkg
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"runtime"
|
|
"slices"
|
|
|
|
"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.
|
|
//
|
|
// 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
|
|
// 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
|
|
}
|
|
|
|
// 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.
|
|
// 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.
|
|
//
|
|
// If the first path targets [fhs.AbsRoot], it is made writable via an overlay
|
|
// mount with writes going to an ephemeral tmpfs bound to the lifetime of the
|
|
// container. This is primarily to make it possible for [container] to set up
|
|
// mount points targeting paths not available in the [Artifact] backing root,
|
|
// and to accommodate poorly written programs that insist on writing to awkward
|
|
// paths, it must not be used as scratch space.
|
|
//
|
|
// 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,
|
|
path *check.Absolute,
|
|
args []string,
|
|
|
|
paths ...ExecContainerPath,
|
|
) Artifact {
|
|
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.
|
|
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 {
|
|
if p.P != nil {
|
|
buf.WriteString(p.P.String())
|
|
} else {
|
|
buf.WriteString("invalid P\x00")
|
|
}
|
|
if p.A != nil {
|
|
id := Ident(p.A)
|
|
buf.Write(id[:])
|
|
} else {
|
|
buf.WriteString("invalid A\x00")
|
|
}
|
|
}
|
|
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, 0, len(a.paths))
|
|
for _, p := range a.paths {
|
|
if p.A != nil {
|
|
artifacts = append(artifacts, 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) {
|
|
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()
|
|
}
|
|
|
|
paths := make([][2]*check.Absolute, len(a.paths))
|
|
for i, p := range a.paths {
|
|
if p.P == nil || p.A == nil {
|
|
return os.ErrInvalid
|
|
}
|
|
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.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
|
|
}
|
|
|
|
z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args
|
|
z.Grow(len(paths) + 4)
|
|
if len(paths) > 0 && paths[0][1].Is(fhs.AbsRoot) {
|
|
z.OverlayEphemeral(fhs.AbsRoot, paths[0][0])
|
|
paths = paths[1:]
|
|
}
|
|
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()
|
|
}
|