Ctrl K
ComunidadGuías…

internal/portforward/runner_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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:   "[email protected]",
		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 != "[email protected]" {
		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")
	}
}