mesh/merge-gate pass: builds distribution, new: modules/artifact-store-tools → novox; no bus step; every machine composes with the change as it did without…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery failed: its walk failed: a gate on a first machine (what it carried put back), a build, a machine
A module that provides the artifact store cannot build an artifact: building publishes to the store, so the store would be needed to create itself, and the module check refuses it. The tools become artifact-store-tools, assigned beside the store, and read manifests from the store's files through its container as they already read the listing, so they need no address for the store's door. Named for the artifact store, because "store" alone is the database server (hq glossary).
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 ""
|
|
}
|