Files
exec/docker/docker.go
T
2026-06-18 22:39:18 -03:00

274 lines
7.3 KiB
Go

// Run process insider a docker container
package docker
import (
"context"
"errors"
"fmt"
"io"
"net/netip"
"os"
"github.com/docker/docker/pkg/stdcopy"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"sirherobrine23.com.br/go-bds/exec/v2/process"
)
// Register default client docker
var _ = process.RegisterProcess(NewProc, "docker")
type DockerContainer struct {
DockerClient *client.Client
ContainerName string
Image string
Platform string // Docker platform to run image
Ports []network.Port
Volumes []string
containerID string
statusExit *container.WaitResponse
}
// Return new docker exec
func NewDocker(client *client.Client) *DockerContainer {
return &DockerContainer{
Image: "debian:latest",
DockerClient: client,
Volumes: []string{},
Ports: []network.Port{},
}
}
// Create Docker client connection and return new DockerContainer with "debian:latest" image
func NewProc() (*DockerContainer, error) {
client, err := client.New(client.FromEnv)
if err != nil {
return nil, err
}
return NewDocker(client), nil
}
// Append port on start conteiner
func (docker *DockerContainer) AddPort(networkType string, local, remote uint16) {
switch networkType {
case "udp", "udp4", "udp6":
docker.Ports = append(docker.Ports, network.MustParsePort(fmt.Sprintf("%d:%d/udp", remote, local)))
case "tcp", "tcp4", "tcp6":
docker.Ports = append(docker.Ports, network.MustParsePort(fmt.Sprintf("%d:%d/tcp", remote, local)))
default:
docker.Ports = append(docker.Ports, network.MustParsePort(fmt.Sprintf("%d:%d", remote, local)))
}
}
// Get container addresses
func (docker DockerContainer) ContainerAddr() ([]netip.Addr, error) {
if docker.containerID == "" {
return nil, process.ErrNoProcess
}
// Get container info
info, err := docker.DockerClient.ContainerInspect(context.Background(), docker.containerID, client.ContainerInspectOptions{})
if err != nil {
return nil, err
}
// IP Addresses
addr := []netip.Addr{}
if info.Container.NetworkSettings != nil {
if network := info.Container.NetworkSettings; len(info.Container.NetworkSettings.Networks) == 0 {
for _, network := range network.Networks {
if network.IPAddress.String() != "" {
addr = append(addr, network.IPAddress)
}
if network.GlobalIPv6Address.String() != "" {
addr = append(addr, network.GlobalIPv6Address)
}
}
}
}
return addr, nil
}
func (docker *DockerContainer) Kill() error {
if docker.containerID == "" {
return process.ErrNoProcess
}
_, err := docker.DockerClient.ContainerKill(context.Background(), docker.containerID, client.ContainerKillOptions{Signal: "SIGKILL"})
return err
}
func (docker *DockerContainer) Wait() error {
if docker.containerID == "" {
return process.ErrNoProcess
}
wait := docker.DockerClient.ContainerWait(context.Background(), docker.containerID, client.ContainerWaitOptions{Condition: container.WaitConditionNextExit})
select {
case err := <-wait.Error:
if err != nil {
return err
}
case status := <-wait.Result:
docker.statusExit = &status
if status.Error != nil {
return errors.New(status.Error.Message)
} else if status.StatusCode != 0 {
return fmt.Errorf("exit code %d", status.StatusCode)
}
}
return nil
}
func (docker *DockerContainer) ExitCode() (int, error) {
if docker.statusExit != nil {
return int(docker.statusExit.StatusCode), nil
} else if docker.containerID == "" {
return -1, process.ErrNoProcess
}
wait := docker.DockerClient.ContainerWait(context.Background(), docker.containerID, client.ContainerWaitOptions{Condition: container.WaitConditionNextExit})
select {
case err := <-wait.Error:
if err != nil {
return -1, err
}
case status := <-wait.Result:
docker.statusExit = &status
if status.Error != nil {
return -1, errors.New(status.Error.Message)
} else if status.StatusCode != 0 {
return -1, fmt.Errorf("exit code %d", status.StatusCode)
}
}
return int(docker.statusExit.StatusCode), nil
}
func (docker *DockerContainer) Close() error {
if docker.containerID == "" {
return process.ErrNoProcess
} else if _, err := docker.DockerClient.ContainerStop(context.Background(), docker.containerID, client.ContainerStopOptions{Signal: "SIGTERM"}); err != nil {
return err
}
return docker.Wait()
}
func (docker *DockerContainer) Signal(signal os.Signal) error {
if docker.containerID == "" {
return process.ErrNoProcess
} else if _, err := docker.DockerClient.ContainerStop(context.Background(), docker.containerID, client.ContainerStopOptions{Signal: signal.String()}); err != nil {
return err
}
return nil
}
func (w *DockerContainer) AttachStdin(r io.Reader) error {
if w.containerID != "" {
hijackedResponse, err := w.DockerClient.ContainerAttach(context.Background(), w.containerID, client.ContainerAttachOptions{
Stream: true,
Stdin: true,
})
if err != nil {
return err
}
go io.Copy(hijackedResponse.Conn, r)
}
return process.ErrNoProcess
}
func (w *DockerContainer) AttachStdout(w2 io.Writer) error {
if w.containerID != "" {
log, err := w.DockerClient.ContainerLogs(context.Background(), w.containerID, client.ContainerLogsOptions{Follow: true, ShowStdout: true, Tail: "5"})
if err != nil {
return err
}
go stdcopy.StdCopy(w2, nil, log)
}
return process.ErrNoProcess
}
func (w *DockerContainer) AttachStderr(w2 io.Writer) error {
if w.containerID != "" {
log, err := w.DockerClient.ContainerLogs(context.Background(), w.containerID, client.ContainerLogsOptions{Follow: true, ShowStderr: true, Tail: "5"})
if err != nil {
return err
}
go stdcopy.StdCopy(nil, w2, log)
}
return process.ErrNoProcess
}
func (docker *DockerContainer) Start(options *process.Exec) error {
if docker.DockerClient == nil {
var err error
if docker.DockerClient, err = client.NewClientWithOpts(client.FromEnv); err != nil {
return err
}
}
// Return error if container started
if docker.containerID != "" {
return process.ErrNoCallStart
}
ctx := context.Background()
dVolumes, dPorts := map[string]struct{}{}, network.PortSet{}
for _, vol := range docker.Volumes {
dVolumes[vol] = struct{}{}
}
for _, port := range docker.Ports {
p, err := network.ParsePort(fmt.Sprintf("%s/%s", port.Port(), port.Proto()))
if err != nil {
return err
}
dPorts[p] = struct{}{}
}
config := container.Config{
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
OpenStdin: true,
Env: options.Environment.ToSlice(),
Cmd: []string{},
Entrypoint: options.Arguments,
Image: docker.Image,
WorkingDir: options.Cwd,
Volumes: dVolumes,
ExposedPorts: dPorts,
}
host := container.HostConfig{
AutoRemove: true,
Tmpfs: map[string]string{
"/tmp": "rw,nosuid",
},
}
containerCreated, err := docker.DockerClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &config,
HostConfig: &host,
Name: docker.ContainerName,
})
if docker.containerID = containerCreated.ID; err != nil {
return err
} else if _, err = docker.DockerClient.ContainerStart(ctx, docker.containerID, client.ContainerStartOptions{}); err != nil {
return err
}
if options.Context != nil {
ctx := options.Context
go func() {
<-ctx.Done()
docker.Signal(os.Interrupt)
}()
}
return nil
}