239 lines
6.7 KiB
Go
239 lines
6.7 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"werkdock/internal/engine"
|
|
"werkdock/internal/instance"
|
|
)
|
|
|
|
// stdout and stderr as docker's stream multiplexing numbers them.
|
|
const (
|
|
streamStdout byte = 1
|
|
streamStderr byte = 2
|
|
)
|
|
|
|
// frameWriter wraps a stream in docker's frame format: an 8-byte
|
|
// header per write, the stream number first and the payload length in
|
|
// the last four bytes, big-endian. Every frame is flushed, so a client
|
|
// following a log sees output as it happens.
|
|
type frameWriter struct {
|
|
w io.Writer
|
|
stream byte
|
|
flush func()
|
|
}
|
|
|
|
func (f *frameWriter) Write(p []byte) (int, error) {
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
var header [8]byte
|
|
header[0] = f.stream
|
|
binary.BigEndian.PutUint32(header[4:], uint32(len(p)))
|
|
if _, err := f.w.Write(header[:]); err != nil {
|
|
return 0, err
|
|
}
|
|
n, err := f.w.Write(p)
|
|
if f.flush != nil {
|
|
f.flush()
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func framed(w http.ResponseWriter) (stdout, stderr io.Writer) {
|
|
flush := func() {
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
return &frameWriter{w: w, stream: streamStdout, flush: flush},
|
|
&frameWriter{w: w, stream: streamStderr, flush: flush}
|
|
}
|
|
|
|
// containerLogs streams the recorded output in docker's frame format.
|
|
func (s *Server) containerLogs(w http.ResponseWriter, r *http.Request) {
|
|
inst, ok := s.instanceOf(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
wantOut, wantErr := boolParam(r, "stdout"), boolParam(r, "stderr")
|
|
if !wantOut && !wantErr {
|
|
writeError(w, http.StatusBadRequest, "logs needs stdout or stderr")
|
|
return
|
|
}
|
|
// Parameters that would change what the caller gets are refused
|
|
// rather than ignored: a caller who asked for the last ten lines
|
|
// and silently received ten thousand is worse off than one who was
|
|
// told no (RFC 0002).
|
|
if err := checkLogParams(r); err != nil {
|
|
writeError(w, http.StatusNotImplemented, "%v", err)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
w.WriteHeader(http.StatusOK)
|
|
stdout, stderr := framed(w)
|
|
if !wantOut {
|
|
stdout = io.Discard
|
|
}
|
|
if !wantErr {
|
|
stderr = io.Discard
|
|
}
|
|
stop := make(chan struct{})
|
|
done := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-r.Context().Done():
|
|
close(stop)
|
|
case <-done:
|
|
}
|
|
}()
|
|
defer close(done)
|
|
if err := s.Manager.Logs(inst, stdout, stderr, boolParam(r, "follow"), stop); err != nil {
|
|
s.logf("logs %s: %v", inst.ShortID(), err)
|
|
}
|
|
}
|
|
|
|
// checkLogParams refuses the log parameters werkdock cannot honour.
|
|
// Werkdock records the output as the command wrote it, without
|
|
// timestamps, so anything that selects by time or by line count has
|
|
// nothing to select on.
|
|
func checkLogParams(r *http.Request) error {
|
|
q := r.URL.Query()
|
|
if tail := q.Get("tail"); tail != "" && tail != "all" && tail != "-1" {
|
|
return fmt.Errorf("logs tail=%s is not available: werkdock streams the recorded output whole; ask for all of it and cut it yourself", tail)
|
|
}
|
|
if boolParam(r, "timestamps") {
|
|
return errors.New("logs timestamps are not available: werkdock records the output as written, without a clock beside it")
|
|
}
|
|
for _, name := range []string{"since", "until"} {
|
|
if v := q.Get(name); v != "" && v != "0" {
|
|
return fmt.Errorf("logs %s=%s is not available: werkdock records no timestamps to select by", name, v)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// execCreateRequest is docker's exec create body.
|
|
type execCreateRequest struct {
|
|
Cmd []string `json:"Cmd"`
|
|
Env []string `json:"Env"`
|
|
User string `json:"User"`
|
|
WorkingDir string `json:"WorkingDir"`
|
|
AttachStdout bool `json:"AttachStdout"`
|
|
AttachStderr bool `json:"AttachStderr"`
|
|
Tty bool `json:"Tty"`
|
|
Privileged bool `json:"Privileged"`
|
|
}
|
|
|
|
func (s *Server) execCreate(w http.ResponseWriter, r *http.Request) {
|
|
inst, ok := s.instanceOf(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req execCreateRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid exec body: %v", err)
|
|
return
|
|
}
|
|
if len(req.Cmd) == 0 {
|
|
writeError(w, http.StatusBadRequest, "exec needs a Cmd")
|
|
return
|
|
}
|
|
if !inst.Running() {
|
|
writeError(w, http.StatusConflict, "container %s is not running", inst.ShortID())
|
|
return
|
|
}
|
|
var env []engine.EnvVar
|
|
for _, e := range req.Env {
|
|
key, value, _ := strings.Cut(e, "=")
|
|
env = append(env, engine.EnvVar{Key: key, Value: value})
|
|
}
|
|
rec, err := s.Manager.CreateExec(inst, instance.Exec{
|
|
Cmd: req.Cmd, Env: env, Workdir: req.WorkingDir, User: req.User,
|
|
})
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "%v", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, map[string]string{"Id": rec.ID})
|
|
}
|
|
|
|
// execStart runs the recorded command and streams its output framed.
|
|
// Docker hijacks the connection here only for an attached stdin; with
|
|
// AttachStdout/Stderr alone it is a plain response body, which is what
|
|
// a client sends (docs/testcontainers-api.md).
|
|
func (s *Server) execStart(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
rec, inst, err := s.Manager.GetExec(id)
|
|
if err != nil {
|
|
if errors.Is(err, instance.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "no such exec: %s", id)
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "%v", err)
|
|
return
|
|
}
|
|
var body struct {
|
|
Detach bool `json:"Detach"`
|
|
Tty bool `json:"Tty"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
if body.Detach {
|
|
writeError(w, http.StatusNotImplemented,
|
|
"a detached exec has no instance to record it; werkdock runs an exec attached (werkdock run -d starts a second service)")
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/vnd.docker.raw-stream")
|
|
w.WriteHeader(http.StatusOK)
|
|
stdout, stderr := framed(w)
|
|
code, err := s.Manager.RunExec(rec, inst, &engine.Bwrap{}, stdout, stderr)
|
|
if err != nil {
|
|
s.logf("exec %s: %v", id[:12], err)
|
|
return
|
|
}
|
|
s.logf("exec %s in %s exited %d", id[:12], inst.ShortID(), code)
|
|
}
|
|
|
|
func (s *Server) execInspect(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
rec, inst, err := s.Manager.GetExec(id)
|
|
if err != nil {
|
|
if errors.Is(err, instance.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "no such exec: %s", id)
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "%v", err)
|
|
return
|
|
}
|
|
entrypoint := ""
|
|
var args []string
|
|
if len(rec.Cmd) > 0 {
|
|
entrypoint, args = rec.Cmd[0], rec.Cmd[1:]
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ID": rec.ID,
|
|
"ContainerID": inst.Config.ID,
|
|
"Running": rec.Running,
|
|
"ExitCode": rec.ExitCode,
|
|
"OpenStdin": false,
|
|
"OpenStdout": true,
|
|
"OpenStderr": true,
|
|
"CanRemove": true,
|
|
"DetachKeys": "",
|
|
"Pid": 0,
|
|
"ProcessConfig": map[string]interface{}{
|
|
"entrypoint": entrypoint,
|
|
"arguments": args,
|
|
"privileged": false,
|
|
"tty": false,
|
|
"user": rec.User,
|
|
},
|
|
})
|
|
}
|