// Copyright 2024 Matthew Rich . 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 }