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
+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")
})
}