-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Port manager #42
Port manager #42
Changes from all commits
bcd10b7
d028563
82cd169
ecc19c8
9b660ea
e151907
75726b0
5766ea7
c2e3013
060b9f6
d5ce5cd
61d81d4
b83750f
73d05b1
90ae978
e79df2a
1d30147
91a3228
0bb5532
d191ff3
053dba6
872304f
2726eec
de09bad
25eb5ea
825d86b
bd1e9cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export const MIN_PORT_NUMBER = 1024; | ||
export const MAX_PORT_NUMBER = 65535; | ||
|
||
export const PORT_MANAGER_SERVER_PORT = 8080; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import axios from 'axios'; | ||
|
||
import PortManagerServer from '../utils/PortManagerServer'; | ||
import { detectPort } from '../utils/detectPort'; | ||
import { PORT_MANAGER_SERVER_PORT } from '../constants/ports'; | ||
|
||
const BASE_URL = `http://localhost:${PORT_MANAGER_SERVER_PORT}`; | ||
|
||
async function isPortManagerServerRunning(): Promise<boolean> { | ||
const port = await detectPort(PORT_MANAGER_SERVER_PORT); | ||
return port !== PORT_MANAGER_SERVER_PORT; | ||
} | ||
|
||
export async function startPortManagerServer(): Promise<void> { | ||
const isRunning = await isPortManagerServerRunning(); | ||
|
||
if (!isRunning) { | ||
await PortManagerServer.init(); | ||
} | ||
} | ||
|
||
export async function stopPortManagerServer(): Promise<void> { | ||
const isRunning = await isPortManagerServerRunning(); | ||
|
||
if (isRunning) { | ||
await axios.post(`${BASE_URL}/close`); | ||
} | ||
} | ||
|
||
type RequestPortsData = { | ||
instanceId: string; | ||
port?: number; | ||
}; | ||
|
||
export async function requestPorts( | ||
portData: Array<RequestPortsData> | ||
): Promise<{ [instanceId: string]: number }> { | ||
const { data } = await axios.post(`${BASE_URL}/servers`, { | ||
portData: portData, | ||
}); | ||
|
||
return data.ports; | ||
} | ||
|
||
export async function deleteServerInstance( | ||
serverInstanceId: string | ||
): Promise<void> { | ||
await axios.post(`${BASE_URL}/servers/${serverInstanceId}`); | ||
} | ||
|
||
export async function portManagerHasActiveServers() { | ||
const { data } = await axios.get(`${BASE_URL}/servers`); | ||
return data.count > 0; | ||
} | ||
|
||
function toId(str: string) { | ||
return str.replace(/\s+/g, '-').toLowerCase(); | ||
} | ||
|
||
export function getServerInstanceId(serverId: string, resourceId: string) { | ||
return `${toId(serverId)}__${toId(resourceId)}`; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export type RequestPortsData = { | ||
instanceId: string; | ||
port?: number; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import express, { Express, Request, Response } from 'express'; | ||
import { Server } from 'http'; | ||
import cors from 'cors'; | ||
|
||
import { detectPort } from './detectPort'; | ||
import { | ||
MIN_PORT_NUMBER, | ||
MAX_PORT_NUMBER, | ||
PORT_MANAGER_SERVER_PORT, | ||
} from '../constants/ports'; | ||
import { throwErrorWithMessage } from '../errors/standardErrors'; | ||
import { debug } from './logger'; | ||
import { i18n } from './lang'; | ||
import { BaseError } from '../types/Error'; | ||
import { RequestPortsData } from '../types/PortManager'; | ||
|
||
type ServerPortMap = { | ||
[instanceId: string]: number; | ||
}; | ||
|
||
const i18nKey = 'utils.PortManagerServer'; | ||
|
||
class PortManagerServer { | ||
app?: Express; | ||
server?: Server; | ||
serverPortMap: ServerPortMap; | ||
|
||
constructor() { | ||
this.serverPortMap = {}; | ||
} | ||
|
||
async init(): Promise<void> { | ||
if (this.app) { | ||
throwErrorWithMessage(`${i18nKey}.errors.duplicateInstance`); | ||
} | ||
this.app = express(); | ||
this.app.use(express.json()); | ||
this.app.use(cors()); | ||
this.setupRoutes(); | ||
|
||
try { | ||
this.server = await this.listen(); | ||
} catch (e) { | ||
const error = e as BaseError; | ||
if (error.code === 'EADDRINUSE') { | ||
throwErrorWithMessage( | ||
`${i18nKey}.errors.portInUse`, | ||
{ | ||
port: PORT_MANAGER_SERVER_PORT, | ||
}, | ||
error | ||
); | ||
} | ||
throw error; | ||
} | ||
} | ||
|
||
listen(): Promise<Server> { | ||
return new Promise<Server>((resolve, reject) => { | ||
const server = this.app!.listen(PORT_MANAGER_SERVER_PORT, () => { | ||
debug(`${i18nKey}.started`, { | ||
port: PORT_MANAGER_SERVER_PORT, | ||
}); | ||
resolve(server); | ||
}).on('error', (err: BaseError) => { | ||
reject(err); | ||
}); | ||
}); | ||
} | ||
|
||
setupRoutes(): void { | ||
if (!this.app) { | ||
return; | ||
} | ||
|
||
this.app.get('/servers', this.getServers); | ||
this.app.get('/servers/:instanceId', this.getServerPortByInstanceId); | ||
this.app.post('/servers', this.assignPortsToServers); | ||
this.app.delete('/servers/:instanceId', this.deleteServerInstance); | ||
this.app.post('/close', this.closeServer); | ||
} | ||
|
||
setPort(instanceId: string, port: number) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this protect against initializing duplicate instanceId's? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
debug(`${i18nKey}.setPort`, { instanceId, port }); | ||
this.serverPortMap[instanceId] = port; | ||
} | ||
|
||
deletePort(instanceId: string) { | ||
debug(`${i18nKey}.deletedPort`, { | ||
instanceId, | ||
port: this.serverPortMap[instanceId], | ||
}); | ||
delete this.serverPortMap[instanceId]; | ||
} | ||
|
||
send404(res: Response, instanceId: string) { | ||
res | ||
.status(404) | ||
.send(i18n(`${i18nKey}.errors.404`, { instanceId: instanceId })); | ||
} | ||
|
||
getServers = async (req: Request, res: Response): Promise<void> => { | ||
res.send({ | ||
servers: this.serverPortMap, | ||
count: Object.keys(this.serverPortMap).length, | ||
}); | ||
}; | ||
|
||
getServerPortByInstanceId = (req: Request, res: Response): void => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the return on this be Promise? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't thinks so, this isn't async. Express knows how to handle it as is |
||
const { instanceId } = req.params; | ||
const port = this.serverPortMap[instanceId]; | ||
|
||
if (port) { | ||
res.send({ port }); | ||
} else { | ||
this.send404(res, instanceId); | ||
} | ||
}; | ||
|
||
assignPortsToServers = async ( | ||
req: Request<never, never, { portData: Array<RequestPortsData> }>, | ||
res: Response | ||
): Promise<void> => { | ||
const { portData } = req.body; | ||
|
||
const portPromises: Array<Promise<{ [instanceId: string]: number }>> = []; | ||
|
||
portData.forEach(data => { | ||
const { port, instanceId } = data; | ||
if (this.serverPortMap[instanceId]) { | ||
res.status(409).send( | ||
i18n(`${i18nKey}.errors.409`, { | ||
instanceId, | ||
port: this.serverPortMap[instanceId], | ||
}) | ||
); | ||
return; | ||
} else if (port && (port < MIN_PORT_NUMBER || port > MAX_PORT_NUMBER)) { | ||
res.status(400).send( | ||
i18n(`${i18nKey}.errors.400`, { | ||
minPort: MIN_PORT_NUMBER, | ||
maxPort: MAX_PORT_NUMBER, | ||
}) | ||
); | ||
return; | ||
} else { | ||
const promise = new Promise<{ [instanceId: string]: number }>( | ||
resolve => { | ||
detectPort(port).then(resolvedPort => { | ||
resolve({ | ||
[instanceId]: resolvedPort, | ||
}); | ||
}); | ||
} | ||
); | ||
portPromises.push(promise); | ||
} | ||
}); | ||
|
||
const portList = await Promise.all(portPromises); | ||
const ports = portList.reduce((a, c) => Object.assign(a, c)); | ||
|
||
for (const instanceId in ports) { | ||
this.setPort(instanceId, ports[instanceId]); | ||
} | ||
|
||
res.send({ ports }); | ||
}; | ||
|
||
deleteServerInstance = (req: Request, res: Response): void => { | ||
const { instanceId } = req.params; | ||
const port = this.serverPortMap[instanceId]; | ||
|
||
if (port) { | ||
this.deletePort(instanceId); | ||
res.send(200); | ||
} else { | ||
this.send404(res, instanceId); | ||
} | ||
}; | ||
|
||
closeServer = (req: Request, res: Response): void => { | ||
if (this.server) { | ||
debug(`${i18nKey}.close`); | ||
res.send(200); | ||
this.server.close(); | ||
} | ||
}; | ||
} | ||
|
||
export default new PortManagerServer(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a blocker - I wonder if we should even bother having a
models/
folder in this repo. I think we have a few other classes in here that also aren't in that folder.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah 🤷 . Was left over from cli-lib but definitely don't think we need to make the distinction. I'm down to remove in the future