package kube import ( "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "k8s.io/client-go/rest" "k8s.io/client-go/transport" "github.com/sophotechlabs/spinoza/internal/api" "github.com/sophotechlabs/spinoza/internal/auth" ) const validKubeconfig = `apiVersion: v1 kind: Config current-context: test-ctx clusters: - name: test-cluster cluster: server: https://127.0.0.1:6443 insecure-skip-tls-verify: true contexts: - name: test-ctx context: cluster: test-cluster namespace: test-ns user: test-user users: - name: test-user user: token: test-token ` func writeKubeconfig(t *testing.T, content string) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "config") if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatalf("write kubeconfig: %v", err) } return path } func TestLoadReturnsBundle(t *testing.T) { path := writeKubeconfig(t, validKubeconfig) t.Setenv("KUBECONFIG", path) bundle, err := LoadContext(api.ContextRef{}, Options{}) if err != nil { t.Fatalf("Load: %v", err) } if bundle.Clientset == nil { t.Fatal("Clientset = nil, want non-nil") } if bundle.Dynamic == nil { t.Fatal("Dynamic = nil, want non-nil") } if bundle.Discovery == nil { t.Fatal("Discovery = nil, want non-nil") } if bundle.Mapper == nil { t.Fatal("Mapper = nil, want non-nil") } if bundle.Ref.Name != "test-ctx" { t.Fatalf("Ref.Name = %q, want test-ctx", bundle.Ref.Name) } if bundle.Namespace != "test-ns" { t.Fatalf("Namespace = %q, want test-ns", bundle.Namespace) } } func TestLoadReturnsErrorForEmptyKubeconfig(t *testing.T) { path := writeKubeconfig(t, "") t.Setenv("KUBECONFIG", path) _, err := LoadContext(api.ContextRef{}, Options{}) if err == nil { t.Fatal("Load returned nil error for empty kubeconfig") } } func TestLoadReturnsErrorForInvalidKubeconfig(t *testing.T) { path := writeKubeconfig(t, "not: [valid: yaml") t.Setenv("KUBECONFIG", path) _, err := LoadContext(api.ContextRef{}, Options{}) if err == nil { t.Fatal("Load returned nil error for invalid kubeconfig") } } const twoContextKubeconfig = `apiVersion: v1 kind: Config current-context: beta clusters: - name: c1 cluster: server: https://127.0.0.1:6443 insecure-skip-tls-verify: true contexts: - name: beta context: cluster: c1 namespace: beta-ns user: u1 - name: alpha context: cluster: c1 namespace: alpha-ns user: u1 users: - name: u1 user: token: t ` const otherKubeconfig = `apiVersion: v1 kind: Config current-context: gamma clusters: - name: c2 cluster: server: https://127.0.0.2:6443 insecure-skip-tls-verify: true contexts: - name: gamma context: cluster: c2 user: u2 users: - name: u2 user: token: t ` func TestReadListsEveryContextSorted(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, twoContextKubeconfig)) contexts, err := Read("") if err != nil { t.Fatalf("Read: %v", err) } if len(contexts) != 2 || contexts[0].Name != "alpha" || contexts[1].Name != "beta" { t.Fatalf("contexts = %v, want them sorted", contexts) } if contexts[0].Cluster != "c1" { t.Fatalf("cluster = %q, want the cluster the context points at", contexts[0].Cluster) } if contexts[0].Namespace != "alpha-ns" { t.Fatalf("namespace = %q", contexts[0].Namespace) } } func TestReadReportsAnUnreadableKubeconfig(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, "not: [valid: yaml")) _, err := Read("") if err == nil { t.Fatal("an unreadable kubeconfig was reported as having no contexts") } } func TestReadNamesAFileThatIsNotThere(t *testing.T) { _, err := Read(filepath.Join(t.TempDir(), "absent")) if err == nil { t.Fatal("a kubeconfig that does not exist read as an empty one") } if !strings.Contains(err.Error(), "absent") { t.Fatalf("error = %v, want the path in it", err) } } func TestReadingOneFileIgnoresTheDefaultKubeconfig(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, twoContextKubeconfig)) other := writeKubeconfig(t, otherKubeconfig) contexts, err := Read(other) if err != nil { t.Fatalf("Read: %v", err) } if len(contexts) != 1 || contexts[0].Name != "gamma" { t.Fatalf("contexts = %v; a named kubeconfig must be read on its own, never merged with the default", contexts) } } func TestLoadContextTargetsTheNamedContext(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, twoContextKubeconfig)) bundle, err := LoadContext(api.ContextRef{Name: "alpha"}, Options{}) if err != nil { t.Fatalf("LoadContext: %v", err) } if bundle.Ref.Name != "alpha" { t.Fatalf("Ref.Name = %q, want the requested one rather than the kubeconfig default", bundle.Ref.Name) } if bundle.Namespace != "alpha-ns" { t.Fatalf("Namespace = %q, want the namespace of the requested context", bundle.Namespace) } } func TestLoadContextReadsTheKubeconfigOnTheRef(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, twoContextKubeconfig)) other := writeKubeconfig(t, otherKubeconfig) bundle, err := LoadContext(api.ContextRef{Kubeconfig: other, Name: "gamma"}, Options{}) if err != nil { t.Fatalf("LoadContext: %v", err) } if bundle.Ref.Kubeconfig != other { t.Fatalf("Ref.Kubeconfig = %q, want the file the context came from", bundle.Ref.Kubeconfig) } if bundle.Config.Host != "https://127.0.0.2:6443" { t.Fatalf("host = %q, want the server of the named kubeconfig", bundle.Config.Host) } } func TestARefWithNoKubeconfigFallsBackToTheOneSpinozaWasStartedWith(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, twoContextKubeconfig)) other := writeKubeconfig(t, otherKubeconfig) bundle, err := LoadContext(api.ContextRef{}, Options{Kubeconfig: other}) if err != nil { t.Fatalf("LoadContext: %v", err) } if bundle.Ref.Name != "gamma" { t.Fatalf("Ref.Name = %q, want the current context of the --kubeconfig file", bundle.Ref.Name) } } func TestTheFileSpinozaWasStartedWithIsNamedOnTheRef(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, twoContextKubeconfig)) other := writeKubeconfig(t, otherKubeconfig) bundle, err := LoadContext(api.ContextRef{}, Options{Kubeconfig: other}) if err != nil { t.Fatalf("LoadContext: %v", err) } if bundle.Ref.Kubeconfig != other { t.Fatalf("Ref.Kubeconfig = %q, want the file spinoza was started with", bundle.Ref.Kubeconfig) } } func TestAFileOnTheRefIsNotReplacedByTheOneSpinozaWasStartedWith(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, validKubeconfig)) started := writeKubeconfig(t, twoContextKubeconfig) other := writeKubeconfig(t, otherKubeconfig) bundle, err := LoadContext( api.ContextRef{Kubeconfig: other, Name: "gamma"}, Options{Kubeconfig: started}, ) if err != nil { t.Fatalf("LoadContext: %v", err) } if bundle.Ref.Kubeconfig != other { t.Fatalf("Ref.Kubeconfig = %q, want the file the context came from", bundle.Ref.Kubeconfig) } } func TestAStartWithNoFileNamedLeavesTheRefWithoutOne(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, twoContextKubeconfig)) bundle, err := LoadContext(api.ContextRef{}, Options{}) if err != nil { t.Fatalf("LoadContext: %v", err) } if bundle.Ref.Kubeconfig != "" { t.Fatalf("Ref.Kubeconfig = %q, want the usual lookup rules left alone", bundle.Ref.Kubeconfig) } } func TestLoadContextRejectsAContextThatIsNotThere(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, twoContextKubeconfig)) _, err := LoadContext(api.ContextRef{Name: "gone"}, Options{}) if err == nil { t.Fatal("switching to a context that does not exist reported success") } } func TestLoadContextWithNoNameKeepsTheDefault(t *testing.T) { t.Setenv("KUBECONFIG", writeKubeconfig(t, twoContextKubeconfig)) bundle, err := LoadContext(api.ContextRef{}, Options{}) if err != nil { t.Fatalf("LoadContext: %v", err) } if bundle.Ref.Name != "beta" { t.Fatalf("Ref.Name = %q, want the kubeconfig's current-context", bundle.Ref.Name) } } func TestLoadContextRaisesTheClientRateLimit(t *testing.T) { path := writeKubeconfig(t, validKubeconfig) t.Setenv("KUBECONFIG", path) bundle, err := LoadContext(api.ContextRef{}, Options{}) if err != nil { t.Fatalf("load: %v", err) } if bundle.Config.QPS < 50 { t.Fatalf("QPS = %v; client-go defaults to 5, which throttles a fan-out of 150 lists to 30s", bundle.Config.QPS) } if bundle.Config.Burst < 100 { t.Fatalf("Burst = %d, want headroom for the catalog fan-out", bundle.Config.Burst) } } func TestLoadContextKeepsConfiguredClientRateLimits(t *testing.T) { path := writeKubeconfig(t, validKubeconfig) bundle, err := LoadContext(api.ContextRef{}, Options{Kubeconfig: path, QPS: 17, Burst: 29}) if err != nil { t.Fatalf("LoadContext: %v", err) } if bundle.Config.QPS != 17 { t.Fatalf("QPS = %v, want 17", bundle.Config.QPS) } if bundle.Config.Burst != 29 { t.Fatalf("Burst = %d, want 29", bundle.Config.Burst) } } func TestLoadContextRecordsTheToolKubeconfigAsTheFallbackSource(t *testing.T) { path := writeKubeconfig(t, validKubeconfig) t.Setenv("KUBECONFIG", path) bundle, err := LoadContext(api.ContextRef{}, Options{ToolKubeconfig: "/runtime/spinoza/in-cluster.kubeconfig"}) if err != nil { t.Fatalf("LoadContext: %v", err) } want := "/runtime/spinoza/in-cluster.kubeconfig" if bundle.Ref.Kubeconfig != want { t.Fatalf("Ref.Kubeconfig = %q, want %q", bundle.Ref.Kubeconfig, want) } } func TestLoadContextWiresImpersonationIntoItsTransport(t *testing.T) { path := writeKubeconfig(t, validKubeconfig) bundle, err := LoadContext(api.ContextRef{}, Options{Kubeconfig: path, Impersonate: true}) if err != nil { t.Fatalf("LoadContext: %v", err) } recorder := &recordingTripper{} wrapped := bundle.Config.WrapTransport(recorder) ctx := auth.WithIdentity(t.Context(), auth.Identity{User: "alice@example.com", Groups: []string{"platform"}}) req := httptest.NewRequest(http.MethodGet, "https://apiserver/api/v1/pods", http.NoBody).WithContext(ctx) if _, err := wrapped.RoundTrip(req); err != nil { t.Fatalf("round trip: %v", err) } if recorder.seen.Header.Get(transport.ImpersonateUserHeader) != "alice@example.com" { t.Fatalf("user header = %q", recorder.seen.Header.Get(transport.ImpersonateUserHeader)) } if got := recorder.seen.Header.Values(transport.ImpersonateGroupHeader); len(got) != 1 || got[0] != "platform" { t.Fatalf("group headers = %v, want platform", got) } } func TestBoundedDiscoveryRejectsAMalformedServer(t *testing.T) { config := &rest.Config{Host: "://not-a-server"} _, err := boundedDiscovery(config) if err == nil { t.Fatal("a malformed discovery endpoint reported success") } if !strings.Contains(err.Error(), "kube discovery") { t.Fatalf("error = %q, want discovery named", err.Error()) } if config.Timeout != 0 { t.Fatalf("input timeout = %v, want the caller's config left unchanged", config.Timeout) } } func TestLabelNamesTheFileTheContextsCameFrom(t *testing.T) { path := writeKubeconfig(t, validKubeconfig) if Label(path) != path { t.Fatalf("Label = %q, want the path itself", Label(path)) } } func TestLabelOfTheDefaultNamesEveryFileItReads(t *testing.T) { first := writeKubeconfig(t, validKubeconfig) second := writeKubeconfig(t, otherKubeconfig) t.Setenv("KUBECONFIG", first+string(os.PathListSeparator)+second) label := Label("") if !strings.Contains(label, first) || !strings.Contains(label, second) { t.Fatalf("label = %q, want both files KUBECONFIG points at", label) } }