All checks were successful
Test / Create distribution (push) Successful in 53s
Test / Sandbox (push) Successful in 2m55s
Test / ShareFS (push) Successful in 4m55s
Test / Sandbox (race detector) (push) Successful in 5m20s
Test / Hpkg (push) Successful in 5m38s
Test / Hakurei (push) Successful in 5m49s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m52s
This directly submits the upperdir to cache. It is primarily used in bootstrapping where tools are limited and should not be used unless there is a very good reason to. Signed-off-by: Ophestra <cat@gensokyo.uk>
371 lines
9.3 KiB
Go
371 lines
9.3 KiB
Go
package pkg
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"runtime"
|
|
"slices"
|
|
"syscall"
|
|
|
|
"hakurei.app/container"
|
|
"hakurei.app/container/check"
|
|
"hakurei.app/container/fhs"
|
|
"hakurei.app/container/std"
|
|
"hakurei.app/message"
|
|
)
|
|
|
|
// AbsWork is the container pathname [CureContext.GetWorkDir] is mounted on.
|
|
var AbsWork = fhs.AbsRoot.Append("work/")
|
|
|
|
// 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 [AbsWork] and [fhs.AbsTmp] respectively. If one or more paths target
|
|
// [fhs.AbsTmp], the final entry is set up as a writable overlay mount on /tmp
|
|
// backed by the host side temporary directory. If one or more paths target
|
|
// [AbsWork], the final entry is set up as a writable overlay mount on /work for
|
|
// which the upperdir is the host side work directory. In this configuration,
|
|
// the program must avoid causing whiteout files to be created, cure fails if
|
|
// upperdir ends up with anything other than directory, regular or symlink
|
|
// entries.
|
|
//
|
|
// 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()
|
|
}
|
|
|
|
overlayTempIndex, overlayWorkIndex := -1, -1
|
|
paths := make([][2]*check.Absolute, len(a.paths))
|
|
for i, p := range a.paths {
|
|
if p.P == nil || p.A == nil {
|
|
return os.ErrInvalid
|
|
}
|
|
if p.P.Is(fhs.AbsTmp) {
|
|
overlayTempIndex = i
|
|
} else if p.P.Is(AbsWork) {
|
|
overlayWorkIndex = i
|
|
}
|
|
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:]
|
|
overlayTempIndex--
|
|
overlayWorkIndex--
|
|
}
|
|
|
|
temp, work := c.GetTempDir(), c.GetWorkDir()
|
|
for i, b := range paths {
|
|
if i == overlayTempIndex {
|
|
tempUpper := temp.Append("upper")
|
|
if err = os.MkdirAll(tempUpper.String(), 0700); err != nil {
|
|
return
|
|
}
|
|
tempWork := temp.Append("work")
|
|
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
|
|
return
|
|
}
|
|
z.Overlay(
|
|
fhs.AbsTmp,
|
|
tempUpper,
|
|
tempWork,
|
|
b[0],
|
|
)
|
|
continue
|
|
}
|
|
if i == overlayWorkIndex {
|
|
if err = os.MkdirAll(work.String(), 0700); err != nil {
|
|
return
|
|
}
|
|
tempWork := temp.Append(".work")
|
|
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
|
|
return
|
|
}
|
|
z.Overlay(
|
|
AbsWork,
|
|
work,
|
|
tempWork,
|
|
b[0],
|
|
)
|
|
continue
|
|
}
|
|
z.Bind(b[0], b[1], 0)
|
|
}
|
|
if overlayWorkIndex < 0 {
|
|
z.Bind(
|
|
work,
|
|
AbsWork,
|
|
std.BindWritable|std.BindEnsure,
|
|
)
|
|
}
|
|
if overlayTempIndex < 0 {
|
|
z.Bind(
|
|
c.GetTempDir(),
|
|
fhs.AbsTmp,
|
|
std.BindWritable|std.BindEnsure,
|
|
)
|
|
}
|
|
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
|
|
|
|
if err = z.Start(); err != nil {
|
|
return
|
|
}
|
|
if err = z.Serve(); err != nil {
|
|
return
|
|
}
|
|
if err = z.Wait(); err != nil {
|
|
return
|
|
}
|
|
|
|
// do not allow empty directories to succeed
|
|
for {
|
|
err = syscall.Rmdir(work.String())
|
|
if err != syscall.EINTR {
|
|
break
|
|
}
|
|
}
|
|
if err != nil && errors.Is(err, syscall.ENOTEMPTY) {
|
|
err = nil
|
|
}
|
|
return
|
|
}
|