repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/podman/podman.go
Rawpackage podman
import (
"context"
"io"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"time"
)
type Mount struct {
Source string
Target string
ReadOnly bool
}
type Spec struct {
Image string
Workspace string
Memory string
CPUs string
Env map[string]string
EnvFile string
Mounts []Mount
Commands []string
Timeout time.Duration
}
func BuildArgs(spec Spec) []string {
args := []string{
"run",
"--rm",
"--pull=missing",
"--userns=keep-id",
"--cap-drop=all",
"--security-opt=no-new-privileges",
"--pids-limit=1024",
"--memory=" + spec.Memory,
"--cpus=" + spec.CPUs,
"--timeout=" + strconv.FormatInt(int64((spec.Timeout+time.Second-1)/time.Second), 10),
"--workdir=/work",
"--volume", spec.Workspace + ":/work:Z",
}
for _, mount := range spec.Mounts {
mode := ":Z"
if mount.ReadOnly {
mode = ":ro,Z"
}
args = append(args, "--volume", mount.Source+":"+mount.Target+mode)
}
if spec.EnvFile != "" {
args = append(args, "--env-file", spec.EnvFile)
}
keys := make([]string, 0, len(spec.Env))
for key := range spec.Env {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
args = append(args, "--env", key)
}
args = append(args, "--", spec.Image, "/bin/sh", "-c", StepScript(spec.Commands))
return args
}
// Environment returns only frozen child values named by BuildArgs.
// Host environment never becomes container environment.
func Environment(spec Spec) []string {
return environment(spec.Env)
}
// StepScript wraps each command in a step marker so logs show per-command
// wall time. Commands already run through /bin/sh -c; eval keeps that trust
// boundary unchanged.
func StepScript(commands []string) string {
var b strings.Builder
b.WriteString("STEP=0\nTOTAL=")
b.WriteString(strconv.Itoa(len(commands)))
b.WriteString("\nstep() {\n STEP=$((STEP+1))\n printf '[luci] step %d/%d: %s\\n' \"$STEP\" \"$TOTAL\" \"$1\"\n START=$(date +%s)\n eval \"$1\"\n RC=$?\n END=$(date +%s)\n if [ \"$RC\" -ne 0 ]; then\n printf '[luci] step %d/%d failed after %ds\\n' \"$STEP\" \"$TOTAL\" \"$((END-START))\"\n exit \"$RC\"\n fi\n printf '[luci] step %d/%d done in %ds\\n' \"$STEP\" \"$TOTAL\" \"$((END-START))\"\n}\n")
for _, command := range commands {
b.WriteString("step '")
b.WriteString(strings.ReplaceAll(command, "'", `'\''`))
b.WriteString("'\n")
}
return b.String()
}
type Executor interface {
Run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) error
}
// EnvironmentExecutor runs Podman with a caller-supplied child environment.
// Callers passing BuildArgs(spec) must pass Environment(spec) here.
type EnvironmentExecutor interface {
Executor
RunWithEnv(ctx context.Context, args, env []string, stdout io.Writer, stderr io.Writer) error
}
type CLI struct {
Socket string
}
func (c CLI) Run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) error {
return c.RunWithEnv(ctx, args, os.Environ(), stdout, stderr)
}
func (c CLI) RunWithEnv(ctx context.Context, args, env []string, stdout io.Writer, stderr io.Writer) error {
values := make(map[string]string, len(env)+1)
for _, entry := range env {
key, value, _ := strings.Cut(entry, "=")
values[key] = value
}
if c.Socket != "" {
values["CONTAINER_HOST"] = c.Socket
}
cmd := exec.CommandContext(ctx, "podman", args...)
cmd.Env = environment(values)
cmd.Stdout = stdout
cmd.Stderr = stderr
return cmd.Run()
}
func environment(values map[string]string) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
env := make([]string, 0, len(keys))
for _, key := range keys {
env = append(env, key+"="+values[key])
}
return env
}