BREAKING CHANGE: resolve gRPC API proxy targets (machines) on server instead of client, needs upgrade to v0.20 (#247)

Co-authored-by: Pasha Sviderski <me@psviderski.name>
This commit is contained in:
Justin Bradford
2026-05-19 15:22:50 +10:00
committed by GitHub
co-authored by Pasha Sviderski
parent 03ff4cd51d
commit c95136eae6
29 changed files with 914 additions and 741 deletions
+2 -2
View File
@@ -31,8 +31,8 @@ const (
//
// The two minimums are independent: a client might require a newer daemon for new
// features, while that same daemon could still handle requests from older clients.
MinClientVersion = "0.0.0"
MinServerVersion = "0.0.0"
MinClientVersion = "0.20.0"
MinServerVersion = "0.20.0"
ReleaseURL = "https://github.com/psviderski/uncloud/releases/latest"
)
+73 -50
View File
@@ -81,8 +81,12 @@ type Metadata struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// ID of the machine the response came from.
MachineId string `protobuf:"bytes,4,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"`
// Name of the machine the response came from.
MachineName string `protobuf:"bytes,5,opt,name=machine_name,json=machineName,proto3" json:"machine_name,omitempty"`
// Address of the machine the response came from.
Machine string `protobuf:"bytes,1,opt,name=machine,proto3" json:"machine,omitempty"`
MachineAddr string `protobuf:"bytes,1,opt,name=machine_addr,json=machineAddr,proto3" json:"machine_addr,omitempty"`
// error is set if the request to upstream failed. The rest of the response is undefined.
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
// error as a gRPC Status message.
@@ -121,9 +125,23 @@ func (*Metadata) Descriptor() ([]byte, []int) {
return file_internal_machine_api_pb_common_proto_rawDescGZIP(), []int{0}
}
func (x *Metadata) GetMachine() string {
func (x *Metadata) GetMachineId() string {
if x != nil {
return x.Machine
return x.MachineId
}
return ""
}
func (x *Metadata) GetMachineName() string {
if x != nil {
return x.MachineName
}
return ""
}
func (x *Metadata) GetMachineAddr() string {
if x != nil {
return x.MachineAddr
}
return ""
}
@@ -549,53 +567,58 @@ var file_internal_machine_api_pb_common_proto_rawDesc = []byte{
0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x66, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x61, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65,
0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
0x72, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a,
0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x61, 0x22, 0x37, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x26, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50,
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70,
0x22, 0x35, 0x0a, 0x06, 0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52,
0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65,
0x66, 0x69, 0x78, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04,
0x62, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73,
0x22, 0x75, 0x0a, 0x0b, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18,
0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73,
0x69, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63,
0x65, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x22, 0xd2, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45,
0x6e, 0x74, 0x72, 0x79, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e,
0x74, 0x72, 0x79, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06,
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x53, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e,
0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x4f, 0x55, 0x54, 0x10,
0x01, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a,
0x09, 0x48, 0x45, 0x41, 0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x03, 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,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb1, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49,
0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f,
0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x63, 0x68,
0x69, 0x6e, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2a, 0x0a,
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75,
0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x32, 0x0a, 0x05, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x37, 0x0a,
0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x0a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x08, 0x6d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x14, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x0e, 0x0a, 0x02,
0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x22, 0x35, 0x0a, 0x06,
0x49, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12,
0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70,
0x6f, 0x72, 0x74, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x50, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12,
0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x69, 0x74, 0x73,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x22, 0x75, 0x0a, 0x0b,
0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66,
0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x6c,
0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28,
0x05, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x14, 0x0a,
0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x6e,
0x74, 0x69, 0x6c, 0x22, 0xd2, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79,
0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e,
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18,
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x44, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a,
0x06, 0x53, 0x54, 0x44, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x48, 0x45, 0x41,
0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x03, 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 (
+6 -1
View File
@@ -11,8 +11,13 @@ import "google/protobuf/timestamp.proto";
// Common metadata message nested in all reply message types, injected by the gRPC proxy to provide information
// about the machine that responded to the request.
message Metadata {
// ID of the machine the response came from.
string machine_id = 4;
// Name of the machine the response came from.
string machine_name = 5;
// Address of the machine the response came from.
string machine = 1;
string machine_addr = 1;
// error is set if the request to upstream failed. The rest of the response is undefined.
string error = 2;
// error as a gRPC Status message.
+17 -10
View File
@@ -4,15 +4,18 @@ import (
"fmt"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
)
// One2ManyResponder converts upstream responses into messages from upstreams, so that multiple
// successful and failure responses might be returned in One2Many mode.
type One2ManyResponder struct {
machine string
// MetadataBackend wraps a proxy.Backend and injects machine metadata into responses in One2Many mode.
type MetadataBackend struct {
proxy.Backend
MachineID string
MachineName string
MachineAddr string
}
// AppendInfo is called to enhance response from the backend with additional data.
@@ -59,10 +62,12 @@ type One2ManyResponder struct {
// cuts field header, rest is representation of some reply. Marshal 'Empty' as protobuf,
// which builds 'common.Metadata' field, append it to original response message, build new header
// for new length of some response, and add back new field header.
func (b *One2ManyResponder) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
func (b *MetadataBackend) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
payload, err := proto.Marshal(&pb.Empty{
Metadata: &pb.Metadata{
Machine: b.machine,
MachineAddr: b.MachineAddr,
MachineId: b.MachineID,
MachineName: b.MachineName,
},
})
@@ -124,12 +129,14 @@ func (b *One2ManyResponder) AppendInfo(streaming bool, resp []byte) ([]byte, err
//
// Streaming responses are not wrapped into Empty, so we simply marshall EmptyResponse
// message.
func (b *One2ManyResponder) BuildError(streaming bool, err error) ([]byte, error) {
func (b *MetadataBackend) BuildError(streaming bool, err error) ([]byte, error) {
var resp proto.Message = &pb.Empty{
Metadata: &pb.Metadata{
Machine: b.machine,
Error: err.Error(),
Status: status.Convert(err).Proto(),
MachineAddr: b.MachineAddr,
MachineId: b.MachineID,
MachineName: b.MachineName,
Error: err.Error(),
Status: status.Convert(err).Proto(),
},
}
+80 -31
View File
@@ -2,7 +2,10 @@ package proxy
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"github.com/siderolabs/grpc-proxy/proxy"
"google.golang.org/grpc/codes"
@@ -15,27 +18,22 @@ type Director struct {
localBackend *LocalBackend
remotePort uint16
remoteBackends sync.Map
// mu synchronizes access to localAddress.
mu sync.RWMutex
localAddress string
localAddress atomic.Value
mapper MachineMapper
}
func NewDirector(localSockPath string, remotePort uint16) *Director {
func NewDirector(localSockPath string, remotePort uint16, mapper MachineMapper) *Director {
return &Director{
localBackend: NewLocalBackend(localSockPath, ""),
localBackend: NewLocalBackend(localSockPath),
remotePort: remotePort,
mapper: mapper,
}
}
// UpdateLocalAddress updates the local machine address used to identify which requests should be proxied
// to the local gRPC server.
// to the local gRPC server. It is called once during machine startup before the proxy server accepts requests.
func (d *Director) UpdateLocalAddress(addr string) {
d.mu.Lock()
defer d.mu.Unlock()
d.localAddress = addr
// Replace the local backend with the one that has local address set.
d.localBackend = NewLocalBackend(d.localBackend.sockPath, addr)
d.localAddress.Store(addr)
}
// Director implements proxy.StreamDirector for grpc-proxy, routing requests to local or remote backends based
@@ -51,39 +49,90 @@ func (d *Director) Director(ctx context.Context, fullMethodName string) (proxy.M
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
}
// If the request metadata doesn't contain machines to proxy to, send it to the local backend.
machines, ok := md["machines"]
if !ok {
machines, hasMachines := md["machines"]
machine, hasMachine := md["machine"]
if !hasMachines && !hasMachine {
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
}
if len(machines) == 0 {
return proxy.One2One, nil, status.Error(codes.InvalidArgument, "no machines specified")
}
d.mu.RLock()
localAddress := d.localAddress
localBackend := d.localBackend
d.mu.RUnlock()
backends := make([]proxy.Backend, len(machines))
for i, addr := range machines {
if addr == localAddress {
backends[i] = localBackend
continue
// Handle singular "machine" case (One2One, no metadata injection)
if hasMachine {
if len(machine) != 1 {
return proxy.One2One, nil, status.Error(codes.InvalidArgument,
"proxy metadata 'machine' must have exactly one value")
}
if hasMachines {
return proxy.One2One, nil, status.Error(codes.InvalidArgument,
"both 'machine' and 'machines' proxy metadata are set")
}
targets, err := d.mapper.MapMachines(ctx, machine)
if err != nil {
return proxy.One2One, nil, mapErrorToStatus(err)
}
backend, err := d.remoteBackend(addr)
backend, err := d.getBackend(targets[0].Addr)
if err != nil {
return proxy.One2One, nil, status.Error(codes.Internal, err.Error())
}
backends[i] = backend
// For One2One, we don't wrap in MetadataBackend as we don't inject metadata.
return proxy.One2One, []proxy.Backend{backend}, nil
}
if len(backends) == 1 {
return proxy.One2One, backends, nil
// Handle plural "machines" case (One2Many, always metadata injection)
if len(machines) == 0 {
return proxy.One2One, nil, status.Error(codes.InvalidArgument, "proxy metadata 'machines' is empty")
}
targets, err := d.mapper.MapMachines(ctx, machines)
if err != nil {
return proxy.One2One, nil, mapErrorToStatus(err)
}
backends := make([]proxy.Backend, len(targets))
for i, t := range targets {
backend, err := d.getBackend(t.Addr)
if err != nil {
return proxy.One2One, nil, status.Error(codes.Internal, err.Error())
}
// Wrap with metadata injector
backends[i] = &MetadataBackend{
Backend: backend,
MachineID: t.ID,
MachineName: t.Name,
MachineAddr: t.Addr,
}
}
// TODO: should we periodically close and delete outdated remote backends (the ones left after removing machines)?
// IIRC the proxy will try to reconnect to them indefinitely. This can be stopped by restarting the daemon.
// But we can clean them up, e.g. when a client requests 'machines: *' so we know all the current targets
// or run a background goroutine that periodically lists them and closes old remoteBackends.
return proxy.One2Many, backends, nil
}
// mapErrorToStatus converts mapper errors to appropriate gRPC status errors.
func mapErrorToStatus(err error) error {
if notFound, ok := errors.AsType[*MachinesNotFoundError](err); ok {
return status.Error(codes.InvalidArgument, notFound.Error())
}
// Check if already a gRPC status error.
if _, ok := status.FromError(err); ok {
return err
}
return status.Error(codes.Internal, fmt.Sprintf("failed to resolve machines: %v", err))
}
// getBackend returns a backend for the given address, utilizing local backend if matching local address.
func (d *Director) getBackend(addr string) (proxy.Backend, error) {
if localAddr, _ := d.localAddress.Load().(string); localAddr != "" && addr == localAddr {
return d.localBackend, nil
}
return d.remoteBackend(addr)
}
// remoteBackend returns a RemoteBackend for the given address from the cache or creates a new one.
func (d *Director) remoteBackend(addr string) (*RemoteBackend, error) {
b, ok := d.remoteBackends.Load(addr)
+255
View File
@@ -0,0 +1,255 @@
package proxy
import (
"context"
"errors"
"testing"
"github.com/siderolabs/grpc-proxy/proxy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type mockMapper struct {
targets []MachineTarget
err error
}
func (m *mockMapper) MapMachines(_ context.Context, _ []string) ([]MachineTarget, error) {
if m.err != nil {
return nil, m.err
}
return m.targets, nil
}
func TestDirector_Director(t *testing.T) {
d := NewDirector("/tmp/test.sock", 8080, nil)
t.Cleanup(d.Close)
// Use a valid IPv6 address for remote targets.
remoteTarget := MachineTarget{ID: "id-2", Name: "machine-b", Addr: "fd00::2"}
localTarget := MachineTarget{ID: "id-1", Name: "machine-a", Addr: "fd00::1"}
t.Run("no metadata routes to local", func(t *testing.T) {
ctx := context.Background()
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2One, mode)
assert.Len(t, backends, 1)
assert.IsType(t, (*LocalBackend)(nil), backends[0])
})
t.Run("proxy-authority routes to local", func(t *testing.T) {
md := metadata.Pairs("proxy-authority", "test", "machines", remoteTarget.Name)
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2One, mode)
assert.Len(t, backends, 1)
assert.IsType(t, (*LocalBackend)(nil), backends[0])
})
t.Run("machine singular local", func(t *testing.T) {
d.localAddress.Store(localTarget.Addr)
d.mapper = &mockMapper{targets: []MachineTarget{localTarget}}
md := metadata.New(map[string]string{"machine": localTarget.Name})
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2One, mode)
assert.Len(t, backends, 1)
assert.IsType(t, (*LocalBackend)(nil), backends[0])
})
t.Run("machine singular remote", func(t *testing.T) {
d.localAddress.Store(localTarget.Addr)
d.mapper = &mockMapper{targets: []MachineTarget{remoteTarget}}
md := metadata.New(map[string]string{"machine": remoteTarget.Name})
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2One, mode)
assert.Len(t, backends, 1)
assert.IsType(t, (*RemoteBackend)(nil), backends[0])
assert.Equal(t, "[fd00::2]:8080", backends[0].(*RemoteBackend).target)
})
t.Run("machine not found", func(t *testing.T) {
d.mapper = &mockMapper{err: &MachinesNotFoundError{NotFound: []string{"missing"}}}
md := metadata.New(map[string]string{"machine": "missing"})
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "machine not found: missing")
})
t.Run("machines plural single local", func(t *testing.T) {
d.localAddress.Store(localTarget.Addr)
d.mapper = &mockMapper{targets: []MachineTarget{localTarget}}
md := metadata.Pairs("machines", localTarget.Name)
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2Many, mode)
assert.Len(t, backends, 1)
mb := backends[0].(*MetadataBackend)
assert.Equal(t, localTarget.ID, mb.MachineID)
assert.Equal(t, localTarget.Name, mb.MachineName)
assert.Equal(t, localTarget.Addr, mb.MachineAddr)
assert.IsType(t, (*LocalBackend)(nil), mb.Backend)
})
t.Run("machines plural multiple", func(t *testing.T) {
d.localAddress.Store(localTarget.Addr)
d.mapper = &mockMapper{targets: []MachineTarget{localTarget, remoteTarget}}
md := metadata.Pairs("machines", localTarget.Name, "machines", remoteTarget.Name)
ctx := metadata.NewIncomingContext(context.Background(), md)
mode, backends, err := d.Director(ctx, "/Test/Method")
require.NoError(t, err)
assert.Equal(t, proxy.One2Many, mode)
assert.Len(t, backends, 2)
// First backend should be local.
mb0 := backends[0].(*MetadataBackend)
assert.Equal(t, localTarget.ID, mb0.MachineID)
assert.IsType(t, (*LocalBackend)(nil), mb0.Backend)
// Second backend should be remote.
mb1 := backends[1].(*MetadataBackend)
assert.Equal(t, remoteTarget.ID, mb1.MachineID)
assert.IsType(t, (*RemoteBackend)(nil), mb1.Backend)
assert.Equal(t, "[fd00::2]:8080", mb1.Backend.(*RemoteBackend).target)
})
t.Run("machines empty string", func(t *testing.T) {
d.mapper = &mockMapper{err: &MachinesNotFoundError{NotFound: []string{""}}}
md := metadata.Pairs("machines", "")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "machine not found")
})
t.Run("machine empty slice", func(t *testing.T) {
md := metadata.MD{"machine": []string{}}
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "proxy metadata 'machine' must have exactly one value")
})
t.Run("machines empty slice", func(t *testing.T) {
md := metadata.MD{"machines": []string{}}
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "proxy metadata 'machines' is empty")
})
t.Run("both machine and machines set", func(t *testing.T) {
md := metadata.Pairs("machine", "m1", "machines", "m1", "machines", "m2")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
assert.Contains(t, st.Message(), "both 'machine' and 'machines' proxy metadata are set")
})
t.Run("machines not found", func(t *testing.T) {
d.mapper = &mockMapper{err: &MachinesNotFoundError{NotFound: []string{"missing"}}}
md := metadata.Pairs("machines", "missing")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
})
t.Run("machines mapper generic error", func(t *testing.T) {
d.mapper = &mockMapper{err: errors.New("boom")}
md := metadata.Pairs("machines", "any")
ctx := metadata.NewIncomingContext(context.Background(), md)
_, _, err := d.Director(ctx, "/Test/Method")
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.Internal, st.Code())
})
}
func TestMapErrorToStatus(t *testing.T) {
t.Run("machines not found", func(t *testing.T) {
err := mapErrorToStatus(&MachinesNotFoundError{NotFound: []string{"a", "b"}})
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, st.Code())
})
t.Run("already grpc status", func(t *testing.T) {
original := status.Error(codes.DeadlineExceeded, "timeout")
err := mapErrorToStatus(original)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.DeadlineExceeded, st.Code())
})
t.Run("generic error", func(t *testing.T) {
err := mapErrorToStatus(errors.New("something broke"))
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.Internal, st.Code())
assert.Contains(t, st.Message(), "something broke")
})
}
+14 -10
View File
@@ -10,9 +10,8 @@ import (
"google.golang.org/grpc/metadata"
)
// LocalBackend is a proxy.One2ManyResponder implementation that proxies to a local gRPC server listening on a Unix socket.
// LocalBackend is a proxy.Backend implementation that proxies to a local gRPC server listening on a Unix socket.
type LocalBackend struct {
One2ManyResponder
sockPath string
mu sync.RWMutex
@@ -21,20 +20,15 @@ type LocalBackend struct {
var _ proxy.Backend = (*LocalBackend)(nil)
// NewLocalBackend returns a new LocalBackend for the given Unix socket path. The addr parameter is the local address
// of the current machine which could be empty if it's not known. The address is used to populate response metadata
// in one2many mode.
func NewLocalBackend(sockPath, addr string) *LocalBackend {
// NewLocalBackend returns a new LocalBackend for the given Unix socket path.
func NewLocalBackend(sockPath string) *LocalBackend {
return &LocalBackend{
One2ManyResponder: One2ManyResponder{
machine: addr,
},
sockPath: sockPath,
}
}
func (b *LocalBackend) String() string {
return b.machine
return "unix://" + b.sockPath
}
// GetConnection returns a gRPC connection to the local server listening on the Unix socket.
@@ -64,6 +58,16 @@ func (b *LocalBackend) GetConnection(ctx context.Context, _ string) (context.Con
return outCtx, b.conn, err
}
// AppendInfo is a no-op for LocalBackend as it does not inject metadata.
func (b *LocalBackend) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
return resp, nil
}
// BuildError is a no-op for LocalBackend.
func (b *LocalBackend) BuildError(streaming bool, err error) ([]byte, error) {
return nil, err
}
// Close closes the upstream gRPC connection.
func (b *LocalBackend) Close() {
b.mu.Lock()
+108
View File
@@ -0,0 +1,108 @@
package proxy
import (
"context"
"fmt"
"slices"
"strings"
"github.com/psviderski/uncloud/internal/machine/api/pb"
)
// MachineTarget represents a resolved machine target.
type MachineTarget struct {
ID, Name, Addr string
}
// MachinesNotFoundError indicates that one or more requested machines were not found.
type MachinesNotFoundError struct {
NotFound []string
}
func (e *MachinesNotFoundError) Error() string {
if len(e.NotFound) == 1 {
return fmt.Sprintf("machine not found: %s", e.NotFound[0])
}
return fmt.Sprintf("machines not found: %s", strings.Join(e.NotFound, ", "))
}
// MachineMapper provides access to machine information in the cluster.
type MachineMapper interface {
// MapMachines resolves a list of machine names/IDs (or "*") to a list of machine targets.
// Returns MachinesNotFoundError if any requested machine is not found (except when "*" is used).
MapMachines(ctx context.Context, namesOrIDs []string) ([]MachineTarget, error)
}
// Store is the interface required by MachineMapper to access the cluster store.
type Store interface {
ListMachines(ctx context.Context) ([]*pb.MachineInfo, error)
}
// CorrosionMapper implements MachineMapper using the corrosion store.
type CorrosionMapper struct {
store Store
}
func NewCorrosionMapper(store Store) *CorrosionMapper {
return &CorrosionMapper{store: store}
}
func (m *CorrosionMapper) MapMachines(ctx context.Context, namesOrIDs []string) ([]MachineTarget, error) {
if len(namesOrIDs) == 0 {
return nil, fmt.Errorf("no machines specified")
}
machines, err := m.store.ListMachines(ctx)
if err != nil {
return nil, fmt.Errorf("list machines: %w", err)
}
allTargets := make([]MachineTarget, 0, len(machines))
for _, machine := range machines {
ip, err := machine.Network.ManagementIp.ToAddr()
if err != nil {
return nil, fmt.Errorf("invalid management IP for machine '%s' in store: %w", machine.Name, err)
}
allTargets = append(allTargets, MachineTarget{
ID: machine.Id,
Name: machine.Name,
Addr: ip.String(),
})
}
if slices.Contains(namesOrIDs, "*") {
if len(allTargets) == 0 {
return nil, fmt.Errorf("no machines in cluster")
}
return allTargets, nil
}
// Build a map for lookup (keyed by both ID and name)
targetByLookup := make(map[string]MachineTarget, len(allTargets)*2)
for _, t := range allTargets {
targetByLookup[t.ID] = t
targetByLookup[t.Name] = t
}
// Resolve each requested machine.
targets := make([]MachineTarget, 0, len(namesOrIDs))
var notFound []string
seenTarget := make(map[string]struct{}, len(namesOrIDs))
for _, nameOrID := range namesOrIDs {
if t, ok := targetByLookup[nameOrID]; ok {
if _, seen := seenTarget[t.ID]; !seen {
targets = append(targets, t)
seenTarget[t.ID] = struct{}{}
}
} else {
notFound = append(notFound, nameOrID)
}
}
if len(notFound) > 0 {
return nil, &MachinesNotFoundError{NotFound: notFound}
}
return targets, nil
}
+168
View File
@@ -0,0 +1,168 @@
package proxy
import (
"context"
"errors"
"net/netip"
"testing"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockStore struct {
machines []*pb.MachineInfo
err error
}
func (s *mockStore) ListMachines(_ context.Context) ([]*pb.MachineInfo, error) {
if s.err != nil {
return nil, s.err
}
return s.machines, nil
}
func machineInfo(id, name, ip string) *pb.MachineInfo {
return &pb.MachineInfo{
Id: id,
Name: name,
Network: &pb.NetworkConfig{
ManagementIp: pb.NewIP(netip.MustParseAddr(ip)),
},
}
}
func TestCorrosionMapper_MapMachines(t *testing.T) {
ctx := context.Background()
machines := []*pb.MachineInfo{
machineInfo("id-1", "machine-a", "fd00::1"),
machineInfo("id-2", "machine-b", "fd00::2"),
}
tests := []struct {
name string
store *mockStore
input []string
want []MachineTarget
wantErr bool
errMsg string
}{
{
name: "wildcard returns all machines",
store: &mockStore{machines: machines},
input: []string{"*"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
{ID: "id-2", Name: "machine-b", Addr: "fd00::2"},
},
},
{
name: "single name",
store: &mockStore{machines: machines},
input: []string{"machine-a"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
},
},
{
name: "single id",
store: &mockStore{machines: machines},
input: []string{"id-2"},
want: []MachineTarget{
{ID: "id-2", Name: "machine-b", Addr: "fd00::2"},
},
},
{
name: "multiple mixed",
store: &mockStore{machines: machines},
input: []string{"machine-a", "id-2"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
{ID: "id-2", Name: "machine-b", Addr: "fd00::2"},
},
},
{
name: "deduplicates repeated inputs",
store: &mockStore{machines: machines},
input: []string{"machine-a", "machine-a"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
},
},
{
name: "deduplicates name and id for same machine",
store: &mockStore{machines: machines},
input: []string{"machine-a", "id-1"},
want: []MachineTarget{
{ID: "id-1", Name: "machine-a", Addr: "fd00::1"},
},
},
{
name: "not found single",
store: &mockStore{machines: machines},
input: []string{"missing"},
wantErr: true,
errMsg: "machine not found: missing",
},
{
name: "not found multiple",
store: &mockStore{machines: machines},
input: []string{"missing", "also-missing"},
wantErr: true,
errMsg: "machines not found: missing, also-missing",
},
{
name: "partial not found",
store: &mockStore{machines: machines},
input: []string{"machine-a", "missing"},
wantErr: true,
errMsg: "machine not found: missing",
},
{
name: "wildcard with no machines",
store: &mockStore{machines: []*pb.MachineInfo{}},
input: []string{"*"},
wantErr: true,
errMsg: "no machines in cluster",
},
{
name: "store error",
store: &mockStore{err: errors.New("store down")},
input: []string{"*"},
wantErr: true,
errMsg: "list machines: store down",
},
{
name: "invalid management ip",
store: &mockStore{machines: []*pb.MachineInfo{{Id: "bad", Name: "bad-ip", Network: &pb.NetworkConfig{ManagementIp: &pb.IP{}}}}},
input: []string{"*"},
wantErr: true,
errMsg: "invalid management IP for machine 'bad-ip' in store",
},
{
name: "empty input returns error",
store: &mockStore{machines: machines},
input: []string{},
wantErr: true,
errMsg: "no machines specified",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mapper := NewCorrosionMapper(tt.store)
got, err := mapper.MapMachines(ctx, tt.input)
if tt.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errMsg)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
+13 -7
View File
@@ -14,13 +14,11 @@ import (
"google.golang.org/grpc/metadata"
)
// RemoteBackend is a proxy.One2ManyResponder implementation that proxies to a remote gRPC server, injecting machine metadata
// into the response.
// RemoteBackend is a proxy.Backend implementation that proxies to a remote gRPC server.
//
// Based on the Talos apid implementation:
// https://github.com/siderolabs/talos/blob/59a78da42cdea8fbccc35d0851f9b0eef928261b/internal/app/apid/pkg/backend/apid.go
type RemoteBackend struct {
One2ManyResponder
target string
mu sync.RWMutex
@@ -37,15 +35,12 @@ func NewRemoteBackend(addr string, port uint16) (*RemoteBackend, error) {
}
return &RemoteBackend{
One2ManyResponder: One2ManyResponder{
machine: addr,
},
target: netip.AddrPortFrom(ip, port).String(),
}, nil
}
func (b *RemoteBackend) String() string {
return b.machine
return b.target
}
// GetConnection returns a gRPC connection to the remote server.
@@ -58,6 +53,7 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
}
delete(md, ":authority")
delete(md, "machines")
delete(md, "machine")
outCtx := metadata.NewOutgoingContext(ctx, md)
@@ -100,6 +96,16 @@ func (b *RemoteBackend) GetConnection(ctx context.Context, _ string) (context.Co
return outCtx, b.conn, err
}
// AppendInfo is a no-op for RemoteBackend as it does not inject metadata.
func (b *RemoteBackend) AppendInfo(streaming bool, resp []byte) ([]byte, error) {
return resp, nil
}
// BuildError is a no-op for RemoteBackend.
func (b *RemoteBackend) BuildError(streaming bool, err error) ([]byte, error) {
return nil, err
}
// Close closes the upstream gRPC connection.
func (b *RemoteBackend) Close() {
b.mu.Lock()
+2 -1
View File
@@ -250,7 +250,8 @@ func NewMachine(config *Config) (*Machine, error) {
dockerService := machinedocker.NewService(config.DockerClient, db)
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, constants.MachineAPIPort)
mapper := apiproxy.NewCorrosionMapper(corroStore)
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, constants.MachineAPIPort, mapper)
localProxyServer := grpc.NewServer(
grpc.ForceServerCodecV2(proxy.Codec()),
grpc.UnaryInterceptor(grpcversion.ServerUnaryInterceptor),