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
+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
}
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_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,
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,
0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xfb,
0x09, 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, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x90,
0x01, 0x0a, 0x14, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78,
0x65, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x12, 0x16, 0x0a, 0x05, 0x73, 0x74, 0x64, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0c, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x64, 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65,
0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x06,
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,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72,
0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 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, 0x42,
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,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 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, 0x74, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53,
0x74, 0x61, 0x72, 0x74, 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, 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,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 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, 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 (
@@ -1986,7 +2341,7 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte {
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{
(*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest
(*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse
@@ -2020,62 +2375,70 @@ var file_internal_machine_api_pb_docker_proto_goTypes = []any{
(*ListServiceContainersRequest)(nil), // 29: api.ListServiceContainersRequest
(*ListServiceContainersResponse)(nil), // 30: api.ListServiceContainersResponse
(*MachineServiceContainers)(nil), // 31: api.MachineServiceContainers
(*Metadata)(nil), // 32: api.Metadata
(*emptypb.Empty)(nil), // 33: google.protobuf.Empty
(*ExecContainerRequest)(nil), // 32: api.ExecContainerRequest
(*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{
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
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
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
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
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
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
0, // 13: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
2, // 14: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
4, // 15: api.Docker.StartContainer:input_type -> api.StartContainerRequest
5, // 16: api.Docker.StopContainer:input_type -> api.StopContainerRequest
6, // 17: api.Docker.ListContainers:input_type -> api.ListContainersRequest
9, // 18: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
10, // 19: api.Docker.PullImage:input_type -> api.PullImageRequest
12, // 20: api.Docker.InspectImage:input_type -> api.InspectImageRequest
15, // 21: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
18, // 22: api.Docker.ListImages:input_type -> api.ListImagesRequest
21, // 23: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
23, // 24: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
26, // 25: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
27, // 26: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
2, // 27: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
29, // 28: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
9, // 29: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
1, // 30: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
3, // 31: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
33, // 32: api.Docker.StartContainer:output_type -> google.protobuf.Empty
33, // 33: api.Docker.StopContainer:output_type -> google.protobuf.Empty
7, // 34: api.Docker.ListContainers:output_type -> api.ListContainersResponse
33, // 35: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
11, // 36: api.Docker.PullImage:output_type -> api.JSONMessage
13, // 37: api.Docker.InspectImage:output_type -> api.InspectImageResponse
16, // 38: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
19, // 39: api.Docker.ListImages:output_type -> api.ListImagesResponse
22, // 40: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
24, // 41: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
33, // 42: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
1, // 43: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
28, // 44: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
30, // 45: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
33, // 46: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
30, // [30:47] is the sub-list for method output_type
13, // [13:30] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
33, // 13: api.ExecContainerRequest.config:type_name -> api.ExecConfig
34, // 14: api.ExecContainerRequest.resize:type_name -> api.ResizeEvent
0, // 15: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
2, // 16: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
4, // 17: api.Docker.StartContainer:input_type -> api.StartContainerRequest
5, // 18: api.Docker.StopContainer:input_type -> api.StopContainerRequest
6, // 19: api.Docker.ListContainers:input_type -> api.ListContainersRequest
9, // 20: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
10, // 21: api.Docker.PullImage:input_type -> api.PullImageRequest
12, // 22: api.Docker.InspectImage:input_type -> api.InspectImageRequest
15, // 23: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
18, // 24: api.Docker.ListImages:input_type -> api.ListImagesRequest
21, // 25: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
23, // 26: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
26, // 27: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
27, // 28: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
2, // 29: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
29, // 30: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
9, // 31: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
32, // 32: api.Docker.ExecContainer:input_type -> api.ExecContainerRequest
1, // 33: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
3, // 34: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
37, // 35: api.Docker.StartContainer:output_type -> google.protobuf.Empty
37, // 36: api.Docker.StopContainer:output_type -> google.protobuf.Empty
7, // 37: api.Docker.ListContainers:output_type -> api.ListContainersResponse
37, // 38: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
11, // 39: api.Docker.PullImage:output_type -> api.JSONMessage
13, // 40: api.Docker.InspectImage:output_type -> api.InspectImageResponse
16, // 41: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
19, // 42: api.Docker.ListImages:output_type -> api.ListImagesResponse
22, // 43: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
24, // 44: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
37, // 45: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
1, // 46: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
28, // 47: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
30, // 48: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
37, // 49: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
35, // 50: api.Docker.ExecContainer:output_type -> api.ExecContainerResponse
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() }
@@ -2469,6 +2832,65 @@ func file_internal_machine_api_pb_docker_proto_init() {
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{}
out := protoimpl.TypeBuilder{
@@ -2476,7 +2898,7 @@ func file_internal_machine_api_pb_docker_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc,
NumEnums: 0,
NumMessages: 32,
NumMessages: 36,
NumExtensions: 0,
NumServices: 1,
},
+38
View File
@@ -30,6 +30,8 @@ service Docker {
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
rpc ExecContainer(stream ExecContainerRequest) returns (stream ExecContainerResponse);
}
message CreateContainerRequest {
@@ -214,3 +216,39 @@ message MachineServiceContainers {
Metadata metadata = 1;
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_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
Docker_ExecContainer_FullMethodName = "/api.Docker/ExecContainer"
)
// 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)
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, 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 {
@@ -251,6 +253,19 @@ func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveCon
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.
// All implementations must embed UnimplementedDockerServer
// for forward compatibility.
@@ -274,6 +289,7 @@ type DockerServer interface {
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
ExecContainer(grpc.BidiStreamingServer[ExecContainerRequest, ExecContainerResponse]) error
mustEmbedUnimplementedDockerServer()
}
@@ -335,6 +351,9 @@ func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListSer
func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) {
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) testEmbeddedByValue() {}
@@ -655,6 +674,13 @@ func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context
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.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -733,6 +759,12 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
Handler: _Docker_PullImage_Handler,
ServerStreams: true,
},
{
StreamName: "ExecContainer",
Handler: _Docker_ExecContainer_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
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"
dockercommand "github.com/docker/cli/cli/command"
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/filters"
"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/volume"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
@@ -1012,3 +1014,250 @@ func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveConta
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
}