Files
miandmhoennig f9c1ae835d Api socket (#5)
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #5
2026-09-10 22:09:58 +02:00

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
}