package main import ( "context" "errors" "fmt" "io" "net" "os" "os/exec" "path/filepath" "strings" "syscall" "time" "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/pkg/api/handlers" "github.com/containers/podman/v5/pkg/bindings" "github.com/containers/podman/v5/pkg/bindings/containers" "github.com/containers/podman/v5/pkg/bindings/images" "github.com/containers/podman/v5/pkg/bindings/network" "github.com/containers/podman/v5/pkg/bindings/pods" "github.com/containers/podman/v5/pkg/bindings/volumes" entitiesTypes "github.com/containers/podman/v5/pkg/domain/entities/types" "github.com/containers/podman/v5/pkg/specgen" dockerContainer "github.com/docker/docker/api/types/container" "go.podman.io/common/libnetwork/types" ) // engine is the slice of the podman API a session uses. type engine interface { NetworkCreate(ctx context.Context, n *types.Network, ignoreIfExists bool) (types.Network, error) NetworkRemove(ctx context.Context, name string) error VolumeCreate(ctx context.Context, name string) error PodCreate(ctx context.Context, s *specgen.PodSpecGenerator) error PodRemove(ctx context.Context, name string) error ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) error ContainerStart(ctx context.Context, name string) error // ContainerRun attaches stdio, starts the container and returns its exit code. ContainerRun(ctx context.Context, name string) (int, error) ContainerExec(ctx context.Context, name string, cmd []string) (int, error) ContainerLogs(ctx context.Context, name string, tail int) string ContainerRemove(ctx context.Context, name string) error } // startService runs a private podman API service on a socket in dir and // returns the connection context, an engine bound to it and a stop func. var startService = func(dir string) (context.Context, engine, func(), error) { sock := filepath.Join(dir, "podman.sock") logPath := filepath.Join(dir, "service.log") logf, err := os.Create(logPath) if err != nil { return nil, nil, nil, err } defer func() { _ = logf.Close() }() // The service stops when we close lifeline, or the kernel does on SIGKILL. // Pdeathsig can't do this: podman's userns join resets it. lifeline, lifelineW, err := os.Pipe() if err != nil { return nil, nil, nil, err } cmd := exec.Command("sh", "-c", `podman system service --time=60 "$1" 3<&- & p=$!; { read -r _ <&3; kill $p; } & wait $p`, "sh", "unix://"+sock) cmd.Stdout, cmd.Stderr = logf, logf cmd.ExtraFiles = []*os.File{lifeline} // Own process group: terminal signals must not kill it before cleanup. cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} err = cmd.Start() _ = lifeline.Close() if err != nil { _ = lifelineW.Close() return nil, nil, nil, err } exited := make(chan struct{}) go func() { _ = cmd.Wait(); close(exited) }() stop := func() { _ = lifelineW.Close() select { case <-exited: case <-time.After(5 * time.Second): _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) <-exited } } fail := func(err error) (context.Context, engine, func(), error) { stop() b, _ := os.ReadFile(logPath) return nil, nil, nil, fmt.Errorf("podman API service: %w: %s", err, strings.TrimSpace(string(b))) } for deadline := time.Now().Add(30 * time.Second); ; { if c, err := net.Dial("unix", sock); err == nil { _ = c.Close() break } select { case <-exited: return fail(errors.New("exited")) case <-time.After(50 * time.Millisecond): } if time.Now().After(deadline) { return fail(errors.New("timed out")) } } ctx, err := bindings.NewConnection(context.Background(), "unix://"+sock) if err != nil { return fail(err) } return ctx, bindingsEngine{}, stop, nil } type bindingsEngine struct{} func (bindingsEngine) NetworkCreate(ctx context.Context, n *types.Network, ignore bool) (types.Network, error) { return network.CreateWithOptions(ctx, n, new(network.ExtraCreateOptions).WithIgnoreIfExists(ignore)) } func (bindingsEngine) NetworkRemove(ctx context.Context, name string) error { _, err := network.Remove(ctx, name, new(network.RemoveOptions).WithForce(true)) return err } func (bindingsEngine) VolumeCreate(ctx context.Context, name string) error { _, err := volumes.Create(ctx, entitiesTypes.VolumeCreateOptions{Name: name, IgnoreIfExists: true}, nil) return err } func (bindingsEngine) PodCreate(ctx context.Context, s *specgen.PodSpecGenerator) error { _, err := pods.CreatePodFromSpec(ctx, &entitiesTypes.PodSpec{PodSpecGen: *s}) return err } func (bindingsEngine) PodRemove(ctx context.Context, name string) error { _, err := pods.Remove(ctx, name, new(pods.RemoveOptions).WithForce(true).WithTimeout(0)) return err } // ContainerCreate pulls a missing image first, as `podman run` does. func (bindingsEngine) ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) error { if ok, err := images.Exists(ctx, s.Image, nil); err != nil { return err } else if !ok { var w io.Writer = os.Stderr if _, err := images.Pull(ctx, s.Image, new(images.PullOptions).WithPolicy("missing").WithProgressWriter(w)); err != nil { return err } } r, err := containers.CreateWithSpec(ctx, s, nil) if err != nil { return err } for _, w := range r.Warnings { fmt.Fprintln(os.Stderr, w) } return nil } func (bindingsEngine) ContainerStart(ctx context.Context, name string) error { return containers.Start(ctx, name, nil) } // ContainerRun mirrors podman-remote's startAndAttach: attach, start, then // wait right away so a --rm container can't be removed before we see its code. func (bindingsEngine) ContainerRun(ctx context.Context, name string) (int, error) { attached := make(chan error, 1) ready := make(chan bool) go func() { attached <- containers.Attach(ctx, name, os.Stdin, os.Stdout, os.Stderr, ready, new(containers.AttachOptions).WithStream(true)) }() select { case <-ready: case err := <-attached: return define.ExitCode(err), err } if err := containers.Start(ctx, name, nil); err != nil { return define.ExitCode(err), err } type result struct { code int32 err error } waited := make(chan result, 1) go func() { code, err := containers.Wait(ctx, name, nil) waited <- result{code, err} }() select { case err := <-attached: if errors.Is(err, define.ErrDetach) { return 0, nil } if err != nil { return 125, err } r := <-waited return int(r.code), r.err case r := <-waited: if r.err != nil { return 125, r.err } return int(r.code), <-attached } } func (bindingsEngine) ContainerExec(ctx context.Context, name string, cmd []string) (int, error) { id, err := containers.ExecCreate(ctx, name, &handlers.ExecCreateConfig{ExecOptions: dockerContainer.ExecOptions{Cmd: cmd, AttachStderr: true}}) if err != nil { return -1, err } defer func() { _ = containers.ExecRemove(ctx, id, new(containers.ExecRemoveOptions).WithForce(true)) }() if err := containers.ExecStartAndAttach(ctx, id, new(containers.ExecStartAndAttachOptions).WithAttachError(true).WithErrorStream(io.Discard)); err != nil { return -1, err } s, err := containers.ExecInspect(ctx, id, nil) if err != nil { return -1, err } return s.ExitCode, nil } func (bindingsEngine) ContainerLogs(ctx context.Context, name string, tail int) string { lines := make(chan string) done := make(chan struct{}) var out []string go func() { for l := range lines { out = append(out, l) } close(done) }() opts := new(containers.LogOptions).WithStdout(true).WithStderr(true).WithTail(fmt.Sprint(tail)) _ = containers.Logs(ctx, name, opts, lines, lines) close(lines) <-done return strings.Join(out, "\n") } func (bindingsEngine) ContainerRemove(ctx context.Context, name string) error { _, err := containers.Remove(ctx, name, new(containers.RemoveOptions).WithForce(true).WithTimeout(0)) return err }