mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
176 lines
5.6 KiB
Go
176 lines
5.6 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/siderolabs/grpc-proxy/proxy"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// Director manages routing of gRPC requests between local and remote backends.
|
|
type Director struct {
|
|
localBackend *LocalBackend
|
|
remotePort uint16
|
|
remoteBackends sync.Map
|
|
localAddress atomic.Value
|
|
mapper MachineMapper
|
|
}
|
|
|
|
func NewDirector(localSockPath string, remotePort uint16, mapper MachineMapper) *Director {
|
|
return &Director{
|
|
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. It is called once during machine startup before the proxy server accepts requests.
|
|
func (d *Director) UpdateLocalAddress(addr string) {
|
|
d.localAddress.Store(addr)
|
|
}
|
|
|
|
// Director implements proxy.StreamDirector for grpc-proxy, routing requests to local or remote backends based
|
|
// on gRPC metadata in the context. Each machine metadata is injected into the response messages by the proxy
|
|
// if the request is proxied to multiple backends.
|
|
func (d *Director) Director(ctx context.Context, fullMethodName string) (proxy.Mode, []proxy.Backend, error) {
|
|
md, ok := metadata.FromIncomingContext(ctx)
|
|
if !ok {
|
|
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
|
}
|
|
// If the request is already proxied, send it to the local backend.
|
|
if _, ok = md["proxy-authority"]; ok {
|
|
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, hasMachines := md["machines"]
|
|
machine, hasMachine := md["machine"]
|
|
if !hasMachines && !hasMachine {
|
|
return proxy.One2One, []proxy.Backend{d.localBackend}, nil
|
|
}
|
|
|
|
// 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.getBackend(targets[0].Addr)
|
|
if err != nil {
|
|
return proxy.One2One, nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
// For One2One, we don't wrap in MetadataBackend as we don't inject metadata.
|
|
return proxy.One2One, []proxy.Backend{backend}, 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)
|
|
if ok {
|
|
return b.(*RemoteBackend), nil
|
|
}
|
|
|
|
backend, err := NewRemoteBackend(addr, d.remotePort)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
existing, loaded := d.remoteBackends.LoadOrStore(addr, backend)
|
|
if loaded {
|
|
// A concurrent remoteBackend call built a different backend.
|
|
backend.Close()
|
|
return existing.(*RemoteBackend), nil
|
|
}
|
|
|
|
return backend, nil
|
|
}
|
|
|
|
// FlushRemoteBackends closes all remote backend connections and removes them from the cache.
|
|
func (d *Director) FlushRemoteBackends() {
|
|
d.remoteBackends.Range(func(key, value any) bool {
|
|
backend, ok := value.(*RemoteBackend)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
backend.Close()
|
|
d.remoteBackends.Delete(key)
|
|
return true
|
|
})
|
|
}
|
|
|
|
// Close closes all backend connections.
|
|
func (d *Director) Close() {
|
|
d.localBackend.Close()
|
|
d.FlushRemoteBackends()
|
|
}
|