Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 59 additions & 5 deletions packages/driver-kube-pod/src/driver/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -315,3 +368,4 @@ export type CreationClient = ReturnType<typeof kubeCreationClient>

export { extractInstance, extractEnvId, extractName, extractNamespace, extractTemplateHash } from './metadata.js'
export { DeploymentNotReadyError, DeploymentNotReadyErrorReason } from './k8s-helpers.js'
export { KubernetesConnectionError } from './log-error.js'
Original file line number Diff line number Diff line change
@@ -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')
})
})
Original file line number Diff line number Diff line change
@@ -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)
})
})
68 changes: 66 additions & 2 deletions packages/driver-kube-pod/src/driver/client/log-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}

Expand Down