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