mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
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:
co-authored by
Pasha Sviderski
parent
03ff4cd51d
commit
c95136eae6
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user