Files
unkin-agent 818ec3f59c
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Refuse launch dirs containing the API socket and stop the service with the launcher
2026-10-24 17:29:08 +11:00

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
}