undici's fetch() collapses DNS/TLS/connection-refused/timeout failures into a vague TypeError. Unwrap err.cause so Test Connection shows the real reason (e.g. ECONNREFUSED, certificate error) instead of just 'fetch failed'.
64 lines
2.3 KiB
TypeScript
64 lines
2.3 KiB
TypeScript
import { Agent } from 'undici'
|
|
import type { IntegrationAdapter, Resource } from './types.js'
|
|
|
|
interface ProxmoxResourceEntry {
|
|
type: string
|
|
name?: string
|
|
status?: string
|
|
vmid?: number
|
|
node?: string
|
|
}
|
|
|
|
function authHeader(apiKey: string): Record<string, string> {
|
|
return { Authorization: `PVEAPIToken=${apiKey}` }
|
|
}
|
|
|
|
// Proxmox ships with a self-signed cert by default, which Node's fetch rejects out of the box.
|
|
const insecureDispatcher = new Agent({ connect: { rejectUnauthorized: false } })
|
|
|
|
// undici's fetch() wraps the real failure (DNS, TLS, connection refused/timed out) in a generic
|
|
// "fetch failed" TypeError — unwrap err.cause so the actual reason reaches the UI.
|
|
function describeFetchError(err: unknown): string {
|
|
if (err instanceof Error) {
|
|
const cause = (err as { cause?: unknown }).cause
|
|
if (cause instanceof Error) return `${err.message}: ${cause.message}`
|
|
return err.message
|
|
}
|
|
return 'Connection failed'
|
|
}
|
|
|
|
export const proxmox: IntegrationAdapter = {
|
|
async testConnection(config, secrets) {
|
|
const baseUrl = config.baseUrl?.replace(/\/$/, '')
|
|
const apiKey = secrets.apiKey
|
|
if (!baseUrl) return { ok: false, message: 'Missing baseUrl' }
|
|
if (!apiKey) return { ok: false, message: 'Missing API token' }
|
|
try {
|
|
const res = await fetch(`${baseUrl}/api2/json/version`, {
|
|
headers: authHeader(apiKey),
|
|
dispatcher: insecureDispatcher,
|
|
} as RequestInit)
|
|
if (!res.ok) return { ok: false, message: `HTTP ${res.status}` }
|
|
return { ok: true, message: 'Connected' }
|
|
} catch (err) {
|
|
return { ok: false, message: describeFetchError(err) }
|
|
}
|
|
},
|
|
|
|
async listResources(config, secrets): Promise<Resource[]> {
|
|
const baseUrl = config.baseUrl?.replace(/\/$/, '')
|
|
const apiKey = secrets.apiKey
|
|
if (!baseUrl || !apiKey) return []
|
|
const res = await fetch(`${baseUrl}/api2/json/cluster/resources?type=vm`, {
|
|
headers: authHeader(apiKey),
|
|
dispatcher: insecureDispatcher,
|
|
} as RequestInit)
|
|
if (!res.ok) return []
|
|
const body = (await res.json()) as { data: ProxmoxResourceEntry[] }
|
|
return body.data.map((entry) => ({
|
|
name: entry.name ?? `vm-${entry.vmid}`,
|
|
status: entry.status === 'running' ? 'healthy' : entry.status === 'stopped' ? 'unknown' : 'warning',
|
|
detail: `${entry.type} on ${entry.node} — ${entry.status}`,
|
|
}))
|
|
},
|
|
}
|