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 }