package portforward import ( "context" "errors" "fmt" "io" "net" "net/http" "net/url" "strings" "sync" "testing" "time" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/portforward" streamhttp "k8s.io/streaming/pkg/httpstream" "github.com/sophotechlabs/spinoza/internal/auth" ) type errorStream struct { headers http.Header } func (e *errorStream) Read([]byte) (int, error) { return 0, io.EOF } func (e *errorStream) Write(p []byte) (int, error) { return len(p), nil } func (e *errorStream) Close() error { return nil } func (e *errorStream) Reset() error { return nil } func (e *errorStream) Headers() http.Header { return e.headers } func (e *errorStream) Identifier() uint32 { return 1 } type pipeStream struct { net.Conn headers http.Header } func (p *pipeStream) Reset() error { return p.Close() } func (p *pipeStream) Headers() http.Header { return p.headers } func (p *pipeStream) Identifier() uint32 { return 2 } type fakeConnection struct { closed chan bool mu sync.Mutex headers []http.Header } func (c *fakeConnection) CreateStream(headers http.Header) (streamhttp.Stream, error) { c.mu.Lock() c.headers = append(c.headers, headers.Clone()) c.mu.Unlock() if headers.Get(corev1.StreamType) == corev1.StreamTypeError { return &errorStream{headers: headers}, nil } client, server := net.Pipe() go func() { defer func() { _ = server.Close() }() _, _ = io.Copy(server, server) }() return &pipeStream{Conn: client, headers: headers}, nil } func (c *fakeConnection) Close() error { select { case <-c.closed: default: close(c.closed) } return nil } func (c *fakeConnection) CloseChan() <-chan bool { return c.closed } func (c *fakeConnection) SetIdleTimeout(time.Duration) {} func (c *fakeConnection) RemoveStreams(...streamhttp.Stream) {} func (c *fakeConnection) portHeaders() []string { c.mu.Lock() defer c.mu.Unlock() out := make([]string, 0, len(c.headers)) for _, h := range c.headers { out = append(out, h.Get(corev1.PortHeader)) } return out } type fakeDialer struct { conn *fakeConnection err error } func (d *fakeDialer) Dial(protocols ...string) (streamhttp.Connection, string, error) { if d.err != nil { return nil, "", d.err } return d.conn, protocols[0], nil } func runnerWith(dialer streamhttp.Dialer, err error) *streamRunner { return &streamRunner{ dialerFor: func(context.Context, string, string) (streamhttp.Dialer, error) { if err != nil { return nil, err } return dialer, nil }, } } func TestRunForwardsTrafficThroughALocalListener(t *testing.T) { conn := &fakeConnection{closed: make(chan bool)} runner := runnerWith(&fakeDialer{conn: conn}, nil) ready := make(chan int32, 1) stop := make(chan struct{}) done := make(chan error, 1) go func() { done <- runner.Run(context.Background(), "flux-system", "web", 0, 8080, ready, stop) }() var local int32 select { case local = <-ready: case err := <-done: t.Fatalf("run ended before ready: %v", err) case <-time.After(10 * time.Second): t.Fatalf("timed out waiting for the forward to become ready") } if local == 0 { t.Fatalf("local port was not reported") } client, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", local)) if err != nil { t.Fatalf("dial local port: %v", err) } defer func() { _ = client.Close() }() if _, err := client.Write([]byte("ping")); err != nil { t.Fatalf("write: %v", err) } buf := make([]byte, 4) if err := client.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil { t.Fatalf("deadline: %v", err) } if _, err := io.ReadFull(client, buf); err != nil { t.Fatalf("read back: %v", err) } if string(buf) != "ping" { t.Fatalf("echoed %q, want ping", buf) } ports := conn.portHeaders() if len(ports) < 2 { t.Fatalf("expected an error stream and a data stream, got %v", ports) } for _, port := range ports { if port != "8080" { t.Fatalf("stream opened for port %q, want 8080", port) } } close(stop) select { case err := <-done: if err != nil { t.Fatalf("run returned %v after stop", err) } case <-time.After(10 * time.Second): t.Fatalf("run did not return after stop") } } func TestRunStopsWithoutAnyTraffic(t *testing.T) { runner := runnerWith(&fakeDialer{conn: &fakeConnection{closed: make(chan bool)}}, nil) ready := make(chan int32, 1) stop := make(chan struct{}) done := make(chan error, 1) go func() { done <- runner.Run(context.Background(), "flux-system", "web", 0, 9090, ready, stop) }() <-ready close(stop) select { case err := <-done: if err != nil { t.Fatalf("run returned %v", err) } case <-time.After(10 * time.Second): t.Fatalf("run did not return after stop") } } func TestRunEndsWhenTheClusterContextIsCancelled(t *testing.T) { runner := runnerWith(&fakeDialer{conn: &fakeConnection{closed: make(chan bool)}}, nil) ready := make(chan int32, 1) stop := make(chan struct{}) defer close(stop) done := make(chan error, 1) ctx, cancel := context.WithCancel(context.Background()) go func() { done <- runner.Run(ctx, "flux-system", "web", 0, 9090, ready, stop) }() <-ready cancel() select { case err := <-done: if err != nil { t.Fatalf("run returned %v", err) } case <-time.After(10 * time.Second): t.Fatal("a canceled cluster context left the forward tunneling into the old cluster") } } func TestRunAsksForTheLocalPortItWasGiven(t *testing.T) { runner := runnerWith(&fakeDialer{conn: &fakeConnection{closed: make(chan bool)}}, nil) ready := make(chan int32, 1) stop := make(chan struct{}) done := make(chan error, 1) listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } addr, ok := listener.Addr().(*net.TCPAddr) if !ok { t.Fatalf("addr = %T, want a tcp address", listener.Addr()) } wanted := int32(addr.Port) if closeErr := listener.Close(); closeErr != nil { t.Fatalf("close: %v", closeErr) } go func() { done <- runner.Run(context.Background(), "flux-system", "web", wanted, 9090, ready, stop) }() select { case local := <-ready: if local != wanted { t.Fatalf("local = %d, want the port the caller kept: %d", local, wanted) } case err := <-done: t.Fatalf("run ended before ready: %v", err) case <-time.After(10 * time.Second): t.Fatal("timed out waiting for the forward to become ready") } close(stop) <-done } func TestRunSurfacesADialerBuildFailure(t *testing.T) { runner := runnerWith(nil, errors.New("no kubeconfig")) err := runner.Run(context.Background(), "flux-system", "web", 0, 8080, make(chan int32, 1), make(chan struct{})) if err == nil { t.Fatalf("expected the dialer failure to surface") } } func TestRunSurfacesADialFailure(t *testing.T) { runner := runnerWith(&fakeDialer{err: errors.New("upgrade refused")}, nil) stop := make(chan struct{}) defer close(stop) err := runner.Run(context.Background(), "flux-system", "web", 0, 8080, make(chan int32, 1), stop) if err == nil { t.Fatalf("expected the dial failure to surface") } } func TestAnnounceGivesUpWhenTheContextEnds(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) ready := make(chan int32, 1) done := make(chan struct{}) go func() { announce(ctx, nil, make(chan struct{}), make(chan struct{}), ready) close(done) }() cancel() select { case <-done: case <-time.After(5 * time.Second): t.Fatalf("announce did not return when the context ended") } if len(ready) != 0 { t.Fatalf("announce reported a port after the context ended") } } func TestFallbackDialerIsBuiltForAValidConfig(t *testing.T) { endpoint, err := url.Parse("https://example.test/api/v1/namespaces/x/pods/y/portforward") if err != nil { t.Fatalf("parse: %v", err) } dialer, err := fallbackDialer(&restclient.Config{Host: "https://example.test"}, endpoint) if err != nil { t.Fatalf("fallbackDialer: %v", err) } if dialer == nil { t.Fatalf("dialer is nil") } } func TestFallbackDialerRejectsABrokenConfig(t *testing.T) { endpoint, err := url.Parse("https://example.test/portforward") if err != nil { t.Fatalf("parse: %v", err) } config := &restclient.Config{ Host: "https://example.test", CertFile: "/nonexistent/cert.pem", KeyFile: "/nonexistent/key.pem", } _, err = fallbackDialer(config, endpoint) if err == nil { t.Fatalf("expected a transport build failure") } } func TestNewRunnerBuildsADialerFromTheClientset(t *testing.T) { config := &restclient.Config{Host: "https://example.test"} cs, err := kubernetes.NewForConfig(config) if err != nil { t.Fatalf("clientset: %v", err) } runner, ok := NewRunner(cs, config).(*streamRunner) if !ok { t.Fatalf("NewRunner did not return a streamRunner") } dialer, err := runner.dialerFor(t.Context(), "flux-system", "web") if err != nil { t.Fatalf("dialerFor: %v", err) } if dialer == nil { t.Fatalf("dialer is nil") } } func TestAnnounceIgnoresAForwarderThatIsNotReady(t *testing.T) { forwarder, err := portforward.NewForStreaming( &fakeDialer{conn: &fakeConnection{closed: make(chan bool)}}, []string{"0:8080"}, make(chan struct{}), make(chan struct{}), io.Discard, io.Discard, ) if err != nil { t.Fatalf("new forwarder: %v", err) } ready := make(chan int32, 1) closed := make(chan struct{}) close(closed) announce(context.Background(), forwarder, closed, make(chan struct{}), ready) if len(ready) != 0 { t.Fatalf("announce reported a port for a forwarder that never bound one") } } func TestShouldFallbackOnlyForTransportCompatibilityFailures(t *testing.T) { cases := []struct { name string err error want bool }{ {name: "connection failure", err: errors.New("connection refused")}, {name: "upgrade failure", err: &streamhttp.UpgradeFailureError{Cause: errors.New("no")}, want: true}, {name: "https proxy", err: errors.New("proxy: unknown scheme: https"), want: true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := shouldFallback(tc.err); got != tc.want { t.Fatalf("shouldFallback(%v) = %v, want %v", tc.err, got, tc.want) } }) } } func TestAnnounceGivesUpWhenTheForwardEndsBeforeItIsReady(t *testing.T) { ready := make(chan int32, 1) done := make(chan struct{}) close(done) returned := make(chan struct{}) go func() { defer close(returned) announce(context.Background(), nil, make(chan struct{}), done, ready) }() select { case <-returned: case <-time.After(5 * time.Second): t.Fatal("announce parked after the forward failed; every failed start would leak a goroutine") } if len(ready) != 0 { t.Fatalf("announce reported a port for a forward that never started") } } func TestAForwardCarriesTheSignedInUserRatherThanSpinozasOwnAccount(t *testing.T) { config := &restclient.Config{Host: "https://apiserver"} ctx := auth.WithIdentity(t.Context(), auth.Identity{ User: "alice@example.com", Groups: []string{"platform", "sre"}, }) acting := actingAs(ctx, config) if acting == config { t.Fatal("the caller's own config was handed back, so the forward would act as the pod") } if acting.Impersonate.UserName != "alice@example.com" { t.Fatalf("impersonated user = %q, want the signed-in one", acting.Impersonate.UserName) } if strings.Join(acting.Impersonate.Groups, ",") != "platform,sre" { t.Fatalf("impersonated groups = %v, want both", acting.Impersonate.Groups) } if config.Impersonate.UserName != "" { t.Fatal("the shared config was changed, so every later forward would act as alice") } } func TestAForwardWithNobodySignedInGoesAsSpinozaItself(t *testing.T) { config := &restclient.Config{Host: "https://apiserver"} if actingAs(t.Context(), config) != config { t.Fatal("a local forward was given an impersonation it never needed") } if actingAs(auth.AsServer(t.Context()), config) != config { t.Fatal("a forward spinoza started for itself would impersonate nobody") } }