diff --git a/Makefile b/Makefile index 34045b1345..f3911ba9c3 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor) +force: . + help: @echo "Various utilities for managing the terragrunt repository" @@ -50,4 +52,9 @@ install-mockery: generate-mocks: go generate ./... +plugins: force + protoc --go_out=. --go_opt=paths=source_relative plugins/plugins.proto + protoc --go-grpc_out=. --go-grpc_opt=paths=source_relative plugins/plugins.proto + + .PHONY: help fmtcheck fmt install-fmt-hook clean install-lint run-lint diff --git a/go.mod b/go.mod index 126fda075c..d3817b319d 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,7 @@ require ( github.com/gruntwork-io/go-commons v0.17.1 github.com/gruntwork-io/gruntwork-cli v0.7.0 github.com/hashicorp/go-getter/v2 v2.2.1 + github.com/hashicorp/go-plugin v1.4.10 github.com/labstack/echo/v4 v4.11.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 @@ -86,6 +87,8 @@ require ( golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/term v0.21.0 golang.org/x/text v0.16.0 + google.golang.org/grpc v1.61.0 + google.golang.org/protobuf v1.33.0 gopkg.in/ini.v1 v1.67.0 ) @@ -168,7 +171,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/go-plugin v1.4.10 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect @@ -239,8 +241,6 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect - google.golang.org/grpc v1.61.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.2.0 // indirect diff --git a/plugins/plugins.pb.go b/plugins/plugins.pb.go new file mode 100644 index 0000000000..cfaf490b7d --- /dev/null +++ b/plugins/plugins.pb.go @@ -0,0 +1,447 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.12.4 +// source: plugins/plugins.proto + +package plugins + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type InitRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkingDir string `protobuf:"bytes,1,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` + Metadata map[string]string `protobuf:"bytes,2,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *InitRequest) Reset() { + *x = InitRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_plugins_plugins_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitRequest) ProtoMessage() {} + +func (x *InitRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugins_plugins_proto_msgTypes[0] + 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 InitRequest.ProtoReflect.Descriptor instead. +func (*InitRequest) Descriptor() ([]byte, []int) { + return file_plugins_plugins_proto_rawDescGZIP(), []int{0} +} + +func (x *InitRequest) GetWorkingDir() string { + if x != nil { + return x.WorkingDir + } + return "" +} + +func (x *InitRequest) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type InitResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Stdout string `protobuf:"bytes,1,opt,name=stdout,proto3" json:"stdout,omitempty"` + Stderr string `protobuf:"bytes,2,opt,name=stderr,proto3" json:"stderr,omitempty"` + ResultCode int32 `protobuf:"varint,3,opt,name=result_code,json=resultCode,proto3" json:"result_code,omitempty"` +} + +func (x *InitResponse) Reset() { + *x = InitResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_plugins_plugins_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InitResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitResponse) ProtoMessage() {} + +func (x *InitResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugins_plugins_proto_msgTypes[1] + 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 InitResponse.ProtoReflect.Descriptor instead. +func (*InitResponse) Descriptor() ([]byte, []int) { + return file_plugins_plugins_proto_rawDescGZIP(), []int{1} +} + +func (x *InitResponse) GetStdout() string { + if x != nil { + return x.Stdout + } + return "" +} + +func (x *InitResponse) GetStderr() string { + if x != nil { + return x.Stderr + } + return "" +} + +func (x *InitResponse) GetResultCode() int32 { + if x != nil { + return x.ResultCode + } + return 0 +} + +type RunRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkingDir string `protobuf:"bytes,1,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` + Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` + Args []string `protobuf:"bytes,3,rep,name=args,proto3" json:"args,omitempty"` + AllocatePseudoTty bool `protobuf:"varint,4,opt,name=allocate_pseudo_tty,json=allocatePseudoTty,proto3" json:"allocate_pseudo_tty,omitempty"` + EnvVars map[string]string `protobuf:"bytes,5,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *RunRequest) Reset() { + *x = RunRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_plugins_plugins_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RunRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunRequest) ProtoMessage() {} + +func (x *RunRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugins_plugins_proto_msgTypes[2] + 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 RunRequest.ProtoReflect.Descriptor instead. +func (*RunRequest) Descriptor() ([]byte, []int) { + return file_plugins_plugins_proto_rawDescGZIP(), []int{2} +} + +func (x *RunRequest) GetWorkingDir() string { + if x != nil { + return x.WorkingDir + } + return "" +} + +func (x *RunRequest) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +func (x *RunRequest) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +func (x *RunRequest) GetAllocatePseudoTty() bool { + if x != nil { + return x.AllocatePseudoTty + } + return false +} + +func (x *RunRequest) GetEnvVars() map[string]string { + if x != nil { + return x.EnvVars + } + return nil +} + +type RunResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Stdout string `protobuf:"bytes,1,opt,name=stdout,proto3" json:"stdout,omitempty"` + Stderr string `protobuf:"bytes,2,opt,name=stderr,proto3" json:"stderr,omitempty"` + ResultCode int32 `protobuf:"varint,3,opt,name=result_code,json=resultCode,proto3" json:"result_code,omitempty"` +} + +func (x *RunResponse) Reset() { + *x = RunResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_plugins_plugins_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RunResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunResponse) ProtoMessage() {} + +func (x *RunResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugins_plugins_proto_msgTypes[3] + 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 RunResponse.ProtoReflect.Descriptor instead. +func (*RunResponse) Descriptor() ([]byte, []int) { + return file_plugins_plugins_proto_rawDescGZIP(), []int{3} +} + +func (x *RunResponse) GetStdout() string { + if x != nil { + return x.Stdout + } + return "" +} + +func (x *RunResponse) GetStderr() string { + if x != nil { + return x.Stderr + } + return "" +} + +func (x *RunResponse) GetResultCode() int32 { + if x != nil { + return x.ResultCode + } + return 0 +} + +var File_plugins_plugins_proto protoreflect.FileDescriptor + +var file_plugins_plugins_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, + 0x22, 0xab, 0x01, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x69, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, + 0x72, 0x12, 0x3e, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2e, 0x49, 0x6e, + 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5f, + 0x0a, 0x0c, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x64, 0x65, 0x72, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x64, 0x65, 0x72, 0x72, 0x12, 0x1f, + 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x22, + 0x84, 0x02, 0x0a, 0x0a, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, + 0x0a, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x69, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x72, 0x12, + 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x12, 0x2e, 0x0a, + 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x73, 0x65, 0x75, 0x64, 0x6f, + 0x5f, 0x74, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, + 0x63, 0x61, 0x74, 0x65, 0x50, 0x73, 0x65, 0x75, 0x64, 0x6f, 0x54, 0x74, 0x79, 0x12, 0x3b, 0x0a, + 0x08, 0x65, 0x6e, 0x76, 0x5f, 0x76, 0x61, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x20, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2e, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x07, 0x65, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x45, 0x6e, + 0x76, 0x56, 0x61, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5e, 0x0a, 0x0b, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x64, 0x65, 0x72, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x74, 0x64, 0x65, 0x72, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x32, 0x7c, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x12, 0x35, 0x0a, 0x04, 0x49, 0x6e, 0x69, + 0x74, 0x12, 0x14, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2e, 0x49, 0x6e, 0x69, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x73, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, + 0x12, 0x32, 0x0a, 0x03, 0x52, 0x75, 0x6e, 0x12, 0x13, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x73, 0x2e, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2e, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x30, 0x01, 0x42, 0x0a, 0x5a, 0x08, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_plugins_plugins_proto_rawDescOnce sync.Once + file_plugins_plugins_proto_rawDescData = file_plugins_plugins_proto_rawDesc +) + +func file_plugins_plugins_proto_rawDescGZIP() []byte { + file_plugins_plugins_proto_rawDescOnce.Do(func() { + file_plugins_plugins_proto_rawDescData = protoimpl.X.CompressGZIP(file_plugins_plugins_proto_rawDescData) + }) + return file_plugins_plugins_proto_rawDescData +} + +var file_plugins_plugins_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_plugins_plugins_proto_goTypes = []interface{}{ + (*InitRequest)(nil), // 0: plugins.InitRequest + (*InitResponse)(nil), // 1: plugins.InitResponse + (*RunRequest)(nil), // 2: plugins.RunRequest + (*RunResponse)(nil), // 3: plugins.RunResponse + nil, // 4: plugins.InitRequest.MetadataEntry + nil, // 5: plugins.RunRequest.EnvVarsEntry +} +var file_plugins_plugins_proto_depIdxs = []int32{ + 4, // 0: plugins.InitRequest.metadata:type_name -> plugins.InitRequest.MetadataEntry + 5, // 1: plugins.RunRequest.env_vars:type_name -> plugins.RunRequest.EnvVarsEntry + 0, // 2: plugins.CommandExecutor.Init:input_type -> plugins.InitRequest + 2, // 3: plugins.CommandExecutor.Run:input_type -> plugins.RunRequest + 1, // 4: plugins.CommandExecutor.Init:output_type -> plugins.InitResponse + 3, // 5: plugins.CommandExecutor.Run:output_type -> plugins.RunResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_plugins_plugins_proto_init() } +func file_plugins_plugins_proto_init() { + if File_plugins_plugins_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_plugins_plugins_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InitRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_plugins_plugins_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InitResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_plugins_plugins_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RunRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_plugins_plugins_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RunResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_plugins_plugins_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_plugins_plugins_proto_goTypes, + DependencyIndexes: file_plugins_plugins_proto_depIdxs, + MessageInfos: file_plugins_plugins_proto_msgTypes, + }.Build() + File_plugins_plugins_proto = out.File + file_plugins_plugins_proto_rawDesc = nil + file_plugins_plugins_proto_goTypes = nil + file_plugins_plugins_proto_depIdxs = nil +} diff --git a/plugins/plugins.proto b/plugins/plugins.proto new file mode 100644 index 0000000000..3b2bff7598 --- /dev/null +++ b/plugins/plugins.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package plugins; +option go_package = "/plugins"; + +service CommandExecutor { + rpc Init(InitRequest) returns (stream InitResponse); + rpc Run(RunRequest) returns (stream RunResponse); +} + +message InitRequest { + string working_dir = 1; + map metadata = 2; +} + +message InitResponse { + string stdout = 1; + string stderr = 2; + int32 result_code = 3; +} + +message RunRequest { + string working_dir = 1; + string command = 2; + repeated string args = 3; + bool allocate_pseudo_tty = 4; + map env_vars = 5; +} + +message RunResponse { + string stdout = 1; + string stderr = 2; + int32 result_code = 3; +} diff --git a/plugins/plugins_grpc.pb.go b/plugins/plugins_grpc.pb.go new file mode 100644 index 0000000000..8bc29685c9 --- /dev/null +++ b/plugins/plugins_grpc.pb.go @@ -0,0 +1,195 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.12.4 +// source: plugins/plugins.proto + +package plugins + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// CommandExecutorClient is the client API for CommandExecutor service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type CommandExecutorClient interface { + Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (CommandExecutor_InitClient, error) + Run(ctx context.Context, in *RunRequest, opts ...grpc.CallOption) (CommandExecutor_RunClient, error) +} + +type commandExecutorClient struct { + cc grpc.ClientConnInterface +} + +func NewCommandExecutorClient(cc grpc.ClientConnInterface) CommandExecutorClient { + return &commandExecutorClient{cc} +} + +func (c *commandExecutorClient) Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (CommandExecutor_InitClient, error) { + stream, err := c.cc.NewStream(ctx, &CommandExecutor_ServiceDesc.Streams[0], "/plugins.CommandExecutor/Init", opts...) + if err != nil { + return nil, err + } + x := &commandExecutorInitClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type CommandExecutor_InitClient interface { + Recv() (*InitResponse, error) + grpc.ClientStream +} + +type commandExecutorInitClient struct { + grpc.ClientStream +} + +func (x *commandExecutorInitClient) Recv() (*InitResponse, error) { + m := new(InitResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *commandExecutorClient) Run(ctx context.Context, in *RunRequest, opts ...grpc.CallOption) (CommandExecutor_RunClient, error) { + stream, err := c.cc.NewStream(ctx, &CommandExecutor_ServiceDesc.Streams[1], "/plugins.CommandExecutor/Run", opts...) + if err != nil { + return nil, err + } + x := &commandExecutorRunClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type CommandExecutor_RunClient interface { + Recv() (*RunResponse, error) + grpc.ClientStream +} + +type commandExecutorRunClient struct { + grpc.ClientStream +} + +func (x *commandExecutorRunClient) Recv() (*RunResponse, error) { + m := new(RunResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// CommandExecutorServer is the server API for CommandExecutor service. +// All implementations must embed UnimplementedCommandExecutorServer +// for forward compatibility +type CommandExecutorServer interface { + Init(*InitRequest, CommandExecutor_InitServer) error + Run(*RunRequest, CommandExecutor_RunServer) error + mustEmbedUnimplementedCommandExecutorServer() +} + +// UnimplementedCommandExecutorServer must be embedded to have forward compatible implementations. +type UnimplementedCommandExecutorServer struct { +} + +func (UnimplementedCommandExecutorServer) Init(*InitRequest, CommandExecutor_InitServer) error { + return status.Errorf(codes.Unimplemented, "method Init not implemented") +} +func (UnimplementedCommandExecutorServer) Run(*RunRequest, CommandExecutor_RunServer) error { + return status.Errorf(codes.Unimplemented, "method Run not implemented") +} +func (UnimplementedCommandExecutorServer) mustEmbedUnimplementedCommandExecutorServer() {} + +// UnsafeCommandExecutorServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CommandExecutorServer will +// result in compilation errors. +type UnsafeCommandExecutorServer interface { + mustEmbedUnimplementedCommandExecutorServer() +} + +func RegisterCommandExecutorServer(s grpc.ServiceRegistrar, srv CommandExecutorServer) { + s.RegisterService(&CommandExecutor_ServiceDesc, srv) +} + +func _CommandExecutor_Init_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(InitRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(CommandExecutorServer).Init(m, &commandExecutorInitServer{stream}) +} + +type CommandExecutor_InitServer interface { + Send(*InitResponse) error + grpc.ServerStream +} + +type commandExecutorInitServer struct { + grpc.ServerStream +} + +func (x *commandExecutorInitServer) Send(m *InitResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _CommandExecutor_Run_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(RunRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(CommandExecutorServer).Run(m, &commandExecutorRunServer{stream}) +} + +type CommandExecutor_RunServer interface { + Send(*RunResponse) error + grpc.ServerStream +} + +type commandExecutorRunServer struct { + grpc.ServerStream +} + +func (x *commandExecutorRunServer) Send(m *RunResponse) error { + return x.ServerStream.SendMsg(m) +} + +// CommandExecutor_ServiceDesc is the grpc.ServiceDesc for CommandExecutor service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var CommandExecutor_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "plugins.CommandExecutor", + HandlerType: (*CommandExecutorServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Init", + Handler: _CommandExecutor_Init_Handler, + ServerStreams: true, + }, + { + StreamName: "Run", + Handler: _CommandExecutor_Run_Handler, + ServerStreams: true, + }, + }, + Metadata: "plugins/plugins.proto", +} diff --git a/plugins/terraform/terraform.go b/plugins/terraform/terraform.go new file mode 100644 index 0000000000..2bcce8a254 --- /dev/null +++ b/plugins/terraform/terraform.go @@ -0,0 +1,185 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "github.com/creack/pty" + "github.com/gruntwork-io/terragrunt/pkg/log" + pb "github.com/gruntwork-io/terragrunt/plugins" + "github.com/hashicorp/go-plugin" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" + "google.golang.org/grpc" + "io" + "os" + "os/exec" + "sync" +) + +type TerraformCommandExecutor struct { + pb.UnimplementedCommandExecutorServer +} + +func (c *TerraformCommandExecutor) Init(req *pb.InitRequest, stream pb.CommandExecutor_InitServer) error { + log.Infof("Init Terraform plugin") + err := stream.Send(&pb.InitResponse{Stdout: "Terraform Initialization started", Stderr: "", ResultCode: 0}) + if err != nil { + return err + } + + // Stream some metadata as stdout for demonstration + for key, value := range req.Metadata { + err := stream.Send(&pb.InitResponse{Stdout: fmt.Sprintf("Terraform Metadata: %s = %s", key, value), Stderr: "", ResultCode: 0}) + if err != nil { + return err + } + } + + err = stream.Send(&pb.InitResponse{Stdout: "Terraform Initialization completed", Stderr: "", ResultCode: 0}) + if err != nil { + return err + } + return nil +} + +func (c *TerraformCommandExecutor) Run(req *pb.RunRequest, stream pb.CommandExecutor_RunServer) error { + cmd := exec.Command(req.Command, req.Args...) + cmd.Dir = req.WorkingDir + + env := make([]string, 0, len(req.EnvVars)) + for key, value := range req.EnvVars { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + cmd.Env = append(cmd.Env, env...) + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return err + } + stderrPipe, err := cmd.StderrPipe() + if err != nil { + return err + } + + if req.AllocatePseudoTty { + ptmx, err := pty.Start(cmd) + if err != nil { + log.Errorf("Error allocating pseudo-TTY: %v", err) + return err + } + defer func() { _ = ptmx.Close() }() + + go func() { + _, _ = io.Copy(ptmx, os.Stdin) + }() + go func() { + _, _ = io.Copy(os.Stdout, ptmx) + }() + go func() { + _, _ = io.Copy(os.Stderr, ptmx) + }() + } else { + cmd.Stdin = os.Stdin + } + + if err := cmd.Start(); err != nil { + return err + } + + var wg sync.WaitGroup + + // 2 streams to send stdout and stderr + wg.Add(2) + + // Stream stdout + go func() { + defer wg.Done() + reader := transform.NewReader(stdoutPipe, unicode.UTF8.NewDecoder()) + bufReader := bufio.NewReader(reader) + for { + char, _, err := bufReader.ReadRune() + if err != nil { + if err != io.EOF { + log.Errorf("Error reading stdout: %v", err) + } + break + } + err = stream.Send(&pb.RunResponse{ + Stdout: string(char), + }) + if err != nil { + log.Errorf("Error sending stdout: %v", err) + return + } + } + }() + + // Stream stderr + go func() { + defer wg.Done() + reader := transform.NewReader(stderrPipe, unicode.UTF8.NewDecoder()) + bufReader := bufio.NewReader(reader) + for { + char, _, err := bufReader.ReadRune() + if err != nil { + if err != io.EOF { + log.Errorf("Error reading stderr: %v", err) + } + break + } + err = stream.Send(&pb.RunResponse{ + Stderr: string(char), + }) + if err != nil { + log.Errorf("Error sending stderr: %v", err) + return + } + } + }() + + wg.Wait() + err = cmd.Wait() + resultCode := 0 + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + resultCode = exitError.ExitCode() + } else { + resultCode = 1 + } + } + + err = stream.Send(&pb.RunResponse{ + ResultCode: int32(resultCode), + }) + if err != nil { + return err + } + + return nil +} + +// GRPCServer is used to register the TerraformCommandExecutor with the gRPC server +func (c *TerraformCommandExecutor) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + pb.RegisterCommandExecutorServer(s, c) + return nil +} + +// GRPCClient is used to create a client that connects to the TerraformCommandExecutor +func (c *TerraformCommandExecutor) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, client *grpc.ClientConn) (interface{}, error) { + return pb.NewCommandExecutorClient(client), nil +} + +func main() { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "plugin", + MagicCookieValue: "terragrunt", + }, + Plugins: map[string]plugin.Plugin{ + "terraform": &pb.TerragruntGRPCPlugin{Impl: &TerraformCommandExecutor{}}, + }, + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/plugins/tofu/tofu.go b/plugins/tofu/tofu.go new file mode 100644 index 0000000000..d3884cb7b2 --- /dev/null +++ b/plugins/tofu/tofu.go @@ -0,0 +1,185 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "github.com/creack/pty" + "github.com/gruntwork-io/terragrunt/pkg/log" + pb "github.com/gruntwork-io/terragrunt/plugins" + "github.com/hashicorp/go-plugin" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" + "google.golang.org/grpc" + "io" + "os" + "os/exec" + "sync" +) + +type TofuCommandExecutor struct { + pb.UnimplementedCommandExecutorServer +} + +func (c *TofuCommandExecutor) Init(req *pb.InitRequest, stream pb.CommandExecutor_InitServer) error { + log.Infof("Init Tofu plugin") + err := stream.Send(&pb.InitResponse{Stdout: "Tofu Initialization started", Stderr: "", ResultCode: 0}) + if err != nil { + return err + } + + // Stream some metadata as stdout for demonstration + for key, value := range req.Metadata { + err := stream.Send(&pb.InitResponse{Stdout: fmt.Sprintf("Tofu Metadata: %s = %s", key, value), Stderr: "", ResultCode: 0}) + if err != nil { + return err + } + } + + err = stream.Send(&pb.InitResponse{Stdout: "Tofu Initialization completed", Stderr: "", ResultCode: 0}) + if err != nil { + return err + } + return nil +} + +func (c *TofuCommandExecutor) Run(req *pb.RunRequest, stream pb.CommandExecutor_RunServer) error { + cmd := exec.Command(req.Command, req.Args...) + cmd.Dir = req.WorkingDir + + env := make([]string, 0, len(req.EnvVars)) + for key, value := range req.EnvVars { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + cmd.Env = append(cmd.Env, env...) + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return err + } + stderrPipe, err := cmd.StderrPipe() + if err != nil { + return err + } + + if req.AllocatePseudoTty { + ptmx, err := pty.Start(cmd) + if err != nil { + log.Errorf("Error allocating pseudo-TTY: %v", err) + return err + } + defer func() { _ = ptmx.Close() }() + + go func() { + _, _ = io.Copy(ptmx, os.Stdin) + }() + go func() { + _, _ = io.Copy(os.Stdout, ptmx) + }() + go func() { + _, _ = io.Copy(os.Stderr, ptmx) + }() + } else { + cmd.Stdin = os.Stdin + } + + if err := cmd.Start(); err != nil { + return err + } + + var wg sync.WaitGroup + + // 2 streams to send stdout and stderr + wg.Add(2) + + // Stream stdout + go func() { + defer wg.Done() + reader := transform.NewReader(stdoutPipe, unicode.UTF8.NewDecoder()) + bufReader := bufio.NewReader(reader) + for { + char, _, err := bufReader.ReadRune() + if err != nil { + if err != io.EOF { + log.Errorf("Error reading stdout: %v", err) + } + break + } + err = stream.Send(&pb.RunResponse{ + Stdout: string(char), + }) + if err != nil { + log.Errorf("Error sending stdout: %v", err) + return + } + } + }() + + // Stream stderr + go func() { + defer wg.Done() + reader := transform.NewReader(stderrPipe, unicode.UTF8.NewDecoder()) + bufReader := bufio.NewReader(reader) + for { + char, _, err := bufReader.ReadRune() + if err != nil { + if err != io.EOF { + log.Errorf("Error reading stderr: %v", err) + } + break + } + err = stream.Send(&pb.RunResponse{ + Stderr: string(char), + }) + if err != nil { + log.Errorf("Error sending stderr: %v", err) + return + } + } + }() + + wg.Wait() + err = cmd.Wait() + resultCode := 0 + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + resultCode = exitError.ExitCode() + } else { + resultCode = 1 + } + } + + err = stream.Send(&pb.RunResponse{ + ResultCode: int32(resultCode), + }) + if err != nil { + return err + } + + return nil +} + +// GRPCServer is used to register the TofuCommandExecutor with the gRPC server +func (c *TofuCommandExecutor) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + pb.RegisterCommandExecutorServer(s, c) + return nil +} + +// GRPCClient is used to create a client that connects to the TofuCommandExecutor +func (c *TofuCommandExecutor) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, client *grpc.ClientConn) (interface{}, error) { + return pb.NewCommandExecutorClient(client), nil +} + +func main() { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "plugin", + MagicCookieValue: "terragrunt", + }, + Plugins: map[string]plugin.Plugin{ + "tofu": &pb.TerragruntGRPCPlugin{Impl: &TofuCommandExecutor{}}, + }, + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/plugins/types.go b/plugins/types.go new file mode 100644 index 0000000000..1920ef2dd1 --- /dev/null +++ b/plugins/types.go @@ -0,0 +1,21 @@ +package plugins + +import ( + "context" + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" +) + +type TerragruntGRPCPlugin struct { + plugin.Plugin + Impl CommandExecutorServer +} + +func (p *TerragruntGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + RegisterCommandExecutorServer(s, p.Impl) + return nil +} + +func (p *TerragruntGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return NewCommandExecutorClient(c), nil +} diff --git a/shell/run_shell_cmd.go b/shell/run_shell_cmd.go index fcedd9850e..4770587fc5 100644 --- a/shell/run_shell_cmd.go +++ b/shell/run_shell_cmd.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "fmt" + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" "io" "net/url" "os" @@ -13,6 +15,7 @@ import ( "syscall" "time" + "github.com/gruntwork-io/terragrunt/plugins" "github.com/gruntwork-io/terragrunt/telemetry" "github.com/hashicorp/go-version" @@ -72,6 +75,55 @@ func RunTerraformCommandWithOutput(ctx context.Context, terragruntOptions *optio return RunShellCommandWithOutput(ctx, terragruntOptions, "", false, needPTY, terragruntOptions.TerraformPath, args...) } +// plugin name - plugin path, inline +var pluginMap = map[string]string{ + "terraform": "/projects/gruntwork/terragrunt/plugins/terraform/terraform", + "tofu": "/projects/gruntwork/terragrunt/plugins/tofu/tofu", +} + +var pluginInstances = map[string]plugins.CommandExecutorClient{} + +func load() { + if len(pluginInstances) > 0 { + return + } + // iterate over plugins and save pluginInstances + for name, path := range pluginMap { + fmt.Printf("Loading plugin %s from %s\n", name, path) + + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "plugin", + MagicCookieValue: "terragrunt", + }, + Plugins: map[string]plugin.Plugin{ + name: &plugins.TerragruntGRPCPlugin{}, + }, + Cmd: exec.Command(path), + GRPCDialOptions: []grpc.DialOption{ + grpc.WithInsecure(), + }, + AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, + }) + + rpcClient, err := client.Client() + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + rawClient, err := rpcClient.Dispense(name) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + terragruntPlugin := rawClient.(plugins.CommandExecutorClient) + pluginInstances[name] = terragruntPlugin + //TODO: run kill on clients + } + +} + // Run the specified shell command with the specified arguments. Connect the command's stdin, stdout, and stderr to // the currently running app. The command can be executed in a custom working directory by using the parameter // `workingDir`. Terragrunt working directory will be assumed if empty string. @@ -84,6 +136,7 @@ func RunShellCommandWithOutput( command string, args ...string, ) (*CmdOutput, error) { + load() if command == terragruntOptions.TerraformPath { if fn := TerraformCommandHookFromContext(ctx); fn != nil { return fn(ctx, terragruntOptions, args) @@ -143,6 +196,63 @@ func RunShellCommandWithOutput( cmdStdout = io.MultiWriter(&stdoutBuf) } + p, exists := pluginInstances[command] + if exists { + _, err := p.Init(childCtx, &plugins.InitRequest{}) + if err != nil { + return errors.WithStackTrace(err) + } + runStream, err := p.Run(childCtx, &plugins.RunRequest{ + Command: command, + Args: args, + AllocatePseudoTty: allocatePseudoTty, + WorkingDir: cmd.Dir, + }) + if err != nil { + return errors.WithStackTrace(err) + } + + var resultCode = 0 + for { + runResp, err := runStream.Recv() + if err != nil { + break + } + if runResp.Stdout != "" { + _, err := cmdStdout.Write([]byte(runResp.Stdout)) + if err != nil { + return err + } + } + if runResp.Stderr != "" { + _, err := cmdStderr.Write([]byte(runResp.Stderr)) + if err != nil { + return err + } + } + resultCode = int(runResp.ResultCode) + } + + terragruntOptions.Logger.Infof("Plugin execution done in %v", cmd.Dir) + + if resultCode != 0 { + err = ProcessExecutionError{ + Err: fmt.Errorf("command failed with exit code %d", resultCode), + StdOut: stdoutBuf.String(), + Stderr: stderrBuf.String(), + WorkingDir: cmd.Dir, + } + return errors.WithStackTrace(err) + } + + cmdOutput := CmdOutput{ + Stdout: stdoutBuf.String(), + Stderr: stderrBuf.String(), + } + output = &cmdOutput + return nil + } + // If we need to allocate a ptty for the command, route through the ptty routine. Otherwise, directly call the // command. if allocatePseudoTty {