1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | {request} = require 'http'
|
8 | fs = require 'fs'
|
9 | bouncy = require 'bouncy'
|
10 | semver = require 'semver'
|
11 | dateFormat = require 'dateformat'
|
12 |
|
13 | unless (configFile = process.argv[2])
|
14 | console.error "usage: #{process.argv[1]} <config_file>"
|
15 | process.exit 1
|
16 |
|
17 |
|
18 | config = JSON.parse fs.readFileSync configFile
|
19 | try
|
20 | fs.statSync config.stateFile
|
21 | catch e
|
22 | fs.writeFileSync config.stateFile, JSON.stringify backends: {}, null, 2
|
23 | state = JSON.parse fs.readFileSync config.stateFile
|
24 |
|
25 |
|
26 | doEvery = (delay, cb) -> setInterval cb, delay
|
27 |
|
28 |
|
29 | log = (args...) ->
|
30 | now = dateFormat new Date, "yyyy-mm-dd HH:MM:ss"
|
31 | console.log "[" + now + "]", args...
|
32 |
|
33 |
|
34 |
|
35 | registerBackend = (version, location, cfg) ->
|
36 | state.backends[version] ?= {}
|
37 | state.backends[version][location] = cfg
|
38 | fs.writeFileSync config.stateFile, JSON.stringify state, null, 2
|
39 | {status: 'ok', location: location, version: version}
|
40 |
|
41 |
|
42 | unregisterBackend = (version, location) ->
|
43 | backends = state.backends[version]
|
44 | delete backends[location] if backends?
|
45 |
|
46 |
|
47 |
|
48 | updateBackend = (version, location, alive) ->
|
49 | state.backends[version][location].alive = alive
|
50 | fs.writeFileSync config.stateFile, JSON.stringify state, null, 2
|
51 |
|
52 |
|
53 | pickVersion = (range) ->
|
54 | semver.maxSatisfying Object.keys(state.backends), range
|
55 |
|
56 |
|
57 | listBackends = (version) ->
|
58 | return [null, []] unless version
|
59 | [version, state.backends[version] or {}]
|
60 |
|
61 |
|
62 | pickBackend = (backends) ->
|
63 |
|
64 | for loc, stat of backends
|
65 | return loc if stat.alive
|
66 | null
|
67 |
|
68 |
|
69 | bouncy((req, bounce) ->
|
70 | reqVer = req.headers['x-version'] ? config.defaultVersion
|
71 | unless semver.validRange(reqVer) and (version = pickVersion reqVer)
|
72 | res = bounce.respond()
|
73 | res.statusCode = 400
|
74 | return res.end JSON.stringify error: "Bad version: #{reqVer}"
|
75 | unavailable = ->
|
76 | res = bounce.respond()
|
77 | res.statusCode = 404
|
78 | res.end JSON.stringify error: "Version unavailable: #{version}"
|
79 | forward = ->
|
80 | backends = state.backends[version]
|
81 | if backends? and Object.keys(backends).length
|
82 | loc = pickBackend backends
|
83 | return unavailable() unless loc?
|
84 | [host, port] = loc.split ':'
|
85 | bounce(host, port).on 'error', (exc) ->
|
86 | updateBackend version, loc, false
|
87 | if config.retry then forward() else unavailable()
|
88 | else
|
89 | unavailable()
|
90 | forward()
|
91 | ).listen config.ports.proxy
|
92 |
|
93 |
|
94 | if config.pollFrequency
|
95 | doEvery config.pollFrequency, ->
|
96 | for ver, backends of state.backends
|
97 | for loc, stat of backends
|
98 | do (ver, loc, stat) ->
|
99 | [host, port] = loc.split ':'
|
100 | path = stat.healthCheckPath or '/'
|
101 | method = 'GET'
|
102 | req = request {host:host, port:port, path:path, method:method}, (res) ->
|
103 | if res.statusCode == 200 and not stat.alive
|
104 | log "Backend #{ver} is ALIVE: #{host}:#{port}" unless stat.alive
|
105 | updateBackend ver, loc, true
|
106 | else if res.statusCode != 200 and stat.alive
|
107 | log "Backend #{ver} is DEAD: #{host}:#{port}" if stat.alive
|
108 | updateBackend ver, loc, false
|
109 | req.on 'error', ->
|
110 | log "Backend #{ver} is DEAD: #{host}:#{port}" if stat.alive
|
111 | updateBackend ver, loc, false
|
112 | req.end()
|
113 |
|
114 | if config.ports.management
|
115 | require('lazorse') ->
|
116 | @port = config.ports.management
|
117 | @route '/defaultVersion':
|
118 | shortName: 'defaultVersion'
|
119 | GET: -> @ok config.defaultVersion
|
120 |
|
121 | @route '/versions':
|
122 | shortName: "versions"
|
123 | GET: -> @ok Object.keys state.backends
|
124 |
|
125 | @route '/version/{range}':
|
126 | shortName: "versionForRange"
|
127 | GET: ->
|
128 | ver = pickVersion @range
|
129 | return @ok "none" unless ver?
|
130 | @ok ver
|
131 |
|
132 | @coerce range: (r, next) ->
|
133 | if (valid = semver.validRange r) then return next null, valid
|
134 | @error 'InvalidParameter', 'range', r
|
135 |
|
136 | @helper getHostAndPort: (withPort) ->
|
137 | if (port = @req.body.port)
|
138 | host = @req.body.host ? @req.connection.remoteAddress
|
139 | withPort.call @, host, port
|
140 | else
|
141 | @res.statusCode = 422
|
142 | @res.end '"port" is required'
|
143 |
|
144 | @route '/backends/{version}':
|
145 | shortName: "backends"
|
146 | GET: -> @ok listBackends @version
|
147 | POST: ->
|
148 | @getHostAndPort (host, port) ->
|
149 | cfg = alive: true
|
150 | if @req.body.healthCheckPath
|
151 | cfg.healthCheckPath = @req.body.healthCheckPath
|
152 | @ok registerBackend @version, "#{host}:#{port}", cfg
|
153 | DELETE: ->
|
154 | @getHostAndPort (host, port) ->
|
155 | @ok unregisterBackend @version, "#{host}:#{port}"
|
156 |
|
157 | @coerce 'version': (v, next) =>
|
158 | if (valid = semver.valid v) then return next null, valid
|
159 | next new @errors.InvalidParameter 'version', v
|