feat: "exec" command to start processes inside remote containers (#139)

This commit is contained in:
Anton Ovchinnikov
2025-11-05 23:47:24 +01:00
committed by GitHub
parent 1c3f210848
commit abea3e229e
19 changed files with 1773 additions and 133 deletions
+1
View File
@@ -28,6 +28,7 @@ type ContainerClient interface {
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error
ExecContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, config ExecOptions) (int, error)
}
type DNSClient interface {
+37
View File
@@ -0,0 +1,37 @@
package api
import "io"
// ExecOptions contains configuration for executing a command in a container.
type ExecOptions struct {
// Command is the command to run in the container.
Command []string
// AttachStdin attaches the stdin stream to the exec session.
AttachStdin bool
// AttachStdout attaches the stdout stream to the exec session.
AttachStdout bool
// AttachStderr attaches the stderr stream to the exec session.
AttachStderr bool
// Tty allocates a pseudo-TTY for the exec session.
Tty bool
// Detach runs the command in the background without attaching to streams.
Detach bool
//// Not yet implemented fields
// User specifies the user to run the command as.
User string
// Privileged runs the command in privileged mode.
Privileged bool
// WorkingDir sets the working directory for the command.
WorkingDir string
// Env sets environment variables for the command.
Env []string
// Client-side only fields (not serialized, not sent to server)
// Stdin is the input stream. Defaults to os.Stdin if nil.
Stdin io.Reader `json:"-"`
// Stdout is the output stream. Defaults to os.Stdout if nil.
Stdout io.Writer `json:"-"`
// Stderr is the error stream. Defaults to os.Stderr if nil.
Stderr io.Writer `json:"-"`
}
+46
View File
@@ -299,3 +299,49 @@ func (cli *Client) RemoveContainer(
return nil
}
// ExecContainer executes a command in a container within the service.
// If containerNameOrID is empty, the first container in the service will be used.
func (cli *Client) ExecContainer(
ctx context.Context, serviceNameOrID, containerNameOrID string, execOpts api.ExecOptions,
) (int, error) {
var ctr api.MachineServiceContainer
if containerNameOrID == "" {
// Find the first (random) container in the service
service, err := cli.InspectService(ctx, serviceNameOrID)
if err != nil {
return -1, fmt.Errorf("inspect service: %w", err)
}
if len(service.Containers) == 0 {
return -1, fmt.Errorf("no containers found in service %s", serviceNameOrID)
}
ctr = service.Containers[0]
} else {
// Find the specific container
var err error
ctr, err = cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID)
if err != nil {
return -1, fmt.Errorf("inspect container: %w", err)
}
}
machine, err := cli.InspectMachine(ctx, ctr.MachineID)
if err != nil {
return -1, fmt.Errorf("inspect machine '%s': %w", ctr.MachineID, err)
}
// Proxy Docker gRPC requests to the machine hosting the container
ctx = proxyToMachine(ctx, machine.Machine)
// Execute the command in the container
exitCode, err := cli.Docker.ExecContainer(ctx, machinedocker.ExecConfig{
ContainerID: ctr.Container.ID,
Options: execOpts,
})
if err != nil {
return exitCode, fmt.Errorf("exec in container %s: %w", ctr.Container.Name, err)
}
return exitCode, nil
}