jx/internal/command/containercommand.go

143 lines
4.0 KiB
Go
Raw Normal View History

// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
"context"
"fmt"
"io"
"os"
"decl/internal/containerlog"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"time"
"log/slog"
)
type ContainerExecClient interface {
ContainerExecAttach(ctx context.Context, execID string, config container.ExecAttachOptions) (types.HijackedResponse, error)
ContainerExecCreate(ctx context.Context, containerID string, options container.ExecOptions) (types.IDResponse, error)
ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error)
ContainerExecStart(ctx context.Context, execID string, config container.ExecStartOptions) (error)
ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
Close() error
}
type ContainerCommandProvider struct {
containerID string
execID string
ExitCode int
container.ExecOptions
response types.HijackedResponse
pipes *containerlog.StreamReader
Stdin io.Reader
apiClient ContainerExecClient `json:"-" yaml:"-"`
}
// ref could be a resource, but it just needs to implement the ExecResource interface
func NewContainerProvider(cmd *Command, value any) (p *ContainerCommandProvider) {
if args, err := cmd.Template(value); err == nil {
p = &ContainerCommandProvider {
ExecOptions: container.ExecOptions {
Cmd: strslice.StrSlice(append([]string{cmd.Path}, args...)),
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
},
}
p.containerID = p.ResolveId(context.Background(), cmd.TargetRef)
slog.Info("command.NewContainerProvider", "command", cmd.Path, "args", args, "target", cmd.TargetRef, "container", p.containerID)
}
return
}
func (c *ContainerCommandProvider) ResolveId(ctx context.Context, ref CommandTargetRef) (containerID string) {
name := ref.Name()
filterArgs := filters.NewArgs()
filterArgs.Add("name", "/"+name)
containers, listErr := c.apiClient.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filterArgs,
})
if listErr != nil {
panic(listErr)
}
for _, container := range containers {
for _, containerName := range container.Names {
if containerName == "/"+name {
containerID = container.ID
}
}
}
return
}
func (c *ContainerCommandProvider) Start() (err error) {
var execIDResponse types.IDResponse
ctx := context.Background()
if execIDResponse, err = c.apiClient.ContainerExecCreate(ctx, c.containerID, c.ExecOptions); err == nil {
c.execID = execIDResponse.ID
if c.execID == "" {
return fmt.Errorf("Failed creating a container exec ID")
}
execStartCheck := types.ExecStartCheck{
Tty: false,
}
if c.response, err = c.apiClient.ContainerExecAttach(ctx, c.execID, execStartCheck); err == nil {
c.pipes = containerlog.NewStreamReader(c.response.Conn)
}
}
return
}
func (c *ContainerCommandProvider) Wait() (err error) {
var containerDetails container.ExecInspect
ctx := context.Background()
for {
// copy Stdin to the connection
if c.Stdin != nil {
io.Copy(c.response.Conn, c.Stdin)
}
if containerDetails, err = c.apiClient.ContainerExecInspect(ctx, c.execID); err != nil || ! containerDetails.Running {
c.ExitCode = containerDetails.ExitCode
break
} else {
time.Sleep(500 * time.Millisecond)
}
}
return
}
func (c *ContainerCommandProvider) SetCmdEnv(env []string) {
c.Env = append(os.Environ(), env...)
}
func (c *ContainerCommandProvider) SetStdin(r io.Reader) {
c.Stdin = r
}
func (c *ContainerCommandProvider) StdinPipe() (io.WriteCloser, error) {
return c.response.Conn, nil
}
func (c *ContainerCommandProvider) StdoutPipe() (io.ReadCloser, error) {
return c.pipes.StdoutPipe(), nil
}
func (c *ContainerCommandProvider) StderrPipe() (io.ReadCloser, error) {
return c.pipes.StderrPipe(), nil
}
func (c *ContainerCommandProvider) Close() (err error) {
err = c.response.CloseWrite()
c.response.Close()
return
}