add support for remote command execution
This commit is contained in:
parent
1c6b113e15
commit
94f3998fcd
@ -3,15 +3,12 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "context"
|
_ "context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"errors"
|
"errors"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
_ "net/url"
|
_ "net/url"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
@ -19,31 +16,43 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// A resource that implements the ExecProvider interface can be used as an exec target.
|
||||||
|
type CommandProvider interface {
|
||||||
|
Start() error
|
||||||
|
Wait() error
|
||||||
|
SetCmdEnv([]string)
|
||||||
|
SetStdin(io.Reader)
|
||||||
|
StdinPipe() (io.WriteCloser, error)
|
||||||
|
StdoutPipe() (io.ReadCloser, error)
|
||||||
|
StderrPipe() (io.ReadCloser, error)
|
||||||
|
}
|
||||||
|
|
||||||
var ErrUnknownCommand error = errors.New("Unable to find command in path")
|
var ErrUnknownCommand error = errors.New("Unable to find command in path")
|
||||||
|
|
||||||
type CommandExecutor func(value any) ([]byte, error)
|
type CommandExecutor func(value any) ([]byte, error)
|
||||||
type CommandExtractAttributes func(output []byte, target any) error
|
type CommandExtractAttributes func(output []byte, target any) error
|
||||||
type CommandExists func() error
|
type CommandExists func() error
|
||||||
|
|
||||||
type CommandArg string
|
|
||||||
|
|
||||||
type CommandInput string
|
type CommandInput string
|
||||||
|
|
||||||
type Command struct {
|
type Command struct {
|
||||||
Path string `json:"path" yaml:"path"`
|
Path string `json:"path" yaml:"path"`
|
||||||
Args []CommandArg `json:"args" yaml:"args"`
|
Args []CommandArg `json:"args" yaml:"args"`
|
||||||
Env []string `json:"env" yaml:"env"`
|
Env []string `json:"env" yaml:"env"`
|
||||||
Split bool `json:"split" yaml:"split"`
|
Split bool `json:"split" yaml:"split"`
|
||||||
FailOnError bool `json:"failonerror" yaml:"failonerror"`
|
FailOnError bool `json:"failonerror" yaml:"failonerror"`
|
||||||
StdinAvailable bool `json:"stdinavailable,omitempty" yaml:"stdinavailable,omitempty"`
|
StdinAvailable bool `json:"stdinavailable,omitempty" yaml:"stdinavailable,omitempty"`
|
||||||
ExitCode int `json:"exitcode,omitempty" yaml:"exitcode,omitempty"`
|
ExitCode int `json:"exitcode,omitempty" yaml:"exitcode,omitempty"`
|
||||||
Stdout string `json:"stdout,omitempty" yaml:"stdout,omitempty"`
|
Stdout string `json:"stdout,omitempty" yaml:"stdout,omitempty"`
|
||||||
Stderr string `json:"stderr,omitempty" yaml:"stderr,omitempty"`
|
Stderr string `json:"stderr,omitempty" yaml:"stderr,omitempty"`
|
||||||
Executor CommandExecutor `json:"-" yaml:"-"`
|
Executor CommandExecutor `json:"-" yaml:"-"`
|
||||||
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
|
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
|
||||||
CommandExists CommandExists `json:"-" yaml:"-"`
|
CommandExists CommandExists `json:"-" yaml:"-"`
|
||||||
Input CommandInput `json:"-" yaml:"-"`
|
Input CommandInput `json:"-" yaml:"-"`
|
||||||
stdin io.Reader `json:"-" yaml:"-"`
|
stdin io.Reader `json:"-" yaml:"-"`
|
||||||
|
|
||||||
|
TargetRef CommandTargetRef `json:"targetref,omitempty" yaml:"targetref,omitempty"`
|
||||||
|
execHandle CommandProvider `json:"-" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommand() *Command {
|
func NewCommand() *Command {
|
||||||
@ -70,21 +79,20 @@ func (c *Command) Defaults() {
|
|||||||
}
|
}
|
||||||
c.Executor = func(value any) ([]byte, error) {
|
c.Executor = func(value any) ([]byte, error) {
|
||||||
c.ClearOutput()
|
c.ClearOutput()
|
||||||
args, err := c.Template(value)
|
|
||||||
if err != nil {
|
c.execHandle = c.TargetRef.Provider(c, value)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if inputErr := c.SetInput(value); inputErr != nil {
|
if inputErr := c.SetInput(value); inputErr != nil {
|
||||||
return nil, inputErr
|
return nil, inputErr
|
||||||
}
|
}
|
||||||
cmd := exec.Command(c.Path, args...)
|
|
||||||
c.SetCmdEnv(cmd)
|
c.SetCmdEnv()
|
||||||
|
|
||||||
|
cmd := c.execHandle
|
||||||
if c.stdin != nil {
|
if c.stdin != nil {
|
||||||
cmd.Stdin = c.stdin
|
cmd.SetStdin(c.stdin)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("execute() - cmd", "path", c.Path, "args", args)
|
|
||||||
output, stdoutPipeErr := cmd.StdoutPipe()
|
output, stdoutPipeErr := cmd.StdoutPipe()
|
||||||
if stdoutPipeErr != nil {
|
if stdoutPipeErr != nil {
|
||||||
return nil, stdoutPipeErr
|
return nil, stdoutPipeErr
|
||||||
@ -113,11 +121,13 @@ func (c *Command) Defaults() {
|
|||||||
c.Stderr = string(stdErrOutput)
|
c.Stderr = string(stdErrOutput)
|
||||||
c.ExitCode = c.GetExitCodeFromError(waitErr)
|
c.ExitCode = c.GetExitCodeFromError(waitErr)
|
||||||
|
|
||||||
|
/*
|
||||||
if len(stdOutOutput) > 100 {
|
if len(stdOutOutput) > 100 {
|
||||||
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput[:100]), "error", string(stdErrOutput))
|
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput[:100]), "error", string(stdErrOutput))
|
||||||
} else {
|
} else {
|
||||||
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput), "error", string(stdErrOutput))
|
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput), "error", string(stdErrOutput))
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if len(stdErrOutput) > 0 && c.FailOnError {
|
if len(stdErrOutput) > 0 && c.FailOnError {
|
||||||
return stdOutOutput, fmt.Errorf("%w %s", waitErr, string(stdErrOutput))
|
return stdOutOutput, fmt.Errorf("%w %s", waitErr, string(stdErrOutput))
|
||||||
@ -134,8 +144,8 @@ func (c *Command) LoadDecl(yamlResourceDeclaration string) error {
|
|||||||
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c)
|
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) SetCmdEnv(cmd *exec.Cmd) {
|
func (c *Command) SetCmdEnv() {
|
||||||
cmd.Env = append(os.Environ(), c.Env...)
|
c.execHandle.SetCmdEnv(c.Env)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) SetStdinReader(r io.Reader) {
|
func (c *Command) SetStdinReader(r io.Reader) {
|
||||||
@ -188,8 +198,10 @@ func (c *Command) Execute(value any) ([]byte, error) {
|
|||||||
func (c *Command) SetInput(value any) error {
|
func (c *Command) SetInput(value any) error {
|
||||||
if len(c.Input) > 0 {
|
if len(c.Input) > 0 {
|
||||||
if r, err := c.Input.Template(value); err != nil {
|
if r, err := c.Input.Template(value); err != nil {
|
||||||
|
slog.Info("Command.SetInput", "input", r.String(), "error", err)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
|
slog.Info("Command.SetInput", "input", r.String())
|
||||||
c.SetStdinReader(strings.NewReader(r.String()))
|
c.SetStdinReader(strings.NewReader(r.String()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,24 +212,3 @@ func (c *CommandInput) Template(value any) (result strings.Builder, err error) {
|
|||||||
err = template.Must(template.New("commandInput").Parse(string(*c))).Execute(&result, value)
|
err = template.Must(template.New("commandInput").Parse(string(*c))).Execute(&result, value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandArg) UnmarshalValue(value string) error {
|
|
||||||
*c = CommandArg(value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CommandArg) UnmarshalJSON(data []byte) error {
|
|
||||||
var s string
|
|
||||||
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
|
|
||||||
return unmarshalRouteTypeErr
|
|
||||||
}
|
|
||||||
return c.UnmarshalValue(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error {
|
|
||||||
var s string
|
|
||||||
if err := value.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.UnmarshalValue(s)
|
|
||||||
}
|
|
||||||
|
31
internal/command/commandarg.go
Normal file
31
internal/command/commandarg.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandArg string
|
||||||
|
|
||||||
|
func (c *CommandArg) UnmarshalValue(value string) error {
|
||||||
|
*c = CommandArg(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandArg) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
|
||||||
|
return unmarshalRouteTypeErr
|
||||||
|
}
|
||||||
|
return c.UnmarshalValue(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.UnmarshalValue(s)
|
||||||
|
}
|
30
internal/command/commandtargetref.go
Normal file
30
internal/command/commandtargetref.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"decl/internal/identifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandTargetRef identifier.ID
|
||||||
|
|
||||||
|
func (r CommandTargetRef) GetType() (CommandType) {
|
||||||
|
uri := identifier.ID(r).Parse()
|
||||||
|
if uri != nil {
|
||||||
|
var ct CommandType = CommandType(uri.Scheme)
|
||||||
|
if ct.Validate() == nil {
|
||||||
|
return ct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r CommandTargetRef) Provider(cmd *Command, value any) CommandProvider {
|
||||||
|
return r.GetType().Provider(cmd, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r CommandTargetRef) Name() string {
|
||||||
|
u := identifier.ID(r).Parse()
|
||||||
|
return filepath.Join(u.Hostname(), u.Path)
|
||||||
|
}
|
44
internal/command/commandtype.go
Normal file
44
internal/command/commandtype.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidCommandType error = errors.New("Invalid command type")
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExecCommand CommandType = "exec"
|
||||||
|
ContainerCommand CommandType = "container"
|
||||||
|
SSHCommand CommandType = "ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func (t CommandType) Validate() error {
|
||||||
|
switch t {
|
||||||
|
case ExecCommand, ContainerCommand, SSHCommand:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: %s", ErrInvalidCommandType, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t CommandType) Provider(cmd *Command, value any) CommandProvider {
|
||||||
|
switch t {
|
||||||
|
case ContainerCommand:
|
||||||
|
return NewContainerProvider(cmd, value)
|
||||||
|
case SSHCommand:
|
||||||
|
return NewSSHProvider(cmd, value)
|
||||||
|
default:
|
||||||
|
fallthrough
|
||||||
|
case ExecCommand:
|
||||||
|
return NewExecProvider(cmd, value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
18
internal/command/commandtype_test.go
Normal file
18
internal/command/commandtype_test.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommandType(t *testing.T) {
|
||||||
|
|
||||||
|
var cmdType CommandType = "container"
|
||||||
|
|
||||||
|
assert.Nil(t, cmdType.Validate())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
104
internal/command/container_test.go
Normal file
104
internal/command/container_test.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
_ "os"
|
||||||
|
_ "strings"
|
||||||
|
"testing"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
func TestNewContainerProvider(t *testing.T) {
|
||||||
|
c := NewContainerProvider(nil, nil)
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func TestContainerCommandLoad(t *testing.T) {
|
||||||
|
c := NewCommand()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
|
decl := `
|
||||||
|
path: find
|
||||||
|
args:
|
||||||
|
- "{{ .Path }}"
|
||||||
|
`
|
||||||
|
|
||||||
|
assert.Nil(t, c.LoadDecl(decl))
|
||||||
|
assert.Equal(t, "find", c.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerCommandTemplate(t *testing.T) {
|
||||||
|
c := NewCommand()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
|
decl := `
|
||||||
|
path: find
|
||||||
|
args:
|
||||||
|
- "{{ .Path }}"
|
||||||
|
`
|
||||||
|
|
||||||
|
assert.Nil(t, c.LoadDecl(decl))
|
||||||
|
assert.Equal(t, "find", c.Path)
|
||||||
|
assert.Equal(t, 1, len(c.Args))
|
||||||
|
|
||||||
|
f := struct { Path string } {
|
||||||
|
Path: "./",
|
||||||
|
}
|
||||||
|
|
||||||
|
args, templateErr := c.Template(f)
|
||||||
|
assert.Nil(t, templateErr)
|
||||||
|
assert.Equal(t, 1, len(args))
|
||||||
|
|
||||||
|
assert.Equal(t, "./", string(args[0]))
|
||||||
|
|
||||||
|
out, err := c.Execute(f)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Greater(t, len(out), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerCommandStdin(t *testing.T) {
|
||||||
|
var expected string = "stdin test data"
|
||||||
|
var stdinBuffer bytes.Buffer
|
||||||
|
stdinBuffer.WriteString(expected)
|
||||||
|
|
||||||
|
c := NewCommand()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
|
decl := `
|
||||||
|
path: cat
|
||||||
|
stdinavailable: true
|
||||||
|
`
|
||||||
|
|
||||||
|
assert.Nil(t, c.LoadDecl(decl))
|
||||||
|
assert.Equal(t, "cat", c.Path)
|
||||||
|
|
||||||
|
c.SetStdinReader(&stdinBuffer)
|
||||||
|
out, err := c.Execute(nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, expected, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerCommandExitCode(t *testing.T) {
|
||||||
|
c := NewCommand()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
decl := `
|
||||||
|
path: ls
|
||||||
|
args:
|
||||||
|
- "amissingfile"
|
||||||
|
`
|
||||||
|
|
||||||
|
assert.Nil(t, c.LoadDecl(decl))
|
||||||
|
assert.Equal(t, "ls", c.Path)
|
||||||
|
|
||||||
|
out, err := c.Execute(nil)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Greater(t, c.ExitCode, 0)
|
||||||
|
assert.Equal(t, string(out), c.Stdout)
|
||||||
|
assert.Equal(t, string("ls: amissingfile: No such file or directory\n"), c.Stderr)
|
||||||
|
}
|
142
internal/command/containercommand.go
Normal file
142
internal/command/containercommand.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// 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
|
||||||
|
}
|
34
internal/command/execcommand.go
Normal file
34
internal/command/execcommand.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExecCommandProvider struct {
|
||||||
|
*exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consturct a new exec
|
||||||
|
func NewExecProvider(c *Command, value any) *ExecCommandProvider {
|
||||||
|
if args, err := c.Template(value); err == nil {
|
||||||
|
slog.Info("command.NewExecProvider", "command", c.Path, "args", args, "target", c.TargetRef)
|
||||||
|
return &ExecCommandProvider{exec.Command(c.Path, args...)}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExecCommandProvider) SetCmdEnv(env []string) {
|
||||||
|
e.Cmd.Env = append(os.Environ(), env...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExecCommandProvider) SetStdin(r io.Reader) {
|
||||||
|
e.Cmd.Stdin = r
|
||||||
|
}
|
54
internal/command/sshcommand.go
Normal file
54
internal/command/sshcommand.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
)
|
||||||
|
|
||||||
|
type SSHCommandProvider struct {
|
||||||
|
Env []string
|
||||||
|
Stdin io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consturct a new ssh exec
|
||||||
|
func NewSSHProvider(c *Command, value any) (p *SSHCommandProvider) {
|
||||||
|
if args, err := c.Template(value); err == nil {
|
||||||
|
slog.Info("command.NewSSHProvider", "command", c.Path, "args", args, "target", c.TargetRef)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHCommandProvider) Start() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHCommandProvider) Wait() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHCommandProvider) StdinPipe() (w io.WriteCloser, err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHCommandProvider) StdoutPipe() (r io.ReadCloser, err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHCommandProvider) StderrPipe() (r io.ReadCloser, err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHCommandProvider) SetCmdEnv(env []string) {
|
||||||
|
s.Env = append(os.Environ(), env...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSHCommandProvider) SetStdin(r io.Reader) {
|
||||||
|
s.Stdin = r
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user