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() }