Skip to content

Commit

Permalink
Pretty much that
Browse files Browse the repository at this point in the history
  • Loading branch information
antoineleclair committed Jun 19, 2024
1 parent 2ca15a0 commit a59b0fe
Showing 1 changed file with 59 additions and 5 deletions.
64 changes: 59 additions & 5 deletions src/commands/postgres/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@ import {Command, Flags} from '@oclif/core'
import {getDisco} from '../../config.js'
import {request} from '../../auth-request.js'
import {createTunnel} from 'tunnel-ssh'
import {AddressInfo, createServer} from 'node:net'

async function portIsAvailable(port: number): Promise<boolean> {
const isAvailable = await new Promise<boolean>((resolve) => {
const server = createServer()
server.once('error', (err: {code: string}) => {
if (err.code === 'EADDRINUSE') {
resolve(false) // not available
}
})

server.once('listening', () => {
// close the server if listening doesn't fail
server.close()
resolve(true)
})

server.listen(port)
})

return isAvailable
}

async function localPort(portFlag: number | undefined): Promise<number | undefined> {
if (portFlag === undefined) {
const portInUse = !(await portIsAvailable(5432))
if (portInUse) {
// port in use, let OS pick another port
return undefined
} else {
// port available, use default Postgres port
return 5432
}
} else {
// port specified as CLI arg
return portFlag
}
}

export default class PostgresTunnel extends Command {
static description = 'create a temporary tunnel to access Postgres through localhost'
Expand All @@ -12,11 +50,15 @@ export default class PostgresTunnel extends Command {
disco: Flags.string({required: false}),
project: Flags.string({required: true}),
'env-var': Flags.string({required: false}),
port: Flags.integer({required: false}),
}

public async run(): Promise<void> {
const {flags} = await this.parse(PostgresTunnel)
const discoConfig = getDisco(flags.disco || null)
if (flags.port !== undefined && !(await portIsAvailable(flags.port))) {
this.error(`Port ${flags.port} already in use`)
}

const dbInfoUrl = `https://${discoConfig.host}/api/projects/postgres-addon/cgi/endpoints/tunnels`
const dbInfoResponse = await request({
Expand Down Expand Up @@ -47,8 +89,6 @@ export default class PostgresTunnel extends Command {
})
const respBody = (await res.json()) as {tunnel: {host: string; password: string; port: number}}
const tunnelPort = respBody.tunnel.port
const connString = `postgresql://${dbInfo.user}:${dbInfo.password}@localhost/${dbInfo.database}`

const sshOptions = {
host: discoConfig.host,
port: respBody.tunnel.port,
Expand All @@ -62,16 +102,30 @@ export default class PostgresTunnel extends Command {
}

const tunnelOptions = {
autoClose: true,
// do not close tunnel when third party app disconnects
autoClose: false,
}

const serverOptions = {
host: 'localhost',
port: 5432,
port: await localPort(flags.port),
}

const [_, tunnelClient] = await createTunnel(tunnelOptions, serverOptions, sshOptions, forwardOptions)
// check again now that sshd container is ready
if (flags.port !== undefined && !(await portIsAvailable(flags.port))) {
this.error(`Port ${flags.port} already in use`)
}

const [tunnelServer, tunnelClient] = await createTunnel(tunnelOptions, serverOptions, sshOptions, forwardOptions)
tunnelServer.on('connection', (socket) => {
socket.on('error', () => {
// swallow error
// instead of crashing on errors like ECONNRESET
})
})
this.log('Tunnel created. Connection string:')
const {port} = tunnelServer.address() as AddressInfo
const connString = `postgresql://${dbInfo.user}:${dbInfo.password}@localhost:${port}/${dbInfo.database}`
this.log(connString)
const extendInterval = setInterval(async () => {
await request({
Expand Down

0 comments on commit a59b0fe

Please sign in to comment.