The store had no working tools: its TypeScript client was never built, and nothing could count what the store holds that no record names. A Go bundle lists the store's files through its own container, reads each manifest through its door, and sets that beside the controller's records. Collection is asked of the controller, which decides and records; the store's tools never delete. The image.pushed event was declared and never emitted, and nothing consumes it, so it goes.
97 lines
3.5 KiB
Go
97 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Ran is what a command did: its output, its exit status, and why it never ran to an answer.
|
|
type Ran struct {
|
|
Stdout string
|
|
Stderr string
|
|
Status int
|
|
// Err is "ENOENT" when the program is not installed, or says it was ended for taking too long.
|
|
Err string
|
|
}
|
|
|
|
// Runner runs one command, so every tool can be tested without a daemon.
|
|
type Runner func(ctx context.Context, name string, args ...string) Ran
|
|
|
|
// ListTimeout is how long listing the store's files may take: below the runtime's thirty-second call
|
|
// limit, so a store that hangs is answered as such rather than as a call the runtime gave up on.
|
|
const ListTimeout = 15 * time.Second
|
|
|
|
// ExecRunner runs a command on this machine, bounded by ListTimeout.
|
|
func ExecRunner(ctx context.Context, name string, args ...string) Ran {
|
|
ctx, cancel := context.WithTimeout(ctx, ListTimeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
var out, errb bytes.Buffer
|
|
cmd.Stdout, cmd.Stderr = &out, &errb
|
|
err := cmd.Run()
|
|
r := Ran{Stdout: out.String(), Stderr: errb.String()}
|
|
var exitErr *exec.ExitError
|
|
switch {
|
|
case errors.Is(ctx.Err(), context.DeadlineExceeded):
|
|
r.Status, r.Err = 124, fmt.Sprintf("no answer within %d s", int(ListTimeout/time.Second))
|
|
case errors.Is(err, exec.ErrNotFound):
|
|
r.Status, r.Err = 127, "ENOENT"
|
|
case errors.As(err, &exitErr):
|
|
r.Status = exitErr.ExitCode()
|
|
case err != nil:
|
|
r.Status, r.Err = 1, err.Error()
|
|
}
|
|
return r
|
|
}
|
|
|
|
var socketRefused = regexp.MustCompile(`(?i)permission denied.*docker.*sock|docker\.sock.*permission denied`)
|
|
|
|
// docker runs one docker command as this account and answers its stdout. A socket that refuses the
|
|
// account is asked again through `sudo -n`, as the container runtime's own tools do; never as root
|
|
// otherwise, and never with a prompt.
|
|
func docker(ctx context.Context, run Runner, uid int, args ...string) (string, error) {
|
|
r := run(ctx, "docker", args...)
|
|
program := "docker"
|
|
if r.Status != 0 && r.Err == "" && uid != 0 && socketRefused.MatchString(r.Stderr) {
|
|
program = "sudo"
|
|
r = run(ctx, "sudo", append([]string{"-n", "docker"}, args...)...)
|
|
}
|
|
if r.Status == 0 && r.Err == "" {
|
|
return r.Stdout, nil
|
|
}
|
|
said := strings.TrimSpace(r.Stderr + "\n" + r.Stdout)
|
|
switch {
|
|
case r.Err == "ENOENT" && program == "sudo":
|
|
return "", errors.New("the runtime's socket refused this account, and sudo is not installed here to escalate with")
|
|
case r.Err == "ENOENT":
|
|
return "", errors.New("docker is not installed on this machine, so the store's files cannot be listed")
|
|
case r.Err != "":
|
|
return "", fmt.Errorf("listing the store's files did not answer: %s", r.Err)
|
|
case program == "sudo" && strings.HasPrefix(said, "sudo:"):
|
|
return "", fmt.Errorf("the runtime's socket refused this account and it may not escalate without a prompt: %s", firstLine(said))
|
|
case strings.Contains(said, "No such container"):
|
|
return "", fmt.Errorf("the store's container is not on this machine: %s", firstLine(said))
|
|
case strings.Contains(said, "is not running"):
|
|
return "", fmt.Errorf("the store's container is not running: %s", firstLine(said))
|
|
}
|
|
if l := firstLine(said); l != "" {
|
|
return "", fmt.Errorf("listing the store's files failed (%d): %s", r.Status, l)
|
|
}
|
|
return "", fmt.Errorf("listing the store's files failed with status %d", r.Status)
|
|
}
|
|
|
|
func firstLine(s string) string {
|
|
for _, l := range strings.Split(s, "\n") {
|
|
if l = strings.TrimSpace(l); l != "" {
|
|
return l
|
|
}
|
|
}
|
|
return ""
|
|
}
|