UNPKG

6.3 kBJavaScriptView Raw
1const Docker = require('dockerode')
2const tarstream = require('tar-stream')
3const tarfs = require('tar-fs')
4const async = require('async')
5const chalk = require('chalk')
6const moment = require('moment')
7
8const docker = new Docker({
9 socketPath: '/var/run/docker.sock'
10})
11
12class Container {
13 constructor (devServer, friendlyName, type) {
14 this.devServer = devServer
15 this.network = devServer.network
16 this.friendlyName = friendlyName
17 this.name = (type === 'router') ? 'localapp' : `noop-${this.devServer.ns}-${type}-${friendlyName}`
18 this.type = type
19 this.container = docker.getContainer(this.name)
20 this.desiredRunning = false
21 this.restartAttempts = 0
22 this.specials = []
23 }
24
25 start (done) {
26 const image = this.getImage()
27 this.desiredRunning = true
28 if (this.component) {
29 const directives = this.component.directives.filter((directive) => {
30 return directive.cmd === 'SPECIAL'
31 }).forEach((directive) => {
32 this.specials.push(directive.params.option)
33 })
34 }
35 async.auto({
36 removeExisting: (done) => {
37 this.container.inspect((err) => {
38 if (err && err.statusCode === 404) {
39 done() // no problem if container doesn't exit
40 } else if (err) {
41 done(err)
42 } else {
43 this.container.remove({force: true}, done)
44 }
45 })
46 },
47 inspectImage: (done) => {
48 docker.getImage(image).inspect((err) => {
49 if (err && err.statusCode === 404) {
50 this.pullImage(done)
51 } else {
52 done(err)
53 }
54 })
55 },
56 volumes: (done) => {
57 if ('getVolumes' in this) {
58 this.getVolumes(done)
59 } else {
60 done(null)
61 }
62 },
63 container: ['volumes', 'inspectImage', 'removeExisting', (results, done) => {
64 const envMap = this.getEnv()
65 const env = Object.keys(envMap).map((key) => {
66 return `${key}=${envMap[key]}`
67 })
68 const opts = {
69 'AttachStdout': true,
70 'AttachStderr': true,
71 'Image': image,
72 'Hostname': this.name,
73 'name': this.name
74 }
75 if (results.volumes) {
76 opts.Volumes = {}
77 opts.HostConfig = {Binds: []}
78 Object.keys(results.volumes).forEach((volume) => {
79 opts.Volumes[volume] = {}
80 opts.HostConfig.Binds.push(`${results.volumes[volume]}:${volume}`)
81 })
82 }
83 if (env.length) opts.Env = env
84 if (this.type === 'router') {
85 opts.HostConfig = {
86 PortBindings: {}
87 }
88 if (this.devServer.http === true) {
89 opts.HostConfig.PortBindings['80/tcp'] = [{HostPort: this.devServer.port.toString()}]
90 } else {
91 opts.HostConfig.PortBindings['443/tcp'] = [{HostPort: this.devServer.port.toString()}]
92 }
93 opts.ExposedPorts = {'443/tcp': {}, '80/tcp': {}}
94 }
95 if (this.specials.indexOf('privileged') !== -1) {
96 if (!opts.HostConfig) opts.HostConfig = {}
97 opts.HostConfig.Privileged = true
98 }
99 docker.createContainer(opts, done)
100 }],
101 network: ['container', (results, done) => {
102 this.network.attachContainer(this.name, done)
103 }],
104 output: ['container', (results, done) => this.handleOutput(done)],
105 start: ['network', 'output', (results, done) => {
106 console.log(`Starting '${this.friendlyName}' ${this.type} container`)
107 results.container.start(done)
108 }],
109 watch: ['start', (results, done) => this.watchForExit(done)]
110 }, done)
111 }
112
113 stop (done) {
114 this.desiredRunning = false
115 this.container.remove({force: true}, (err) => {
116 if (err) return done(err)
117 console.log(`Stopped ${this.type} '${this.friendlyName}' container`)
118 done()
119 })
120 }
121
122 restart () {
123 this.restartAttempts++
124 if (this.restartAttempts > 10) return false
125 console.log(`Restarting ${this.type} '${this.friendlyName}' container attempt #${this.restartAttempts}`)
126 this.start((err) => {
127 if (err) console.log(`Unable to restart ${this.type} '${this.friendlyName}' container`, err)
128 })
129 }
130
131 handleOutput (done) {
132 if (this.type === 'resource' && !this.devServer.verbose) return done()
133 const opts = {
134 stream: true,
135 stdout: true,
136 stderr: true
137 }
138 this.container.attach(opts, (err, stream) => {
139 if (err) return done(err)
140 let spaces = ''
141 let prefix
142 if (this.friendlyName.length < 16) {
143 spaces = Array(16 - this.friendlyName.length).join(' ')
144 }
145 if (this.type === 'router') {
146 prefix = chalk.magenta(this.friendlyName.substr(0, 15))
147 } else {
148 prefix = chalk.cyan(this.friendlyName.substr(0, 15))
149 }
150 stream.on('data', (data) => {
151 data = data.slice(8) // no idea why the first 8 bytes is garbage
152 let lines = data.toString().split(/(\r\n|\r|\n)/)
153 let time = chalk.gray(moment().format('HH:mm:ss'))
154 lines.forEach((line) => {
155 if (/\w/.test(line)) console.log(` ${prefix}${spaces} ${time} ${line}`)
156 })
157 })
158 done()
159 })
160 }
161
162 watchForExit (done) {
163 this.container.wait((err, data) => {
164 if (err) return done(err)
165 const capsType = this.type.charAt(0).toUpperCase() + this.type.slice(1)
166 if (this.desiredRunning) {
167 console.log(chalk.red(`${capsType} container '${this.friendlyName}' exited with status code ${data.StatusCode}`))
168 this.restart()
169 }
170 })
171 done()
172 }
173
174 pullImage (done) {
175 const image = this.getImage()
176 console.log(`Pulling container image '${image}'`)
177 const complete = (err) => {
178 console.log('DONE')
179 if (err) {
180 console.log(chalk.red(`Error pulling container image ${image}`))
181 done(new ContainerPullError(err))
182 } else {
183 console.log(`Completed pull of container image '${image}'`)
184 done()
185 }
186 }
187 docker.createImage({fromImage: image}, (err, output) => {
188 if (err) return done(err)
189 docker.modem.followProgress(output, complete, (event) => {
190 // nothing to do with the output
191 })
192 })
193 }
194
195 getEnv() {
196 return {}
197 }
198}
199
200module.exports = Container