476 lines
14 KiB
Go
476 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unicode"
|
|
|
|
rspec "github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"go.podman.io/storage/pkg/ioutils"
|
|
"go.podman.io/storage/pkg/reexec"
|
|
)
|
|
|
|
// use defined names for our various commands. we absolutely don't support
|
|
// everything that an actual functional runtime would, and have no intention of
|
|
// expanding to do so
|
|
type modeType string
|
|
|
|
const (
|
|
modeCreate = modeType("create")
|
|
modeStart = modeType("start")
|
|
modeState = modeType("state")
|
|
modeKill = modeType("kill")
|
|
modeDelete = modeType("delete")
|
|
subprocName = "dumpspec-subproc"
|
|
)
|
|
|
|
// signalsByName is a guess at which signals we'd be asked to send to a child
|
|
// process, currently restricted to the subset defined across all of our
|
|
// targets
|
|
var signalsByName = map[string]syscall.Signal{
|
|
"SIGABRT": syscall.SIGABRT,
|
|
"SIGALRM": syscall.SIGALRM,
|
|
"SIGBUS": syscall.SIGBUS,
|
|
"SIGFPE": syscall.SIGFPE,
|
|
"SIGHUP": syscall.SIGHUP,
|
|
"SIGILL": syscall.SIGILL,
|
|
"SIGINT": syscall.SIGINT,
|
|
"SIGKILL": syscall.SIGKILL,
|
|
"SIGPIPE": syscall.SIGPIPE,
|
|
"SIGQUIT": syscall.SIGQUIT,
|
|
"SIGSEGV": syscall.SIGSEGV,
|
|
"SIGTERM": syscall.SIGTERM,
|
|
"SIGTRAP": syscall.SIGTRAP,
|
|
}
|
|
|
|
var (
|
|
globalArgs struct {
|
|
debug bool
|
|
cgroupManager string
|
|
log string
|
|
logFormat string
|
|
logLevel string
|
|
root string
|
|
systemdCgroup bool
|
|
rootless bool
|
|
}
|
|
createArgs struct {
|
|
bundleDir string
|
|
configFile string
|
|
consoleSocket string
|
|
pidFile string
|
|
noPivot bool
|
|
noNewKeyring bool
|
|
preserveFds int
|
|
}
|
|
stateArgs struct {
|
|
all bool
|
|
pid int
|
|
regex string
|
|
}
|
|
killArgs struct {
|
|
all bool
|
|
pid int
|
|
regex string
|
|
signal int
|
|
}
|
|
deleteArgs struct {
|
|
force bool
|
|
regex string
|
|
}
|
|
)
|
|
|
|
func main() {
|
|
if reexec.Init() {
|
|
return
|
|
}
|
|
|
|
if len(os.Args) < 2 {
|
|
return
|
|
}
|
|
|
|
var container, containerID, containerDir string
|
|
|
|
mainCommand := cobra.Command{
|
|
Use: "dumpspec",
|
|
Short: "fake OCI runtime",
|
|
PersistentPreRunE: func(_ *cobra.Command, args []string) error {
|
|
tmpdir, ok := os.LookupEnv("XDG_RUNTIME_DIR")
|
|
if !ok {
|
|
tmpdir = filepath.Join(os.TempDir(), strconv.Itoa(os.Getuid()))
|
|
}
|
|
if globalArgs.root != "" {
|
|
tmpdir = globalArgs.root
|
|
}
|
|
tmpdir = filepath.Join(tmpdir, "dumpspec")
|
|
if err := os.MkdirAll(tmpdir, 0o700); err != nil && !errors.Is(err, os.ErrExist) {
|
|
return fmt.Errorf("ensuring that %q exists: %w", tmpdir, err)
|
|
}
|
|
if len(args) > 0 {
|
|
// this is the first arg for all of the commands that we care about
|
|
container = args[0]
|
|
}
|
|
containerID = mapToContainerID(container)
|
|
containerDir = filepath.Join(tmpdir, containerID)
|
|
return nil
|
|
},
|
|
}
|
|
mainFlags := mainCommand.PersistentFlags()
|
|
mainFlags.BoolVar(&globalArgs.debug, "debug", false, "log for debugging")
|
|
mainFlags.BoolVar(&globalArgs.systemdCgroup, "systemd-cgroup", false, "use systemd for handling cgroups")
|
|
mainFlags.BoolVar(&globalArgs.rootless, "rootless", false, "ignore some settings to that conflict with rootless operation")
|
|
mainFlags.StringVar(&globalArgs.cgroupManager, "cgroup-manager", "cgroupfs", "method for managing cgroups")
|
|
mainFlags.StringVar(&globalArgs.log, "log", "", "logging destination")
|
|
mainFlags.StringVar(&globalArgs.logFormat, "log-format", "", "logging format specifier")
|
|
mainFlags.StringVar(&globalArgs.logLevel, "log-level", "", "logging level")
|
|
rootUsage := "root `directory` of runtime data"
|
|
rootDefault := ""
|
|
if xdgRuntimeDir, ok := os.LookupEnv("XDG_RUNTIME_DIR"); ok {
|
|
rootUsage += " (default $XDG_RUNTIME_DIR)"
|
|
rootDefault = xdgRuntimeDir
|
|
}
|
|
mainFlags.StringVar(&globalArgs.root, "root", rootDefault, rootUsage)
|
|
|
|
createCommand := &cobra.Command{
|
|
Use: string(modeCreate),
|
|
Args: cobra.ExactArgs(1),
|
|
Short: "create a ready-to-start container process",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
if err := os.MkdirAll(containerDir, 0o700); err != nil {
|
|
return fmt.Errorf("creating container directory: %w", err)
|
|
}
|
|
configFile := createArgs.configFile
|
|
if configFile == "" {
|
|
configFile = filepath.Join(createArgs.bundleDir, "config.json")
|
|
}
|
|
config, err := os.ReadFile(configFile)
|
|
if err != nil {
|
|
return fmt.Errorf("reading runtime configuration: %w", err)
|
|
}
|
|
var spec rspec.Spec
|
|
if err := json.Unmarshal(config, &spec); err != nil {
|
|
return fmt.Errorf("parsing runtime configuration: %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(containerDir, "config.json"), config, 0o600); err != nil {
|
|
return fmt.Errorf("saving copy of runtime configuration: %w", err)
|
|
}
|
|
state := rspec.State{
|
|
Version: rspec.Version,
|
|
ID: container,
|
|
Bundle: createArgs.bundleDir,
|
|
}
|
|
stateBytes, err := json.Marshal(state)
|
|
if err != nil {
|
|
return fmt.Errorf("encoding initial runtime state: %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(containerDir, "state"), stateBytes, 0o600); err != nil {
|
|
return fmt.Errorf("writing initial runtime state: %w", err)
|
|
}
|
|
pr, pw, err := os.Pipe()
|
|
if err != nil {
|
|
return fmt.Errorf("internal error: %w", err)
|
|
}
|
|
defer pr.Close()
|
|
cmd := getStarter(containerDir, createArgs.consoleSocket, createArgs.pidFile, spec, pw)
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("internal error: %w", err)
|
|
}
|
|
pw.Close()
|
|
ready, err := io.ReadAll(pr)
|
|
if err != nil {
|
|
return fmt.Errorf("waiting for child to start: %w", err)
|
|
}
|
|
if strings.TrimSpace(string(ready)) != "OK" {
|
|
return fmt.Errorf("unexpected child status %q", string(ready))
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
createFlags := createCommand.Flags()
|
|
createFlags.StringVarP(&createArgs.bundleDir, "bundle", "b", "", "`directory` containing config.json")
|
|
createFlags.StringVarP(&createArgs.configFile, "config", "f", "", "`path` to config.json")
|
|
createFlags.StringVar(&createArgs.consoleSocket, "console-socket", "", "socket `path` for passing PTY")
|
|
createFlags.StringVar(&createArgs.pidFile, "pid-file", "", "`path` in which to store child PID")
|
|
createFlags.BoolVar(&createArgs.noPivot, "no-pivot", false, "use chroot() instead of pivot_root()")
|
|
createFlags.BoolVar(&createArgs.noNewKeyring, "no-new-keyring", false, "don't create a new keyring")
|
|
mainCommand.AddCommand(createCommand)
|
|
|
|
startCommand := &cobra.Command{
|
|
Use: string(modeStart),
|
|
Args: cobra.ExactArgs(1),
|
|
Short: "start a previously-created container process",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
if err := ioutils.AtomicWriteFile(filepath.Join(containerDir, "start"), []byte("start"), 0o600); err != nil {
|
|
return fmt.Errorf("writing start file: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
mainCommand.AddCommand(startCommand)
|
|
|
|
stateCommand := &cobra.Command{
|
|
Use: string(modeState),
|
|
Args: cobra.ExactArgs(1),
|
|
Short: "poll the state of a container process",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
stateFile, err := os.Open(filepath.Join(containerDir, "state"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stateFile.Close()
|
|
if _, err := io.Copy(os.Stdout, stateFile); err != nil {
|
|
return fmt.Errorf("copying state file: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
stateFlags := stateCommand.Flags()
|
|
stateFlags.BoolVarP(&stateArgs.all, "all", "a", false, "start all containers")
|
|
stateFlags.IntVar(&stateArgs.pid, "pid", 0, "start container by `pid`")
|
|
stateFlags.StringVarP(&stateArgs.regex, "regex", "r", "", "start containers with IDs matching a `regex`")
|
|
mainCommand.AddCommand(stateCommand)
|
|
|
|
killCommand := &cobra.Command{
|
|
Use: string(modeKill),
|
|
Args: cobra.RangeArgs(1, 2),
|
|
Short: "signal/kill a container process",
|
|
RunE: func(_ *cobra.Command, args []string) error {
|
|
if len(args) > 1 {
|
|
signalString := args[1]
|
|
signalNumber, err := strconv.Atoi(signalString)
|
|
if err != nil {
|
|
n, ok := signalsByName[signalString]
|
|
if !ok {
|
|
n, ok = signalsByName["SIG"+signalString]
|
|
if !ok {
|
|
return fmt.Errorf("%v: unrecognized signal %q", os.Args, signalString)
|
|
}
|
|
}
|
|
signalNumber = int(n)
|
|
}
|
|
killArgs.signal = signalNumber
|
|
}
|
|
if err := ioutils.AtomicWriteFile(filepath.Join(containerDir, "kill"), []byte(strconv.Itoa(killArgs.signal)), 0o600); err != nil {
|
|
return fmt.Errorf("writing exit status file: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
killFlags := killCommand.Flags()
|
|
killFlags.BoolVarP(&killArgs.all, "all", "a", false, "signal/kill all containers")
|
|
killFlags.IntVar(&killArgs.pid, "pid", 0, "signal/kill container by `pid`")
|
|
killFlags.StringVarP(&killArgs.regex, "regex", "r", "", "signal/kill containers with IDs matching a `regex`")
|
|
mainCommand.AddCommand(killCommand)
|
|
|
|
deleteCommand := &cobra.Command{
|
|
Use: string(modeDelete),
|
|
Args: cobra.ExactArgs(1),
|
|
Short: "delete a container process",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
if err := os.RemoveAll(containerDir); err != nil {
|
|
return fmt.Errorf("removing container directory: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
deleteFlags := deleteCommand.Flags()
|
|
deleteFlags.StringVarP(&deleteArgs.regex, "regex", "r", "", "delete containers with IDs matching a `regex`")
|
|
deleteFlags.BoolVarP(&deleteArgs.force, "force", "f", false, "forcibly stop containers which are not stopped")
|
|
mainCommand.AddCommand(deleteCommand)
|
|
|
|
err := mainCommand.Execute()
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
func mapToContainerID(container string) string {
|
|
var encoder strings.Builder
|
|
for _, c := range container {
|
|
if unicode.IsLetter(c) || unicode.IsNumber(c) {
|
|
if _, err := encoder.WriteRune(c); err != nil {
|
|
logrus.Fatalf("%v: encoding container ID: %q: %v", os.Args, c, err)
|
|
}
|
|
} else {
|
|
if _, err := encoder.WriteString(strconv.Itoa(int(c))); err != nil {
|
|
logrus.Fatalf("%v: encoding container ID: %q: %v", os.Args, c, err)
|
|
}
|
|
}
|
|
}
|
|
return encoder.String()
|
|
}
|
|
|
|
func waitForFile(dirname, basename string) string {
|
|
waitedFile := filepath.Join(dirname, basename)
|
|
for {
|
|
if _, err := os.Stat(dirname); err != nil {
|
|
logrus.Fatalf("%v: %v", os.Args, err)
|
|
}
|
|
st, err := os.Stat(waitedFile)
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
logrus.Fatalf("%v: %v", os.Args, err)
|
|
}
|
|
if err != nil || st.Size() == 0 {
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
contents, err := os.ReadFile(waitedFile)
|
|
if err != nil {
|
|
logrus.Fatalf("%v: %v", os.Args, err)
|
|
}
|
|
text := strings.TrimSpace(string(contents))
|
|
return text
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
reexec.Register(subprocName, subproc)
|
|
}
|
|
|
|
func subproc() {
|
|
mainCommand := cobra.Command{
|
|
Use: "dumpspec",
|
|
Short: "fake OCI runtime",
|
|
Long: "dumpspec containerDir consoleSocket pidFile [spec ...]",
|
|
Args: cobra.ExactArgs(3),
|
|
RunE: func(_ *cobra.Command, args []string) error {
|
|
dir := args[0]
|
|
consoleSocket := args[1]
|
|
pidFile := args[2]
|
|
|
|
config, err := os.ReadFile(filepath.Join(dir, "config.json"))
|
|
if err != nil {
|
|
return fmt.Errorf("reading runtime configuration: %w", err)
|
|
}
|
|
var spec rspec.Spec
|
|
if err := json.Unmarshal(config, &spec); err != nil {
|
|
return fmt.Errorf("parsing runtime configuration: %w", err)
|
|
}
|
|
|
|
stateBytes, err := os.ReadFile(filepath.Join(dir, "state"))
|
|
if err != nil {
|
|
return fmt.Errorf("reading initial state : %w", err)
|
|
}
|
|
var state rspec.State
|
|
if err := json.Unmarshal(stateBytes, &state); err != nil {
|
|
return fmt.Errorf("parsing initial state: %w", err)
|
|
}
|
|
|
|
saveState := func() error {
|
|
stateBytes, err := json.Marshal(state)
|
|
if err != nil {
|
|
return fmt.Errorf("encoding updated state: %w", err)
|
|
}
|
|
err = ioutils.AtomicWriteFile(filepath.Join(dir, "state"), stateBytes, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("writing updated state: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
output := io.Writer(os.Stdout)
|
|
|
|
if pidFile != "" {
|
|
if err := ioutils.AtomicWriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0o600); err != nil {
|
|
return fmt.Errorf("writing pid file %q: %w", pidFile, err)
|
|
}
|
|
}
|
|
|
|
state.Pid = os.Getpid()
|
|
state.Status = rspec.StateCreated
|
|
if err := saveState(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if consoleSocket != "" {
|
|
if output, err = sendConsoleDescriptor(consoleSocket); err != nil {
|
|
return fmt.Errorf("sending terminal control fd to parent process: %w", err)
|
|
}
|
|
}
|
|
|
|
ok := os.NewFile(3, "startup status pipe")
|
|
fmt.Fprintf(ok, "OK")
|
|
ok.Close()
|
|
|
|
start := waitForFile(dir, "start")
|
|
if start != "start" {
|
|
return fmt.Errorf("unexpected start indicator %q", start)
|
|
}
|
|
|
|
state.Status = rspec.StateRunning
|
|
if err := saveState(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if spec.Process == nil || len(spec.Process.Args) == 0 {
|
|
if _, err := io.Copy(output, bytes.NewReader(config)); err != nil {
|
|
return fmt.Errorf("writing configuration: %w", err)
|
|
}
|
|
} else {
|
|
for _, query := range spec.Process.Args {
|
|
var data any
|
|
if err := json.Unmarshal(config, &data); err != nil {
|
|
return fmt.Errorf("parsing runtime configuration: %w", err)
|
|
}
|
|
path := strings.Split(query, ".")
|
|
for i, component := range path {
|
|
if component == "" {
|
|
continue
|
|
}
|
|
pathSoFar := strings.Join(path[:i], ".")
|
|
if data == nil {
|
|
return fmt.Errorf("unable to descend into %q after %q", component, pathSoFar)
|
|
}
|
|
if m, ok := data.(map[string]any); ok {
|
|
data = m[component]
|
|
} else if s, ok := data.([]any); ok {
|
|
i, err := strconv.Atoi(component)
|
|
if err != nil {
|
|
return fmt.Errorf("%q is not numeric while indexing slice at %q", component, pathSoFar)
|
|
}
|
|
data = s[i]
|
|
} else {
|
|
return fmt.Errorf("unable to descend into %q after %q", component, pathSoFar)
|
|
}
|
|
}
|
|
final, err := json.Marshal(data)
|
|
if err != nil {
|
|
return fmt.Errorf("encoding query result: %w", err)
|
|
}
|
|
if len(final) == 0 || final[len(final)-1] != '\n' {
|
|
final = append(final, byte('\n'))
|
|
}
|
|
if _, err := io.Copy(output, bytes.NewReader(final)); err != nil {
|
|
return fmt.Errorf("writing configuration subset %q: %w", query, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
state.Status = rspec.StateStopped
|
|
if err := saveState(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
err := mainCommand.Execute()
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|