diff --git a/cluster/manager.go b/cluster/manager.go index 3f0a572..aa1d043 100644 --- a/cluster/manager.go +++ b/cluster/manager.go @@ -46,6 +46,37 @@ func New() *Manager { } } +// detectInClusterNamespace reads the namespace from the service account file +// that Kubernetes mounts into pods. Returns "default" if the file cannot be read. +func detectInClusterNamespace() string { + return readNamespaceFromFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") +} + +// readNamespaceFromFile reads the namespace from a file, with fallback to "default" +// This function is extracted to enable testing without filesystem dependencies +func readNamespaceFromFile(namespaceFile string) string { + // #nosec G304 - This is a well-known Kubernetes file path that is safe to read + data, err := os.ReadFile(namespaceFile) + if err != nil { + // If we can't read the file, fall back to "default" + slog.Debug("failed to read namespace from service account file, using default", + slog.String("file", namespaceFile), + slog.String("error", err.Error()), + ) + return "default" + } + + namespace := strings.TrimSpace(string(data)) + if namespace == "" { + slog.Debug("namespace file was empty, using default", + slog.String("file", namespaceFile), + ) + return "default" + } + + return namespace +} + // LoadInClusterConfig loads the in-cluster Kubernetes configuration // This is used when kai is running inside a Kubernetes pod func (cm *Manager) LoadInClusterConfig(name string) error { @@ -78,11 +109,14 @@ func (cm *Manager) LoadInClusterConfig(name string) error { return fmt.Errorf("failed to connect to cluster: %w", err) } + // Detect the actual namespace from the service account file + namespace := detectInClusterNamespace() + contextInfo := &kai.ContextInfo{ Name: name, Cluster: "in-cluster", User: "service-account", - Namespace: "default", + Namespace: namespace, ServerURL: config.Host, ConfigPath: "", IsActive: true, @@ -97,6 +131,7 @@ func (cm *Manager) LoadInClusterConfig(name string) error { slog.Info("in-cluster config loaded", slog.String("context", name), slog.String("server", config.Host), + slog.String("namespace", namespace), ) return nil diff --git a/cluster/manager_test.go b/cluster/manager_test.go index f98daeb..25d64db 100644 --- a/cluster/manager_test.go +++ b/cluster/manager_test.go @@ -50,6 +50,7 @@ func TestExtendedClusterManager(t *testing.T) { func TestInClusterConfig(t *testing.T) { t.Run("LoadInClusterConfig", testLoadInClusterConfig) + t.Run("DetectInClusterNamespace", testDetectInClusterNamespace) } func testLoadInClusterConfig(t *testing.T) { @@ -86,6 +87,54 @@ func testLoadInClusterConfig(t *testing.T) { }) } +func testDetectInClusterNamespace(t *testing.T) { + t.Run("FileDoesNotExist", func(t *testing.T) { + // When the file doesn't exist (normal case outside cluster), should return "default" + namespace := readNamespaceFromFile("/nonexistent/path/namespace") + assert.Equal(t, "default", namespace) + }) + + t.Run("FileExistsWithNamespace", func(t *testing.T) { + // Create a temporary file that simulates the service account namespace file + tmpDir := t.TempDir() + namespaceFile := filepath.Join(tmpDir, "namespace") + err := os.WriteFile(namespaceFile, []byte("my-custom-namespace\n"), 0644) + require.NoError(t, err) + + namespace := readNamespaceFromFile(namespaceFile) + assert.Equal(t, "my-custom-namespace", namespace) + }) + + t.Run("FileIsEmpty", func(t *testing.T) { + // Create a temporary empty file + tmpDir := t.TempDir() + namespaceFile := filepath.Join(tmpDir, "namespace") + err := os.WriteFile(namespaceFile, []byte(""), 0644) + require.NoError(t, err) + + namespace := readNamespaceFromFile(namespaceFile) + assert.Equal(t, "default", namespace) + }) + + t.Run("FileWithWhitespace", func(t *testing.T) { + // Create a file with leading/trailing whitespace + tmpDir := t.TempDir() + namespaceFile := filepath.Join(tmpDir, "namespace") + err := os.WriteFile(namespaceFile, []byte(" production \n"), 0644) + require.NoError(t, err) + + namespace := readNamespaceFromFile(namespaceFile) + assert.Equal(t, "production", namespace) + }) + + t.Run("DefaultFunctionBehavior", func(t *testing.T) { + // Test that detectInClusterNamespace calls readNamespaceFromFile with correct path + // Since the actual file won't exist in test environment, it should return "default" + namespace := detectInClusterNamespace() + assert.Equal(t, "default", namespace) + }) +} + func testNewClusterManager(t *testing.T) { cm := New() assert.NotNil(t, cm)