240 lines
7.7 KiB
Go
240 lines
7.7 KiB
Go
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
|
|
}
|