143 lines
4.0 KiB
Go
143 lines
4.0 KiB
Go
package instance
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"werkdock/internal/engine"
|
|
)
|
|
|
|
// An Exec is a second command in a running instance's sandbox, kept as
|
|
// a record of its own because docker's API creates it, starts it, and
|
|
// asks for its exit code in three separate calls (RFC 0005 milestone 3,
|
|
// docs/testcontainers-api.md). Records live in the instance's exec/
|
|
// directory and go with it.
|
|
type Exec struct {
|
|
ID string `json:"id"`
|
|
InstanceID string `json:"instanceId"`
|
|
Cmd []string `json:"cmd"`
|
|
Env []engine.EnvVar `json:"env,omitempty"`
|
|
Workdir string `json:"workdir,omitempty"`
|
|
User string `json:"user,omitempty"`
|
|
Started bool `json:"started"`
|
|
Running bool `json:"running"`
|
|
ExitCode int `json:"exitCode"`
|
|
StartedAt time.Time `json:"startedAt,omitempty"`
|
|
FinishedAt time.Time `json:"finishedAt,omitempty"`
|
|
dir string
|
|
}
|
|
|
|
func execDir(instanceDir string) string { return filepath.Join(instanceDir, "exec") }
|
|
|
|
// CreateExec records a command to be run in inst later.
|
|
func (m Manager) CreateExec(inst *Instance, e Exec) (*Exec, error) {
|
|
if len(e.Cmd) == 0 {
|
|
return nil, errors.New("exec needs a command")
|
|
}
|
|
raw := make([]byte, 32)
|
|
if _, err := rand.Read(raw); err != nil {
|
|
return nil, err
|
|
}
|
|
e.ID = hex.EncodeToString(raw)
|
|
e.InstanceID = inst.Config.ID
|
|
e.dir = execDir(inst.Dir)
|
|
if err := os.MkdirAll(e.dir, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
return &e, e.save()
|
|
}
|
|
|
|
func (e *Exec) path() string { return filepath.Join(e.dir, e.ID+".json") }
|
|
|
|
func (e *Exec) save() error { return writeJSON(e.path(), e) }
|
|
|
|
// GetExec finds an exec record by id across all instances, together
|
|
// with the instance it belongs to.
|
|
func (m Manager) GetExec(id string) (*Exec, *Instance, error) {
|
|
if !isHex(id) {
|
|
return nil, nil, fmt.Errorf("%w: exec %s", ErrNotFound, id)
|
|
}
|
|
all, err := m.List()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, inst := range all {
|
|
path := filepath.Join(execDir(inst.Dir), id+".json")
|
|
var e Exec
|
|
if err := readJSON(path, &e); err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, nil, err
|
|
}
|
|
e.dir = execDir(inst.Dir)
|
|
return &e, inst, nil
|
|
}
|
|
return nil, nil, fmt.Errorf("%w: exec %s", ErrNotFound, id)
|
|
}
|
|
|
|
func isHex(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
return strings.IndexFunc(s, func(r rune) bool {
|
|
return !strings.ContainsRune("0123456789abcdefABCDEF", r)
|
|
}) < 0
|
|
}
|
|
|
|
// RunExec executes the recorded command in the instance's sandbox and
|
|
// records the exit code. The sandbox is the instance's: same rootfs,
|
|
// mounts, uid and environment, with the exec's own overrides; not the
|
|
// same pid namespace.
|
|
func (m Manager) RunExec(e *Exec, inst *Instance, eng engine.Engine, stdout, stderr io.Writer) (int, error) {
|
|
if e.Started {
|
|
return 0, fmt.Errorf("exec %s has already been started", e.ID[:12])
|
|
}
|
|
spec, err := e.spec(inst)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
e.Started, e.Running, e.StartedAt = true, true, time.Now()
|
|
if err := e.save(); err != nil {
|
|
return 0, err
|
|
}
|
|
if b, ok := eng.(*engine.Bwrap); ok {
|
|
b.Stdout, b.Stderr = stdout, stderr
|
|
}
|
|
code, runErr := eng.Run(spec)
|
|
e.Running, e.FinishedAt, e.ExitCode = false, time.Now(), code
|
|
if runErr != nil {
|
|
e.ExitCode = ExitCodeLost
|
|
}
|
|
if err := e.save(); err != nil && runErr == nil {
|
|
runErr = err
|
|
}
|
|
return code, runErr
|
|
}
|
|
|
|
// spec is the instance's RunSpec with the exec's command and overrides.
|
|
func (e *Exec) spec(inst *Instance) (engine.RunSpec, error) {
|
|
spec := inst.Config.Spec
|
|
spec.Command = e.Cmd
|
|
spec.Mounts = append([]engine.Mount{}, inst.Config.Spec.Mounts...)
|
|
spec.Env = append(append([]engine.EnvVar{}, inst.Config.Spec.Env...), e.Env...)
|
|
if e.Workdir != "" {
|
|
spec.Workdir = e.Workdir
|
|
}
|
|
if e.User != "" {
|
|
u, err := engine.LookupUser(spec.RootFS, e.User)
|
|
if err != nil {
|
|
return engine.RunSpec{}, fmt.Errorf("user: %w", err)
|
|
}
|
|
spec.UID, spec.GID, spec.Home = u.UID, u.GID, u.Home
|
|
}
|
|
return spec, nil
|
|
}
|