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,14 +16,23 @@ 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 { | ||||||
| @ -44,6 +50,9 @@ type Command struct { | |||||||
| 	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