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
+4
View File
@@ -16,6 +16,7 @@ import (
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/cli/config"
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/log"
"github.com/psviderski/uncloud/internal/version" "github.com/psviderski/uncloud/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -26,6 +27,8 @@ type globalOptions struct {
} }
func main() { func main() {
log.InitLoggerFromEnv()
opts := globalOptions{} opts := globalOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uc", Use: "uc",
@@ -87,6 +90,7 @@ func main() {
image.NewRootCommand(), image.NewRootCommand(),
machine.NewRootCommand(), machine.NewRootCommand(),
service.NewRootCommand(), service.NewRootCommand(),
service.NewExecCommand(),
service.NewInspectCommand(), service.NewInspectCommand(),
service.NewListCommand(), service.NewListCommand(),
service.NewRmCommand(), service.NewRmCommand(),
+130
View File
@@ -0,0 +1,130 @@
package service
import (
"context"
"fmt"
"os"
"github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra"
)
type execCliOptions struct {
detach bool
interactive bool
noTty bool
context string
containerId string
}
var DEFAULT_COMMAND = []string{"sh", "-c", "command -v bash >/dev/null 2>&1 && exec bash || exec sh"}
func NewExecCommand() *cobra.Command {
opts := execCliOptions{}
execCmd := &cobra.Command{
Use: "exec [OPTIONS] SERVICE [COMMAND ARGS...]",
Short: "Execute a command in a running service container",
Long: `Execute a command (interactive shell by default) in a running container within a service.
If the service has multiple replicas, the command will be executed in a random container.
`,
Example: `
# Start an interactive shell ("bash" or "sh" will be tried by default)
uc exec web-service
# Start an interactive shell with explicit command
uc exec web-service /bin/zsh
# List files in the specific container of the service
uc exec --container d792ea7347e5 web-service ls -la
# Pipe input to a command inside the service container
cat backup.sql | uc exec -T db-service psql -U postgres mydb
# Run a task in the background (detached mode)
uc exec -d web-service /scripts/cleanup.sh`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
serviceName := args[0]
command := args[1:]
if len(command) == 0 {
command = DEFAULT_COMMAND
}
return runExec(cmd.Context(), uncli, serviceName, command, opts)
},
}
execCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
execCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !cli.IsStdoutTerminal(),
"Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.")
// Keep "-i" and "-t" flags hidden for compatibility with docker exec
execCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
execCmd.Flags().MarkHidden("interactive")
execCmd.Flags().BoolP("tty", "t", false, "Allocate a pseudo-TTY")
execCmd.Flags().MarkHidden("tty")
execCmd.Flags().StringVarP(&opts.context, "context", "c", "",
"Name of the cluster context. (default is the current context)")
// Common flags
execCmd.Flags().StringVar(&opts.containerId, "container", "",
"ID of the container to exec into (default is the random container of the service)")
// This tells Cobra that all flags must come before positional arguments, so that
// commands with their own flags can be handled correctly.
execCmd.Flags().SetInterspersed(false)
return execCmd
}
func runExec(ctx context.Context, uncli *cli.CLI, serviceName string, command []string, opts execCliOptions) error {
if !opts.detach {
// Check if we're trying to attach to a TTY from a non-TTY client, e.g.
// when doing an 'cmd | uc exec ...'
stdin := streams.NewIn(os.Stdin)
// TODO: this logic/behavior mirrors docker-compose, but we can be smarter about it and detect TTY dynamically
if err := stdin.CheckTty(opts.interactive, !opts.noTty); err != nil {
return fmt.Errorf("check TTY: %w; use -T option to disable TTY allocation", err)
}
}
client, err := uncli.ConnectClusterWithOptions(ctx, opts.context, cli.ConnectOptions{
ShowProgress: false,
})
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer client.Close()
execConfig := api.ExecOptions{
Command: command,
AttachStdin: opts.interactive,
Tty: !opts.noTty,
Detach: opts.detach,
}
if !opts.detach {
execConfig.AttachStdout = true
execConfig.AttachStderr = true
}
exitCode, err := client.ExecContainer(ctx, serviceName, opts.containerId, execConfig)
if err != nil {
return fmt.Errorf("exec container: %w", err)
}
// For non-detached mode, exit with the same code as the executed command
if !opts.detach {
if exitCode != 0 {
os.Exit(exitCode)
}
}
return nil
}
+1
View File
@@ -16,6 +16,7 @@ func NewRootCommand() *cobra.Command {
NewRmCommand(), NewRmCommand(),
NewRunCommand(), NewRunCommand(),
NewScaleCommand(), NewScaleCommand(),
NewExecCommand(),
) )
return cmd return cmd
} }
+1 -1
View File
@@ -41,6 +41,7 @@ require (
github.com/lmittmann/tint v1.0.5 github.com/lmittmann/tint v1.0.5
github.com/miekg/dns v1.1.65 github.com/miekg/dns v1.1.65
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/moby/term v0.5.2
github.com/muesli/termenv v0.16.0 github.com/muesli/termenv v0.16.0
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/image-spec v1.1.1
@@ -271,7 +272,6 @@ require (
github.com/moby/sys/symlink v0.3.0 // indirect github.com/moby/sys/symlink v0.3.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
+19
View File
@@ -0,0 +1,19 @@
package log
import (
"log/slog"
"os"
"slices"
"strings"
)
func InitLoggerFromEnv() {
debugValues := []string{"1", "true", "yes"}
if slices.Contains(debugValues, strings.ToLower(os.Getenv("DEBUG"))) {
logger := slog.New(NewSlogTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
slog.SetDefault(logger)
}
slog.Debug("logger initialized")
}
+554 -132
View File
@@ -1727,6 +1727,329 @@ func (x *MachineServiceContainers) GetContainers() []*ServiceContainer {
return nil return nil
} }
type ExecContainerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to Payload:
//
// *ExecContainerRequest_Config
// *ExecContainerRequest_Stdin
// *ExecContainerRequest_Resize
Payload isExecContainerRequest_Payload `protobuf_oneof:"payload"`
}
func (x *ExecContainerRequest) Reset() {
*x = ExecContainerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ExecContainerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExecContainerRequest) ProtoMessage() {}
func (x *ExecContainerRequest) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[32]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExecContainerRequest.ProtoReflect.Descriptor instead.
func (*ExecContainerRequest) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{32}
}
func (m *ExecContainerRequest) GetPayload() isExecContainerRequest_Payload {
if m != nil {
return m.Payload
}
return nil
}
func (x *ExecContainerRequest) GetConfig() *ExecConfig {
if x, ok := x.GetPayload().(*ExecContainerRequest_Config); ok {
return x.Config
}
return nil
}
func (x *ExecContainerRequest) GetStdin() []byte {
if x, ok := x.GetPayload().(*ExecContainerRequest_Stdin); ok {
return x.Stdin
}
return nil
}
func (x *ExecContainerRequest) GetResize() *ResizeEvent {
if x, ok := x.GetPayload().(*ExecContainerRequest_Resize); ok {
return x.Resize
}
return nil
}
type isExecContainerRequest_Payload interface {
isExecContainerRequest_Payload()
}
type ExecContainerRequest_Config struct {
// Initial configuration for the exec session. Must be sent as the first message.
Config *ExecConfig `protobuf:"bytes,1,opt,name=config,proto3,oneof"`
}
type ExecContainerRequest_Stdin struct {
// Raw stdin data to be written to the exec process.
Stdin []byte `protobuf:"bytes,2,opt,name=stdin,proto3,oneof"`
}
type ExecContainerRequest_Resize struct {
// TTY resize event (only used when TTY is enabled).
Resize *ResizeEvent `protobuf:"bytes,3,opt,name=resize,proto3,oneof"`
}
func (*ExecContainerRequest_Config) isExecContainerRequest_Payload() {}
func (*ExecContainerRequest_Stdin) isExecContainerRequest_Payload() {}
func (*ExecContainerRequest_Resize) isExecContainerRequest_Payload() {}
type ExecConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Container ID to execute the command in.
ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"`
// JSON serialised ExecOptions
Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
}
func (x *ExecConfig) Reset() {
*x = ExecConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ExecConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExecConfig) ProtoMessage() {}
func (x *ExecConfig) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[33]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExecConfig.ProtoReflect.Descriptor instead.
func (*ExecConfig) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{33}
}
func (x *ExecConfig) GetContainerId() string {
if x != nil {
return x.ContainerId
}
return ""
}
func (x *ExecConfig) GetOptions() []byte {
if x != nil {
return x.Options
}
return nil
}
type ResizeEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Height uint32 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"`
Width uint32 `protobuf:"varint,2,opt,name=width,proto3" json:"width,omitempty"`
}
func (x *ResizeEvent) Reset() {
*x = ResizeEvent{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ResizeEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ResizeEvent) ProtoMessage() {}
func (x *ResizeEvent) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[34]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ResizeEvent.ProtoReflect.Descriptor instead.
func (*ResizeEvent) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{34}
}
func (x *ResizeEvent) GetHeight() uint32 {
if x != nil {
return x.Height
}
return 0
}
func (x *ResizeEvent) GetWidth() uint32 {
if x != nil {
return x.Width
}
return 0
}
type ExecContainerResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to Payload:
//
// *ExecContainerResponse_ExecId
// *ExecContainerResponse_Stdout
// *ExecContainerResponse_Stderr
// *ExecContainerResponse_ExitCode
Payload isExecContainerResponse_Payload `protobuf_oneof:"payload"`
}
func (x *ExecContainerResponse) Reset() {
*x = ExecContainerResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ExecContainerResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExecContainerResponse) ProtoMessage() {}
func (x *ExecContainerResponse) ProtoReflect() protoreflect.Message {
mi := &file_internal_machine_api_pb_docker_proto_msgTypes[35]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExecContainerResponse.ProtoReflect.Descriptor instead.
func (*ExecContainerResponse) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{35}
}
func (m *ExecContainerResponse) GetPayload() isExecContainerResponse_Payload {
if m != nil {
return m.Payload
}
return nil
}
func (x *ExecContainerResponse) GetExecId() string {
if x, ok := x.GetPayload().(*ExecContainerResponse_ExecId); ok {
return x.ExecId
}
return ""
}
func (x *ExecContainerResponse) GetStdout() []byte {
if x, ok := x.GetPayload().(*ExecContainerResponse_Stdout); ok {
return x.Stdout
}
return nil
}
func (x *ExecContainerResponse) GetStderr() []byte {
if x, ok := x.GetPayload().(*ExecContainerResponse_Stderr); ok {
return x.Stderr
}
return nil
}
func (x *ExecContainerResponse) GetExitCode() int32 {
if x, ok := x.GetPayload().(*ExecContainerResponse_ExitCode); ok {
return x.ExitCode
}
return 0
}
type isExecContainerResponse_Payload interface {
isExecContainerResponse_Payload()
}
type ExecContainerResponse_ExecId struct {
// Exec instance ID returned after creating the exec.
ExecId string `protobuf:"bytes,1,opt,name=exec_id,json=execId,proto3,oneof"`
}
type ExecContainerResponse_Stdout struct {
// Raw stdout data from the exec process.
Stdout []byte `protobuf:"bytes,2,opt,name=stdout,proto3,oneof"`
}
type ExecContainerResponse_Stderr struct {
// Raw stderr data from the exec process (only when TTY is disabled).
Stderr []byte `protobuf:"bytes,3,opt,name=stderr,proto3,oneof"`
}
type ExecContainerResponse_ExitCode struct {
// Exit code of the exec process. Sent as the final message.
ExitCode int32 `protobuf:"varint,4,opt,name=exit_code,json=exitCode,proto3,oneof"`
}
func (*ExecContainerResponse_ExecId) isExecContainerResponse_Payload() {}
func (*ExecContainerResponse_Stdout) isExecContainerResponse_Payload() {}
func (*ExecContainerResponse_Stderr) isExecContainerResponse_Payload() {}
func (*ExecContainerResponse_ExitCode) isExecContainerResponse_Payload() {}
var File_internal_machine_api_pb_docker_proto protoreflect.FileDescriptor var File_internal_machine_api_pb_docker_proto protoreflect.FileDescriptor
var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
@@ -1887,91 +2210,123 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xfb, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x90,
0x09, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x01, 0x0a, 0x14, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69,
0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x05, 0x73, 0x74, 0x64, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x0c, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x64, 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65,
0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x06,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61,
0x64, 0x22, 0x49, 0x0a, 0x0a, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3b, 0x0a, 0x0b,
0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x68,
0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x68, 0x65, 0x69,
0x67, 0x68, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0d, 0x52, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x22, 0x90, 0x01, 0x0a, 0x15, 0x45, 0x78,
0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x07, 0x65, 0x78, 0x65, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x65, 0x78, 0x65, 0x63, 0x49, 0x64, 0x12, 0x18,
0x0a, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00,
0x52, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x12, 0x18, 0x0a, 0x06, 0x73, 0x74, 0x64, 0x65,
0x72, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x64, 0x65,
0x72, 0x72, 0x12, 0x1d, 0x0a, 0x09, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18,
0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64,
0x65, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x32, 0xc7, 0x0a, 0x0a,
0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72,
0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
0x0a, 0x0d, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12,
0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a,
0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61,
0x67, 0x65, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61,
0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a,
0x0c, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x55, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d,
0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0a, 0x4c, 0x69, 0x73,
0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56,
0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a,
0x0b, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x17, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x40, 0x0a, 0x0c, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12,
0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75,
0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 0x12, 0x5a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a,
0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x5e, 0x0a,
0x15, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a,
0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65,
0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x37, 0x5a, 0x35, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e,
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x12, 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52,
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65,
0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53,
0x4f, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x55, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74,
0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x49,
0x6d, 0x61, 0x67, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c,
0x75, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x4c,
0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f,
0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a,
0x0c, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
0x5a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x15, 0x4c,
0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52,
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f,
0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4a, 0x0a, 0x0d, 0x45, 0x78,
0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78, 0x65,
0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f,
0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@@ -1986,7 +2341,7 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
return file_internal_machine_api_pb_docker_proto_rawDescData return file_internal_machine_api_pb_docker_proto_rawDescData
} }
var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 36)
var file_internal_machine_api_pb_docker_proto_goTypes = []any{ var file_internal_machine_api_pb_docker_proto_goTypes = []any{
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest (*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse (*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
@@ -2020,62 +2375,70 @@ var file_internal_machine_api_pb_docker_proto_goTypes = []any{
(*ListServiceContainersRequest)(nil), // 29: api.ListServiceContainersRequest (*ListServiceContainersRequest)(nil), // 29: api.ListServiceContainersRequest
(*ListServiceContainersResponse)(nil), // 30: api.ListServiceContainersResponse (*ListServiceContainersResponse)(nil), // 30: api.ListServiceContainersResponse
(*MachineServiceContainers)(nil), // 31: api.MachineServiceContainers (*MachineServiceContainers)(nil), // 31: api.MachineServiceContainers
(*Metadata)(nil), // 32: api.Metadata (*ExecContainerRequest)(nil), // 32: api.ExecContainerRequest
(*emptypb.Empty)(nil), // 33: google.protobuf.Empty (*ExecConfig)(nil), // 33: api.ExecConfig
(*ResizeEvent)(nil), // 34: api.ResizeEvent
(*ExecContainerResponse)(nil), // 35: api.ExecContainerResponse
(*Metadata)(nil), // 36: api.Metadata
(*emptypb.Empty)(nil), // 37: google.protobuf.Empty
} }
var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{ var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers 8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
32, // 1: api.MachineContainers.metadata:type_name -> api.Metadata 36, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
14, // 2: api.InspectImageResponse.messages:type_name -> api.Image 14, // 2: api.InspectImageResponse.messages:type_name -> api.Image
32, // 3: api.Image.metadata:type_name -> api.Metadata 36, // 3: api.Image.metadata:type_name -> api.Metadata
17, // 4: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage 17, // 4: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage
32, // 5: api.RemoteImage.metadata:type_name -> api.Metadata 36, // 5: api.RemoteImage.metadata:type_name -> api.Metadata
20, // 6: api.ListImagesResponse.messages:type_name -> api.MachineImages 20, // 6: api.ListImagesResponse.messages:type_name -> api.MachineImages
32, // 7: api.MachineImages.metadata:type_name -> api.Metadata 36, // 7: api.MachineImages.metadata:type_name -> api.Metadata
25, // 8: api.ListVolumesResponse.messages:type_name -> api.MachineVolumes 25, // 8: api.ListVolumesResponse.messages:type_name -> api.MachineVolumes
32, // 9: api.MachineVolumes.metadata:type_name -> api.Metadata 36, // 9: api.MachineVolumes.metadata:type_name -> api.Metadata
31, // 10: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers 31, // 10: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
32, // 11: api.MachineServiceContainers.metadata:type_name -> api.Metadata 36, // 11: api.MachineServiceContainers.metadata:type_name -> api.Metadata
28, // 12: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer 28, // 12: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
0, // 13: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest 33, // 13: api.ExecContainerRequest.config:type_name -> api.ExecConfig
2, // 14: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest 34, // 14: api.ExecContainerRequest.resize:type_name -> api.ResizeEvent
4, // 15: api.Docker.StartContainer:input_type -> api.StartContainerRequest 0, // 15: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
5, // 16: api.Docker.StopContainer:input_type -> api.StopContainerRequest 2, // 16: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
6, // 17: api.Docker.ListContainers:input_type -> api.ListContainersRequest 4, // 17: api.Docker.StartContainer:input_type -> api.StartContainerRequest
9, // 18: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest 5, // 18: api.Docker.StopContainer:input_type -> api.StopContainerRequest
10, // 19: api.Docker.PullImage:input_type -> api.PullImageRequest 6, // 19: api.Docker.ListContainers:input_type -> api.ListContainersRequest
12, // 20: api.Docker.InspectImage:input_type -> api.InspectImageRequest 9, // 20: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
15, // 21: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest 10, // 21: api.Docker.PullImage:input_type -> api.PullImageRequest
18, // 22: api.Docker.ListImages:input_type -> api.ListImagesRequest 12, // 22: api.Docker.InspectImage:input_type -> api.InspectImageRequest
21, // 23: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest 15, // 23: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
23, // 24: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest 18, // 24: api.Docker.ListImages:input_type -> api.ListImagesRequest
26, // 25: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest 21, // 25: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
27, // 26: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest 23, // 26: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
2, // 27: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest 26, // 27: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
29, // 28: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest 27, // 28: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
9, // 29: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest 2, // 29: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
1, // 30: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse 29, // 30: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
3, // 31: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse 9, // 31: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
33, // 32: api.Docker.StartContainer:output_type -> google.protobuf.Empty 32, // 32: api.Docker.ExecContainer:input_type -> api.ExecContainerRequest
33, // 33: api.Docker.StopContainer:output_type -> google.protobuf.Empty 1, // 33: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
7, // 34: api.Docker.ListContainers:output_type -> api.ListContainersResponse 3, // 34: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
33, // 35: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty 37, // 35: api.Docker.StartContainer:output_type -> google.protobuf.Empty
11, // 36: api.Docker.PullImage:output_type -> api.JSONMessage 37, // 36: api.Docker.StopContainer:output_type -> google.protobuf.Empty
13, // 37: api.Docker.InspectImage:output_type -> api.InspectImageResponse 7, // 37: api.Docker.ListContainers:output_type -> api.ListContainersResponse
16, // 38: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse 37, // 38: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
19, // 39: api.Docker.ListImages:output_type -> api.ListImagesResponse 11, // 39: api.Docker.PullImage:output_type -> api.JSONMessage
22, // 40: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse 13, // 40: api.Docker.InspectImage:output_type -> api.InspectImageResponse
24, // 41: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse 16, // 41: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
33, // 42: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty 19, // 42: api.Docker.ListImages:output_type -> api.ListImagesResponse
1, // 43: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse 22, // 43: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
28, // 44: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer 24, // 44: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
30, // 45: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse 37, // 45: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
33, // 46: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty 1, // 46: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
30, // [30:47] is the sub-list for method output_type 28, // 47: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
13, // [13:30] is the sub-list for method input_type 30, // 48: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
13, // [13:13] is the sub-list for extension type_name 37, // 49: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
13, // [13:13] is the sub-list for extension extendee 35, // 50: api.Docker.ExecContainer:output_type -> api.ExecContainerResponse
0, // [0:13] is the sub-list for field type_name 33, // [33:51] is the sub-list for method output_type
15, // [15:33] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension type_name
15, // [15:15] is the sub-list for extension extendee
0, // [0:15] is the sub-list for field type_name
} }
func init() { file_internal_machine_api_pb_docker_proto_init() } func init() { file_internal_machine_api_pb_docker_proto_init() }
@@ -2469,6 +2832,65 @@ func file_internal_machine_api_pb_docker_proto_init() {
return nil return nil
} }
} }
file_internal_machine_api_pb_docker_proto_msgTypes[32].Exporter = func(v any, i int) any {
switch v := v.(*ExecContainerRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[33].Exporter = func(v any, i int) any {
switch v := v.(*ExecConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[34].Exporter = func(v any, i int) any {
switch v := v.(*ResizeEvent); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[35].Exporter = func(v any, i int) any {
switch v := v.(*ExecContainerResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_internal_machine_api_pb_docker_proto_msgTypes[32].OneofWrappers = []any{
(*ExecContainerRequest_Config)(nil),
(*ExecContainerRequest_Stdin)(nil),
(*ExecContainerRequest_Resize)(nil),
}
file_internal_machine_api_pb_docker_proto_msgTypes[35].OneofWrappers = []any{
(*ExecContainerResponse_ExecId)(nil),
(*ExecContainerResponse_Stdout)(nil),
(*ExecContainerResponse_Stderr)(nil),
(*ExecContainerResponse_ExitCode)(nil),
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@@ -2476,7 +2898,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc, RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 32, NumMessages: 36,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+38
View File
@@ -30,6 +30,8 @@ service Docker {
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer); rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse); rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty); rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
rpc ExecContainer(stream ExecContainerRequest) returns (stream ExecContainerResponse);
} }
message CreateContainerRequest { message CreateContainerRequest {
@@ -214,3 +216,39 @@ message MachineServiceContainers {
Metadata metadata = 1; Metadata metadata = 1;
repeated ServiceContainer containers = 2; repeated ServiceContainer containers = 2;
} }
message ExecContainerRequest {
oneof payload {
// Initial configuration for the exec session. Must be sent as the first message.
ExecConfig config = 1;
// Raw stdin data to be written to the exec process.
bytes stdin = 2;
// TTY resize event (only used when TTY is enabled).
ResizeEvent resize = 3;
}
}
message ExecConfig {
// Container ID to execute the command in.
string container_id = 1;
// JSON serialised ExecOptions
bytes options = 2;
}
message ResizeEvent {
uint32 height = 1;
uint32 width = 2;
}
message ExecContainerResponse {
oneof payload {
// Exec instance ID returned after creating the exec.
string exec_id = 1;
// Raw stdout data from the exec process.
bytes stdout = 2;
// Raw stderr data from the exec process (only when TTY is disabled).
bytes stderr = 3;
// Exit code of the exec process. Sent as the final message.
int32 exit_code = 4;
}
}
+32
View File
@@ -37,6 +37,7 @@ const (
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer" Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers" Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer" Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
Docker_ExecContainer_FullMethodName = "/api.Docker/ExecContainer"
) )
// DockerClient is the client API for Docker service. // DockerClient is the client API for Docker service.
@@ -62,6 +63,7 @@ type DockerClient interface {
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error) InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error)
} }
type dockerClient struct { type dockerClient struct {
@@ -251,6 +253,19 @@ func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveCon
return out, nil return out, nil
} }
func (c *dockerClient) ExecContainer(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &Docker_ServiceDesc.Streams[1], Docker_ExecContainer_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[ExecContainerRequest, ExecContainerResponse]{ClientStream: stream}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Docker_ExecContainerClient = grpc.BidiStreamingClient[ExecContainerRequest, ExecContainerResponse]
// DockerServer is the server API for Docker service. // DockerServer is the server API for Docker service.
// All implementations must embed UnimplementedDockerServer // All implementations must embed UnimplementedDockerServer
// for forward compatibility. // for forward compatibility.
@@ -274,6 +289,7 @@ type DockerServer interface {
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error
mustEmbedUnimplementedDockerServer() mustEmbedUnimplementedDockerServer()
} }
@@ -335,6 +351,9 @@ func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListSer
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) { func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented") return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented")
} }
func (UnimplementedDockerServer) ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error {
return status.Errorf(codes.Unimplemented, "method ExecContainer not implemented")
}
func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {} func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {}
func (UnimplementedDockerServer) testEmbeddedByValue() {} func (UnimplementedDockerServer) testEmbeddedByValue() {}
@@ -655,6 +674,13 @@ func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Docker_ExecContainer_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(DockerServer).ExecContainer(&grpc.GenericServerStream[ExecContainerRequest, ExecContainerResponse]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Docker_ExecContainerServer = grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]
// Docker_ServiceDesc is the grpc.ServiceDesc for Docker service. // Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -733,6 +759,12 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
Handler: _Docker_PullImage_Handler, Handler: _Docker_PullImage_Handler,
ServerStreams: true, ServerStreams: true,
}, },
{
StreamName: "ExecContainer",
Handler: _Docker_ExecContainer_Handler,
ServerStreams: true,
ClientStreams: true,
},
}, },
Metadata: "internal/machine/api/pb/docker.proto", Metadata: "internal/machine/api/pb/docker.proto",
} }
+303
View File
@@ -0,0 +1,303 @@
package docker
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"os"
"os/signal"
"github.com/moby/term"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"golang.org/x/sync/errgroup"
"golang.org/x/sys/unix"
)
// ExecConfig contains options for executing a command in a container.
type ExecConfig struct {
// Container ID to execute the command in.
ContainerID string
// Exec configuration.
Options api.ExecOptions
}
// sendResizeRequest sends a terminal resize request to the exec stream.
func sendResizeRequest(stream pb.Docker_ExecContainerClient, size *term.Winsize) error {
slog.Debug("sending resize request", "width", size.Width, "height", size.Height)
return stream.Send(
&pb.ExecContainerRequest{
Payload: &pb.ExecContainerRequest_Resize{
Resize: &pb.ResizeEvent{
Height: uint32(size.Height),
Width: uint32(size.Width),
},
},
})
}
// setupTerminal configures the terminal for interactive TTY sessions.
// It checks if stdin is a terminal, sets it to raw mode, and sets up resize handling.
// Returns a cleanup function to restore terminal state, or an error.
func setupTerminal(ctx context.Context, stream pb.Docker_ExecContainerClient) (func(), error) {
inFd, isTerminal := term.GetFdInfo(os.Stdin)
if !isTerminal {
return nil, fmt.Errorf("stdin is not a terminal")
}
// Set terminal to raw mode
oldState, err := term.SetRawTerminal(inFd)
if err != nil {
return nil, fmt.Errorf("set raw terminal: %w", err)
}
// Cleanup function
restoreFunc := func() {
_ = term.RestoreTerminal(inFd, oldState)
}
// Set up resize handling
if err := handleTerminalResize(ctx, inFd, stream); err != nil {
restoreFunc()
return nil, err
}
return restoreFunc, nil
}
// handleTerminalResize sends initial window size and handles window resize signals for TTY sessions.
func handleTerminalResize(ctx context.Context, inFd uintptr, stream pb.Docker_ExecContainerClient) error {
// Handle window resize signals
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, unix.SIGWINCH)
// Send initial window size
if size, err := term.GetWinsize(inFd); err == nil {
_ = sendResizeRequest(stream, size)
}
go func() {
defer signal.Stop(sigCh)
for {
select {
case <-ctx.Done():
return
case <-sigCh:
size, err := term.GetWinsize(inFd)
if err != nil {
slog.Debug("get window size", "error", err)
continue
}
if err = sendResizeRequest(stream, size); err != nil {
slog.Debug("send resize request", "error", err)
}
}
}
}()
return nil
}
// handleClientInputStream reads from stdin and sends data to the remote server.
// It also periodically checks for context cancellation to exit gracefully when e.g.
// the output stream is closed.
func handleClientInputStream(ctx context.Context, stream pb.Docker_ExecContainerClient, stdin io.Reader) error {
slog.Debug("Input goroutine started")
defer slog.Debug("Input goroutine exited")
defer stream.CloseSend()
// Channel to receive stdin data
stdinCh := make(chan []byte)
stdinErrCh := make(chan error, 1)
// Read from stdin in a separate goroutine
// Note: this goroutine may continue blocking on Read even after we exit from the function,
// but that's OK - it will eventually unblock when data arrives or stdin closes.
go func() {
buf := make([]byte, 32*1024) // 32KB buffer
for {
n, err := stdin.Read(buf)
if n > 0 {
data := make([]byte, n)
copy(data, buf[:n])
select {
case stdinCh <- data:
case <-ctx.Done():
slog.Debug("stdin reader exiting due to context done")
return
}
}
if err != nil {
if err == io.EOF {
slog.Debug("stdin reader: EOF received")
} else {
slog.Debug("stdin reader error", "error", err)
}
stdinErrCh <- err
return
}
}
}()
// Send stdin data to the server or exit when context is cancelled
for {
select {
case <-ctx.Done():
return nil
case data := <-stdinCh:
if err := stream.Send(&pb.ExecContainerRequest{
Payload: &pb.ExecContainerRequest_Stdin{Stdin: data},
}); err != nil {
return fmt.Errorf("send stdin: %w", err)
}
case err := <-stdinErrCh:
if err != io.EOF {
return fmt.Errorf("read stdin: %w", err)
}
return nil
}
}
}
// handleClientOutputStream receives output from the exec stream and writes to stdout/stderr.
// It also captures the exit code and signals completion via context cancellation.
func handleClientOutputStream(ctx context.Context, stream pb.Docker_ExecContainerClient, stdout, stderr io.Writer, exitCode *int) error {
slog.Debug("Output goroutine started")
defer slog.Debug("Output goroutine exited")
for {
resp, err := stream.Recv()
if err == io.EOF {
slog.Debug("output stream: EOF received")
return nil
}
if err != nil {
return fmt.Errorf("receive from stream: %w", err)
}
switch payload := resp.Payload.(type) {
case *pb.ExecContainerResponse_ExecId:
// This is sent first; we already processed it earlier, so just ignore duplicates.
case *pb.ExecContainerResponse_Stdout:
if _, err := stdout.Write(payload.Stdout); err != nil {
return fmt.Errorf("write stdout: %w", err)
}
case *pb.ExecContainerResponse_Stderr:
if _, err := stderr.Write(payload.Stderr); err != nil {
return fmt.Errorf("write stderr: %w", err)
}
case *pb.ExecContainerResponse_ExitCode:
slog.Debug("received exit code", "code", payload.ExitCode)
*exitCode = int(payload.ExitCode)
return nil
}
}
}
// ExecContainer executes a command in a running container with bidirectional streaming.
// TODO: This can be merged with pkg/client as it's an unnecessary logic split.
func (c *Client) ExecContainer(ctx context.Context, opts ExecConfig) (exitCode int, err error) {
// TODO: We need to handle Ctrl-C and other signals here to forward them to the container process.
// Right now, Ctrl-C will just terminate the client process, which is not ideal.
// We should catch the signal, send it to the container process, and only exit
// when the container process exits.
slog.Debug("starting ExecContainer", "containerID", opts.ContainerID, "options", opts.Options)
// Initialize exit code to non-zero in case we have to return early
exitCode = 1
// Set up I/O streams - use custom streams if provided, otherwise default to os.Stdin/Stdout/Stderr
stdin := io.Reader(os.Stdin)
stdout := io.Writer(os.Stdout)
stderr := io.Writer(os.Stderr)
if opts.Options.Stdin != nil {
stdin = opts.Options.Stdin
}
if opts.Options.Stdout != nil {
stdout = opts.Options.Stdout
}
if opts.Options.Stderr != nil {
stderr = opts.Options.Stderr
}
// Marshal the exec config
configBytes, err := json.Marshal(opts.Options)
if err != nil {
return -1, fmt.Errorf("marshal exec config: %w", err)
}
// Create the bidirectional stream
stream, err := c.GRPCClient.ExecContainer(ctx)
if err != nil {
return -1, fmt.Errorf("create exec stream: %w", err)
}
// Send the initial configuration
if err := stream.Send(&pb.ExecContainerRequest{
Payload: &pb.ExecContainerRequest_Config{
Config: &pb.ExecConfig{
ContainerId: opts.ContainerID,
Options: configBytes,
},
},
}); err != nil {
return -1, fmt.Errorf("send exec config: %w", err)
}
// Receive the exec ID
resp, err := stream.Recv()
if err != nil {
return -1, fmt.Errorf("receive exec ID: %w", err)
}
execID := resp.GetExecId()
if execID == "" {
return -1, fmt.Errorf("expected exec ID in first response")
}
errGroup, ctx := errgroup.WithContext(ctx)
// Create cancellable context for goroutine coordination
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Handle terminal setup for interactive sessions
if opts.Options.AttachStdin && opts.Options.Tty {
restoreTerminal, err := setupTerminal(ctx, stream)
if err != nil {
return -1, fmt.Errorf("setup terminal: %w", err)
}
if restoreTerminal != nil {
defer restoreTerminal()
}
}
// Handle stdin stream if needed
if opts.Options.AttachStdin {
errGroup.Go(func() error {
return handleClientInputStream(ctx, stream, stdin)
})
} else {
// Close send direction immediately if not attaching stdin
stream.CloseSend()
}
// Handle output streams (stdout/stderr)
errGroup.Go(func() error {
defer cancel()
return handleClientOutputStream(ctx, stream, stdout, stderr, &exitCode)
})
err = errGroup.Wait()
if err == nil && opts.Options.Detach {
return 0, nil
}
return exitCode, err
}
+249
View File
@@ -23,6 +23,7 @@ import (
"github.com/distribution/reference" "github.com/distribution/reference"
dockercommand "github.com/docker/cli/cli/command" dockercommand "github.com/docker/cli/cli/command"
dockerconfig "github.com/docker/cli/cli/config" dockerconfig "github.com/docker/cli/cli/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
@@ -30,6 +31,7 @@ import (
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
@@ -1012,3 +1014,250 @@ func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveConta
return resp, nil return resp, nil
} }
// receiveExecConfig receives and validates the initial exec configuration from the stream.
func (s *Server) receiveExecConfig(stream pb.Docker_ExecContainerServer) (*pb.ExecConfig, api.ExecOptions, error) {
req, err := stream.Recv()
if err != nil {
return nil, api.ExecOptions{}, status.Errorf(codes.InvalidArgument, "receive config: %v", err)
}
execConfig := req.GetConfig()
if execConfig == nil {
return nil, api.ExecOptions{}, status.Error(codes.InvalidArgument, "first message must contain exec config")
}
// Unmarshal the Uncloud's execOpts
var execOpts api.ExecOptions
if err := json.Unmarshal(execConfig.Options, &execOpts); err != nil {
return nil, api.ExecOptions{}, status.Errorf(codes.InvalidArgument, "unmarshal exec config: %v", err)
}
return execConfig, execOpts, nil
}
// handleServerExecInput reads from the gRPC stream and writes to Docker stdin, handling resize requests.
func (s *Server) handleServerExecInput(
ctx context.Context,
stream pb.Docker_ExecContainerServer,
attachConn types.HijackedResponse,
execID string,
tty bool,
) error {
slog.Debug("Input goroutine started", "exec_id", execID, "tty", tty)
defer slog.Debug("Input goroutine exited", "exec_id", execID)
defer attachConn.CloseWrite()
for {
req, err := stream.Recv()
switch {
case errors.Is(err, io.EOF):
slog.Debug("Input goroutine received EOF", "exec_id", execID)
return nil
case status.Code(err) == codes.Canceled:
// Can be the case when the output goroutine ends and the stream context is canceled.
slog.Debug("Input goroutine context canceled", "exec_id", execID)
return nil
case err == nil:
// continue processing
default:
return fmt.Errorf("receive from stream: %w", err)
}
switch payload := req.Payload.(type) {
case *pb.ExecContainerRequest_Stdin:
if _, err := attachConn.Conn.Write(payload.Stdin); err != nil {
return fmt.Errorf("write to stdin: %w", err)
}
case *pb.ExecContainerRequest_Resize:
if tty {
resizeOpts := container.ResizeOptions{
Height: uint(payload.Resize.Height),
Width: uint(payload.Resize.Width),
}
if err := s.client.ContainerExecResize(ctx, execID, resizeOpts); err != nil {
slog.Warn("Failed to resize TTY", "err", err, "exec_id", execID)
}
}
}
}
}
// handleServerExecOutput reads from Docker stdout/stderr and writes to the gRPC stream.
func (s *Server) handleServerExecOutput(
stream pb.Docker_ExecContainerServer,
attachResp types.HijackedResponse,
execID string,
tty bool,
) error {
slog.Debug("Output goroutine started", "exec_id", execID, "tty", tty)
defer slog.Debug("Output goroutine exited", "exec_id", execID)
if tty {
// In TTY mode, all output is stdout - copy directly to stream
stdoutWriter := &grpcStreamWriter{stream: stream, isStderr: false}
_, err := io.Copy(stdoutWriter, attachResp.Reader)
if err != nil && err != io.EOF {
return fmt.Errorf("copy tty output: %w", err)
}
return nil
} else {
// In non-TTY mode, Docker multiplexes stdout/stderr with headers
// Use stdcopy to demultiplex
slog.Debug("Starting StdCopy for non-TTY", "exec_id", execID)
stdoutWriter := &grpcStreamWriter{stream: stream, isStderr: false}
stderrWriter := &grpcStreamWriter{stream: stream, isStderr: true}
written, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, attachResp.Reader)
slog.Debug("StdCopy completed", "exec_id", execID, "bytes", written, "err", err)
if err != nil && err != io.EOF {
return fmt.Errorf("demultiplex docker output: %w", err)
}
return nil
}
}
// grpcStreamWriter is a writer that sends data to a gRPC stream as stdout or stderr.
type grpcStreamWriter struct {
stream pb.Docker_ExecContainerServer
isStderr bool
}
func (w *grpcStreamWriter) Write(p []byte) (n int, err error) {
data := make([]byte, len(p))
copy(data, p)
var resp *pb.ExecContainerResponse
if w.isStderr {
resp = &pb.ExecContainerResponse{
Payload: &pb.ExecContainerResponse_Stderr{Stderr: data},
}
} else {
resp = &pb.ExecContainerResponse{
Payload: &pb.ExecContainerResponse_Stdout{Stdout: data},
}
}
if err := w.stream.Send(resp); err != nil {
return 0, err
}
return len(p), nil
}
// ExecContainer executes a command in a running container with bidirectional streaming for stdin/stdout/stderr.
func (s *Server) ExecContainer(stream pb.Docker_ExecContainerServer) error {
slog.Debug("ExecContainer server-side called")
defer slog.Debug("ExecContainer server-side ended")
ctx := stream.Context()
// Receive and validate configuration
execConfig, execOpts, err := s.receiveExecConfig(stream)
if err != nil {
return err
}
// Convert to Docker's ExecOptions
dockerExecOpts := container.ExecOptions{
Cmd: execOpts.Command,
AttachStdin: execOpts.AttachStdin,
AttachStdout: execOpts.AttachStdout,
AttachStderr: execOpts.AttachStderr,
Tty: execOpts.Tty,
User: execOpts.User,
Privileged: execOpts.Privileged,
WorkingDir: execOpts.WorkingDir,
Env: execOpts.Env,
}
// Create the exec instance
execResp, err := s.client.ContainerExecCreate(ctx, execConfig.ContainerId, dockerExecOpts)
if err != nil {
if errdefs.IsNotFound(err) {
return status.Error(codes.NotFound, err.Error())
}
return status.Errorf(codes.Internal, "create exec: %v", err)
}
// Send the exec ID back to the client
if err := stream.Send(&pb.ExecContainerResponse{
Payload: &pb.ExecContainerResponse_ExecId{ExecId: execResp.ID},
}); err != nil {
return status.Errorf(codes.Internal, "send exec ID: %v", err)
}
slog.Debug("Sent exec ID to the client", "exec_id", execResp.ID)
// For detached mode, start without attaching
if execOpts.Detach {
dockerStartOpts := container.ExecStartOptions{
Tty: dockerExecOpts.Tty,
Detach: true,
}
if err := s.client.ContainerExecStart(ctx, execResp.ID, dockerStartOpts); err != nil {
return status.Errorf(codes.Internal, "start exec: %v", err)
}
return nil
}
// For attached mode, attach to the exec instance
attachOpts := container.ExecAttachOptions{
Tty: dockerExecOpts.Tty,
}
attachConn, err := s.client.ContainerExecAttach(ctx, execResp.ID, attachOpts)
if err != nil {
return status.Errorf(codes.Internal, "attach to exec: %v", err)
}
defer attachConn.Close()
// Create a cancelable context for the handlers
handlerCtx, cancel := context.WithCancel(ctx)
defer cancel() // Ensure handlers are canceled when we return
// Create a channel to wait for output completion
outputDone := make(chan error, 1)
// Start stdin handler if stdin is attached
if dockerExecOpts.AttachStdin {
go func() {
err := s.handleServerExecInput(handlerCtx, stream, attachConn, execResp.ID, dockerExecOpts.Tty)
if err != nil {
slog.Warn("Error in exec input handler", "err", err, "exec_id", execResp.ID)
}
}()
} else {
// If not attaching stdin, close the write side immediately
attachConn.CloseWrite()
}
// Start output handler
// We only wait for this goroutine to complete - it signals when the exec process finishes
go func() {
outputDone <- s.handleServerExecOutput(stream, attachConn, execResp.ID, dockerExecOpts.Tty)
}()
// Wait for the output goroutine to complete (it signals when done)
// We only wait for output, not for stdin goroutine.
if err := <-outputDone; err != nil {
slog.Warn("Error in exec output handler", "err", err, "exec_id", execResp.ID)
}
// The stdin goroutine may still be blocked in stream.Recv() waiting for client data,
// so cancel it explicitly.
cancel()
inspectResp, err := s.client.ContainerExecInspect(ctx, execResp.ID)
if err != nil {
slog.Error("Failed to inspect exec after completion", "err", err, "exec_id", execResp.ID)
return status.Errorf(codes.Internal, "inspect exec: %v", err)
}
// Send the exit code
slog.Debug("Sending exec exit code", "exec_id", execResp.ID, "exit_code", inspectResp.ExitCode)
if err := stream.Send(&pb.ExecContainerResponse{
Payload: &pb.ExecContainerResponse_ExitCode{ExitCode: int32(inspectResp.ExitCode)},
}); err != nil {
slog.Error("Failed to send exec exit code", "err", err, "exec_id", execResp.ID)
return status.Errorf(codes.Internal, "send exit code: %v", err)
}
return nil
}
+1
View File
@@ -28,6 +28,7 @@ type ContainerClient interface {
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) 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 { 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 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
}
+243
View File
@@ -0,0 +1,243 @@
package e2e
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/psviderski/uncloud/internal/ucind"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// deployTestService is a helper function to deploy a simple alpine service for testing exec
func deployTestService(t *testing.T, ctx context.Context, cli *client.Client, name string, replicas uint) {
t.Helper()
spec := api.ServiceSpec{
Name: name,
Replicas: replicas,
Container: api.ContainerSpec{
Image: "alpine:3.20",
Command: []string{"sleep", "3600"},
},
}
deployment := cli.NewDeployment(spec, nil)
err := deployment.Validate(ctx)
require.NoError(t, err)
_, err = deployment.Run(ctx)
require.NoError(t, err)
// Wait for all replicas to be running
require.Eventually(t, func() bool {
service, err := cli.InspectService(ctx, name)
if err != nil {
return false
}
if uint(len(service.Containers)) != replicas {
return false
}
for _, ctr := range service.Containers {
if ctr.Container.State.Status != "running" {
return false
}
}
return true
}, 30*time.Second, 1*time.Second, fmt.Sprintf("service %s should have %d running replicas", name, replicas))
}
// TestExecBasicCommands tests basic command execution, errors, and stderr handling
func TestExecBasicCommands(t *testing.T) {
t.Parallel()
clusterName := "ucind-test.exec-basic"
ctx := context.Background()
c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 1}, true)
cli, err := c.Machines[0].Connect(ctx)
require.NoError(t, err)
// Test: Non-existent service should fail
t.Run("non-existent service", func(t *testing.T) {
execOptions := api.ExecOptions{
Command: []string{"echo", "test"},
AttachStdout: true,
AttachStderr: true,
}
_, err := cli.ExecContainer(ctx, "non-existent-service", "", execOptions)
assert.Error(t, err, "should fail for non-existent service")
assert.Contains(t, strings.ToLower(err.Error()), "inspect service: not found")
})
// Deploy a simple service for all remaining tests
serviceName := "test-exec-service"
deployTestService(t, ctx, cli, serviceName, 1)
// Test: Execute a simple echo command
t.Run("echo command", func(t *testing.T) {
var stdout, stderr bytes.Buffer
execOptions := api.ExecOptions{
Command: []string{"echo", "hello world"},
AttachStdout: true,
AttachStderr: true,
Stdout: &stdout,
Stderr: &stderr,
}
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
require.NoError(t, err)
assert.Equal(t, 0, exitCode, "command should exit with code 0")
assert.Equal(t, "hello world\n", stdout.String(), "should capture stdout")
assert.Empty(t, stderr.String(), "stderr should be empty")
})
// Test: Command with non-zero exit code
t.Run("non-zero exit code", func(t *testing.T) {
execOptions := api.ExecOptions{
Command: []string{"sh", "-c", "exit 42"},
AttachStdout: true,
AttachStderr: true,
}
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
require.NoError(t, err)
assert.Equal(t, 42, exitCode, "should return actual exit code")
})
// Test: Invalid command should fail
t.Run("invalid command", func(t *testing.T) {
var stdout, stderr bytes.Buffer
execOptions := api.ExecOptions{
Command: []string{"nonexistent-command"},
AttachStdout: true,
AttachStderr: true,
Stdout: &stdout,
Stderr: &stderr,
}
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
require.NoError(t, err, "exec should not fail, but command should return non-zero exit")
assert.Equal(t, 126, exitCode, "invalid command should return non-zero exit code")
assert.Contains(t, stdout.String(), "executable file not found")
assert.Equal(t, "", stderr.String(), "stderr should be empty")
})
// Test: Non-existent container ID
t.Run("non-existent container", func(t *testing.T) {
execOptions := api.ExecOptions{
Command: []string{"echo", "test"},
AttachStdout: true,
AttachStderr: true,
}
_, err := cli.ExecContainer(ctx, serviceName, "non-existent-container-id", execOptions)
assert.Error(t, err, "should fail for non-existent container")
})
// Test: Empty command
t.Run("empty command", func(t *testing.T) {
execOptions := api.ExecOptions{
Command: []string{},
AttachStdout: true,
AttachStderr: true,
}
_, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
assert.Error(t, err, "should fail for empty command")
})
// Test: Command with both stdout and stderr
t.Run("mixed stdout/stderr", func(t *testing.T) {
var stdout, stderr bytes.Buffer
execOptions := api.ExecOptions{
Command: []string{"sh", "-c", "echo 'stdout'; echo 'stderr' >&2"},
AttachStdout: true,
AttachStderr: true,
Stdout: &stdout,
Stderr: &stderr,
}
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
require.NoError(t, err)
assert.Equal(t, 0, exitCode)
assert.Equal(t, "stdout\n", stdout.String(), "should capture stdout")
assert.Equal(t, "stderr\n", stderr.String(), "should capture stderr")
})
// Test: Detached command should return immediately
t.Run("detached command", func(t *testing.T) {
var stdout, stderr bytes.Buffer
start := time.Now()
execOptions := api.ExecOptions{
Command: []string{"sh", "-c", "sleep 10; echo hello"}, // Long-running command
Stdout: &stdout,
Stderr: &stderr,
Detach: true,
}
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
elapsed := time.Since(start)
require.NoError(t, err)
assert.Equal(t, 0, exitCode)
// Should return immediately, not wait for command to complete
assert.Less(t, elapsed, 5*time.Second, "detached command should return quickly")
assert.Empty(t, stdout.String(), "stdout should be empty for detached command")
assert.Empty(t, stderr.String(), "stderr should be empty for detached command")
})
// Test: Detached mode with invalid command
t.Run("detached invalid command", func(t *testing.T) {
var stdout, stderr bytes.Buffer
execOptions := api.ExecOptions{
Command: []string{"nonexistent-detached-command"},
Stdout: &stdout,
Stderr: &stderr,
Detach: true,
}
exitCode, err := cli.ExecContainer(ctx, serviceName, "", execOptions)
require.ErrorContains(t, err, "executable file not found")
assert.Equal(t, 1, exitCode)
assert.Empty(t, stdout.String(), "stdout should be empty for detached command")
assert.Empty(t, stderr.String(), "stderr should be empty for detached command")
})
// Deploy a service with multiple replicas
multiServiceName := "multi-replica-service"
deployTestService(t, ctx, cli, multiServiceName, 2)
// Test: Execute command on specific container
t.Run("exec on specific container", func(t *testing.T) {
service, err := cli.InspectService(ctx, multiServiceName)
require.NoError(t, err)
require.GreaterOrEqual(t, len(service.Containers), 2, "should have at least 2 containers")
var stdout, stderr bytes.Buffer
containerID := service.Containers[1].Container.ID
execOptions := api.ExecOptions{
Command: []string{"hostname"},
AttachStdout: true,
AttachStderr: true,
Stdout: &stdout,
Stderr: &stderr,
}
exitCode, err := cli.ExecContainer(ctx, multiServiceName, containerID, execOptions)
require.NoError(t, err)
assert.Equal(t, 0, exitCode)
assert.Greater(t, len(stdout.String()), 4)
assert.Equal(t, service.Containers[1].Container.Name+"\n", stdout.String())
})
}
+1
View File
@@ -26,6 +26,7 @@ type fileInfo struct {
// Uncloud API does not currently expose exec functionality to run commands inside containers. // Uncloud API does not currently expose exec functionality to run commands inside containers.
// Instead this helper function uses "docker cp" inside the ucind container to copy the file from the target container // Instead this helper function uses "docker cp" inside the ucind container to copy the file from the target container
// to a temporary location (also inside the ucind container), and then inspect its content and permissions. // to a temporary location (also inside the ucind container), and then inspect its content and permissions.
// TODO: update when exec functionality is implemented
func readFileInfoInContainer(t *testing.T, machine *ucind.Machine, containerName, filePath string) (fileInfo, error) { func readFileInfoInContainer(t *testing.T, machine *ucind.Machine, containerName, filePath string) (fileInfo, error) {
t.Helper() t.Helper()
ctx := context.Background() ctx := context.Background()
+1
View File
@@ -18,6 +18,7 @@ A CLI tool for managing Uncloud resources such as machines, services, and volume
* [uc ctx](uc_ctx.md) - Switch between different cluster contexts. Contains subcommands to manage contexts. * [uc ctx](uc_ctx.md) - Switch between different cluster contexts. Contains subcommands to manage contexts.
* [uc deploy](uc_deploy.md) - Deploy services from a Compose file. * [uc deploy](uc_deploy.md) - Deploy services from a Compose file.
* [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS. * [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS.
* [uc exec](uc_exec.md) - Execute a command in a running service container
* [uc image](uc_image.md) - Manage images on machines in the cluster. * [uc image](uc_image.md) - Manage images on machines in the cluster.
* [uc images](uc_images.md) - List images on machines in the cluster. * [uc images](uc_images.md) - List images on machines in the cluster.
* [uc inspect](uc_inspect.md) - Display detailed information on a service. * [uc inspect](uc_inspect.md) - Display detailed information on a service.
+56
View File
@@ -0,0 +1,56 @@
# uc exec
Execute a command in a running service container
## Synopsis
Execute a command (interactive shell by default) in a running container within a service.
If the service has multiple replicas, the command will be executed in a random container.
```
uc exec [OPTIONS] SERVICE [COMMAND ARGS...] [flags]
```
## Examples
```
# Start an interactive shell ("bash" or "sh" will be tried by default)
uc exec web-service
# Start an interactive shell with explicit command
uc exec web-service /bin/zsh
# List files in the specific container of the service
uc exec --container d792ea7347e5 web-service ls -la
# Pipe input to a command inside the service container
cat /var/log/app.log | uc exec -T web-service grep "ERROR"
# Run a task in the background (detached mode)
uc exec -d web-service /scripts/cleanup.sh
```
## Options
```
--container string ID of the container to exec into (default is the random container of the service)
-c, --context string Name of the cluster context. (default is the current context)
-d, --detach Detached mode: run command in the background
-h, --help help for exec
-T, --no-tty Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.
```
## Options inherited from parent commands
```
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
Format: [ssh://]user@host[:port] or tcp://host:port
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
```
## See also
* [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
@@ -19,6 +19,7 @@ Manage services in an Uncloud cluster.
## See also ## See also
* [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes. * [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
* [uc service exec](uc_service_exec.md) - Execute a command in a running service container
* [uc service inspect](uc_service_inspect.md) - Display detailed information on a service. * [uc service inspect](uc_service_inspect.md) - Display detailed information on a service.
* [uc service ls](uc_service_ls.md) - List services. * [uc service ls](uc_service_ls.md) - List services.
* [uc service rm](uc_service_rm.md) - Remove one or more services. * [uc service rm](uc_service_rm.md) - Remove one or more services.
@@ -0,0 +1,56 @@
# uc service exec
Execute a command in a running service container
## Synopsis
Execute a command (interactive shell by default) in a running container within a service.
If the service has multiple replicas, the command will be executed in a random container.
```
uc service exec [OPTIONS] SERVICE [COMMAND ARGS...] [flags]
```
## Examples
```
# Start an interactive shell ("bash" or "sh" will be tried by default)
uc exec web-service
# Start an interactive shell with explicit command
uc exec web-service /bin/zsh
# List files in the specific container of the service
uc exec --container d792ea7347e5 web-service ls -la
# Pipe input to a command inside the service container
cat /var/log/app.log | uc exec -T web-service grep "ERROR"
# Run a task in the background (detached mode)
uc exec -d web-service /scripts/cleanup.sh
```
## Options
```
--container string ID of the container to exec into (default is the random container of the service)
-c, --context string Name of the cluster context. (default is the current context)
-d, --detach Detached mode: run command in the background
-h, --help help for exec
-T, --no-tty Disable pseudo-TTY allocation. By default 'uc exec' allocates a TTY when connected to a terminal.
```
## Options inherited from parent commands
```
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
Format: [ssh://]user@host[:port] or tcp://host:port
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
```
## See also
* [uc service](uc_service.md) - Manage services in an Uncloud cluster.