diff --git a/packages/driver-kube-pod/src/driver/client/index.ts b/packages/driver-kube-pod/src/driver/client/index.ts index 4077bc33..19da5dc0 100644 --- a/packages/driver-kube-pod/src/driver/client/index.ts +++ b/packages/driver-kube-pod/src/driver/client/index.ts @@ -38,14 +38,67 @@ const stringify = stringifyModule.default export const loadKubeConfig = ({ kubeconfig, context }: { kubeconfig?: string; context?: string }) => { const kc = new k8s.KubeConfig() - if (kubeconfig) { - kc.loadFromFile(kubeconfig) - } else { - kc.loadFromDefault() + + try { + if (kubeconfig) { + kc.loadFromFile(kubeconfig) + } else { + kc.loadFromDefault() + } + } catch (error: any) { + if (error.message?.includes('ENOENT') || error.message?.includes('no such file')) { + throw new Error( + 'Kubernetes configuration file not found. ' + + (kubeconfig + ? `The specified kubeconfig file "${kubeconfig}" does not exist. ` + : 'No kubeconfig found in default locations (~/.kube/config, $KUBECONFIG). ' + ) + + 'Please ensure your Kubernetes configuration is properly set up.\n\n' + + 'This is a Kubernetes configuration issue, not a tunnel server problem.' + ) + } + + throw new Error( + `Failed to load Kubernetes configuration: ${error.message}\n\n` + + 'Please check your kubeconfig file for syntax errors or corruption. ' + + 'This is a Kubernetes configuration issue, not a tunnel server problem.' + ) } + if (context) { - kc.setCurrentContext(context) + try { + kc.setCurrentContext(context) + } catch (error: any) { + throw new Error( + `Kubernetes context "${context}" not found in configuration. ` + + 'Please check that the specified context exists in your kubeconfig file.\n\n' + + 'This is a Kubernetes configuration issue, not a tunnel server problem.' + ) + } } + + // Validate that we have a current context + try { + const currentContext = kc.getCurrentContext() + if (!currentContext) { + throw new Error( + 'No current Kubernetes context is set. ' + + 'Please set a default context in your kubeconfig file or specify one with the --context flag.\n\n' + + 'This is a Kubernetes configuration issue, not a tunnel server problem.' + ) + } + } catch (error: any) { + if (error.message?.includes('No current context')) { + throw error // Re-throw our own error as-is + } + + throw new Error( + `Failed to get current Kubernetes context: ${error.message}\n\n` + + 'Please check your kubeconfig file. ' + + 'This is a Kubernetes configuration issue, not a tunnel server problem.' + ) + } + return kc } @@ -315,3 +368,4 @@ export type CreationClient = ReturnType export { extractInstance, extractEnvId, extractName, extractNamespace, extractTemplateHash } from './metadata.js' export { DeploymentNotReadyError, DeploymentNotReadyErrorReason } from './k8s-helpers.js' +export { KubernetesConnectionError } from './log-error.js' diff --git a/packages/driver-kube-pod/src/driver/client/kubernetes-error.test.ts b/packages/driver-kube-pod/src/driver/client/kubernetes-error.test.ts new file mode 100644 index 00000000..1dc15989 --- /dev/null +++ b/packages/driver-kube-pod/src/driver/client/kubernetes-error.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect } from '@jest/globals' +import { KubernetesConnectionError } from './log-error.js' + +// Mock error scenarios that would come from the kubernetes client +const createMockConnectionError = (message: string, code?: number) => { + const error: any = new Error(message) + if (code !== undefined) { + error.response = { statusCode: code } + } + return error +} + +const createMockNetworkError = (message: string, code: string) => { + const error: any = new Error(message) + error.code = code + return error +} + +describe('KubernetesConnectionError', () => { + it('should identify and enhance ECONNREFUSED errors', () => { + const originalError = createMockNetworkError('connect ECONNREFUSED 192.168.1.100:6443', 'ECONNREFUSED') + const enhanced = new KubernetesConnectionError( + 'Failed to connect to Kubernetes cluster. The Kubernetes API server appears to be unreachable. Please check that your cluster is running and accessible.\n\nThis is a Kubernetes connectivity issue, not a tunnel server problem.', + originalError + ) + + expect(enhanced.message).toContain('Failed to connect to Kubernetes cluster') + expect(enhanced.message).toContain('This is a Kubernetes connectivity issue, not a tunnel server problem') + expect(enhanced.cause).toBe(originalError) + }) + + it('should identify and enhance DNS resolution errors', () => { + const originalError = createMockNetworkError('getaddrinfo ENOTFOUND k8s.example.com', 'ENOTFOUND') + const enhanced = new KubernetesConnectionError( + 'Failed to connect to Kubernetes cluster. Could not resolve the Kubernetes API server hostname. Please check your cluster configuration and network connectivity.\n\nThis is a Kubernetes connectivity issue, not a tunnel server problem.', + originalError + ) + + expect(enhanced.message).toContain('Could not resolve the Kubernetes API server hostname') + expect(enhanced.message).toContain('This is a Kubernetes connectivity issue, not a tunnel server problem') + }) + + it('should identify and enhance authentication errors', () => { + const originalError = createMockConnectionError('Unauthorized', 401) + const enhanced = new KubernetesConnectionError( + 'Failed to connect to Kubernetes cluster. Authentication failed. Please check your Kubernetes credentials and configuration.\n\nThis is a Kubernetes connectivity issue, not a tunnel server problem.', + originalError + ) + + expect(enhanced.message).toContain('Authentication failed') + expect(enhanced.message).toContain('This is a Kubernetes connectivity issue, not a tunnel server problem') + }) + + it('should identify and enhance authorization errors', () => { + const originalError = createMockConnectionError('Forbidden', 403) + const enhanced = new KubernetesConnectionError( + 'Failed to connect to Kubernetes cluster. Access denied. Please check that your Kubernetes credentials have the necessary permissions.\n\nThis is a Kubernetes connectivity issue, not a tunnel server problem.', + originalError + ) + + expect(enhanced.message).toContain('Access denied') + expect(enhanced.message).toContain('This is a Kubernetes connectivity issue, not a tunnel server problem') + }) + + it('should identify and enhance timeout errors', () => { + const originalError = createMockNetworkError('timeout of 5000ms exceeded', 'TIMEOUT') + const enhanced = new KubernetesConnectionError( + 'Failed to connect to Kubernetes cluster. Connection to the Kubernetes API server timed out. Please check your cluster configuration and network connectivity.\n\nThis is a Kubernetes connectivity issue, not a tunnel server problem.', + originalError + ) + + expect(enhanced.message).toContain('Connection to the Kubernetes API server timed out') + expect(enhanced.message).toContain('This is a Kubernetes connectivity issue, not a tunnel server problem') + }) + + it('should identify and enhance kubeconfig context errors', () => { + const originalError = new Error('no configuration has been provided, try setting KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT') + const enhanced = new KubernetesConnectionError( + 'Failed to connect to Kubernetes cluster. No valid Kubernetes configuration found. Please check your kubeconfig file and ensure it contains a valid context.\n\nThis is a Kubernetes connectivity issue, not a tunnel server problem.', + originalError + ) + + expect(enhanced.message).toContain('No valid Kubernetes configuration found') + expect(enhanced.message).toContain('This is a Kubernetes connectivity issue, not a tunnel server problem') + }) + + it('should preserve error properties correctly', () => { + const originalError = createMockConnectionError('Test error', 500) + originalError.stack = 'original stack trace' + + const enhanced = new KubernetesConnectionError('Enhanced message', originalError) + + expect(enhanced.name).toBe('KubernetesConnectionError') + expect(enhanced.cause).toBe(originalError) + expect(enhanced.message).toBe('Enhanced message') + }) +}) \ No newline at end of file diff --git a/packages/driver-kube-pod/src/driver/client/log-error-integration.test.ts b/packages/driver-kube-pod/src/driver/client/log-error-integration.test.ts new file mode 100644 index 00000000..ca6097b9 --- /dev/null +++ b/packages/driver-kube-pod/src/driver/client/log-error-integration.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from '@jest/globals' +import { logError } from './log-error.js' + +// Mock logger +const mockLogger = { + error: (message?: string, ...args: unknown[]) => console.log('LOG ERROR:', message, ...args), + warn: (message?: string, ...args: unknown[]) => console.log('LOG WARN:', message, ...args), + info: (message?: string, ...args: unknown[]) => console.log('LOG INFO:', message, ...args), + debug: (message?: string, ...args: unknown[]) => console.log('LOG DEBUG:', message, ...args), +} + +// Mock error scenarios that would come from the kubernetes client +const createMockConnectionError = (message: string, code?: number) => { + const error: any = new Error(message) + if (code !== undefined) { + error.response = { statusCode: code } + } + return error +} + +const createMockNetworkError = (message: string, code: string) => { + const error: any = new Error(message) + error.code = code + return error +} + +describe('logError function enhancement', () => { + it('should enhance ECONNREFUSED errors with clear kubernetes messaging', async () => { + const mockFunction = async () => { + throw createMockNetworkError('connect ECONNREFUSED 192.168.1.100:6443', 'ECONNREFUSED') + } + + const wrappedFunction = logError(mockLogger)(mockFunction) + + await expect(wrappedFunction()).rejects.toThrow(/Failed to connect to Kubernetes cluster/) + await expect(wrappedFunction()).rejects.toThrow(/This is a Kubernetes connectivity issue, not a tunnel server problem/) + }) + + it('should enhance DNS resolution errors with helpful guidance', async () => { + const mockFunction = async () => { + throw createMockNetworkError('getaddrinfo ENOTFOUND k8s.example.com', 'ENOTFOUND') + } + + const wrappedFunction = logError(mockLogger)(mockFunction) + + await expect(wrappedFunction()).rejects.toThrow(/Could not resolve the Kubernetes API server hostname/) + await expect(wrappedFunction()).rejects.toThrow(/This is a Kubernetes connectivity issue, not a tunnel server problem/) + }) + + it('should enhance authentication errors with credential guidance', async () => { + const mockFunction = async () => { + throw createMockConnectionError('Unauthorized', 401) + } + + const wrappedFunction = logError(mockLogger)(mockFunction) + + await expect(wrappedFunction()).rejects.toThrow(/Authentication failed/) + await expect(wrappedFunction()).rejects.toThrow(/This is a Kubernetes connectivity issue, not a tunnel server problem/) + }) + + it('should enhance authorization errors with permission guidance', async () => { + const mockFunction = async () => { + throw createMockConnectionError('Forbidden', 403) + } + + const wrappedFunction = logError(mockLogger)(mockFunction) + + await expect(wrappedFunction()).rejects.toThrow(/Access denied/) + await expect(wrappedFunction()).rejects.toThrow(/This is a Kubernetes connectivity issue, not a tunnel server problem/) + }) + + it('should enhance timeout errors with connectivity guidance', async () => { + const mockFunction = async () => { + throw createMockNetworkError('timeout of 5000ms exceeded', 'TIMEOUT') + } + + const wrappedFunction = logError(mockLogger)(mockFunction) + + await expect(wrappedFunction()).rejects.toThrow(/Connection to the Kubernetes API server timed out/) + await expect(wrappedFunction()).rejects.toThrow(/This is a Kubernetes connectivity issue, not a tunnel server problem/) + }) + + it('should enhance configuration errors with kubeconfig guidance', async () => { + const mockFunction = async () => { + throw new Error('no configuration has been provided, try setting KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT') + } + + const wrappedFunction = logError(mockLogger)(mockFunction) + + await expect(wrappedFunction()).rejects.toThrow(/No valid Kubernetes configuration found/) + await expect(wrappedFunction()).rejects.toThrow(/This is a Kubernetes connectivity issue, not a tunnel server problem/) + }) + + it('should pass through non-connection errors unchanged', async () => { + const originalError = new Error('Some other kubernetes error') + const mockFunction = async () => { + throw originalError + } + + const wrappedFunction = logError(mockLogger)(mockFunction) + + await expect(wrappedFunction()).rejects.toBe(originalError) + }) + + it('should return successful results unchanged', async () => { + const expectedResult = { success: true, data: 'test' } + const mockFunction = async () => expectedResult + + const wrappedFunction = logError(mockLogger)(mockFunction) + + await expect(wrappedFunction()).resolves.toBe(expectedResult) + }) +}) diff --git a/packages/driver-kube-pod/src/driver/client/log-error.ts b/packages/driver-kube-pod/src/driver/client/log-error.ts index 21db94d1..861d2492 100644 --- a/packages/driver-kube-pod/src/driver/client/log-error.ts +++ b/packages/driver-kube-pod/src/driver/client/log-error.ts @@ -2,6 +2,68 @@ import { HttpError } from '@kubernetes/client-node' import { Logger } from '@preevy/core' import { inspect } from 'util' +export class KubernetesConnectionError extends Error { + constructor(message: string, public readonly cause: Error) { + super(message) + this.name = 'KubernetesConnectionError' + } +} + +const isConnectionError = (error: any): boolean => { + if (!error) return false + + const message = error.message?.toLowerCase() || '' + const code = error.code || error.response?.statusCode + + // Common kubernetes connection error patterns + return ( + // Network connection errors + message.includes('econnrefused') || + message.includes('enotfound') || + message.includes('timeout') || + message.includes('network is unreachable') || + // Kubernetes API server connection errors + message.includes('unable to connect to the server') || + message.includes('connection refused') || + // Authentication/authorization errors (likely config issues) + code === 401 || code === 403 || + // Kubernetes config/context errors + message.includes('current-context') || + message.includes('no configuration has been provided') || + message.includes('unable to load in-cluster configuration') + ) +} + +const enhanceKubernetesError = (error: any): Error => { + if (isConnectionError(error)) { + const message = error.message?.toLowerCase() || '' + + let userFriendlyMessage = 'Failed to connect to Kubernetes cluster. ' + + if (message.includes('econnrefused') || message.includes('connection refused')) { + userFriendlyMessage += 'The Kubernetes API server appears to be unreachable. Please check that your cluster is running and accessible.' + } else if (message.includes('enotfound') || message.includes('network is unreachable')) { + userFriendlyMessage += 'Could not resolve the Kubernetes API server hostname. Please check your cluster configuration and network connectivity.' + } else if (message.includes('timeout')) { + userFriendlyMessage += 'Connection to the Kubernetes API server timed out. Please check your cluster configuration and network connectivity.' + } else if (error.response?.statusCode === 401) { + userFriendlyMessage += 'Authentication failed. Please check your Kubernetes credentials and configuration.' + } else if (error.response?.statusCode === 403) { + userFriendlyMessage += 'Access denied. Please check that your Kubernetes credentials have the necessary permissions.' + } else if (message.includes('current-context') || message.includes('no configuration has been provided')) { + userFriendlyMessage += 'No valid Kubernetes configuration found. Please check your kubeconfig file and ensure it contains a valid context.' + } else { + userFriendlyMessage += 'Please check your Kubernetes cluster configuration and connectivity.' + } + + userFriendlyMessage += '\n\nThis is a Kubernetes connectivity issue, not a tunnel server problem.' + + return new KubernetesConnectionError(userFriendlyMessage, error) + } + + return error +} + export const logError = (log: Logger) => < Args extends unknown[], ReturnType @@ -12,9 +74,11 @@ export const logError = (log: Logger) => < return await f(...args) } catch (e) { if (e instanceof HttpError) { - log.error(`Response: ${inspect(e.body)}`) + log.error(`Kubernetes API Response: ${inspect(e.body)}`) } - throw e + + const enhancedError = enhanceKubernetesError(e) + throw enhancedError } }