1 | const Docker = require('dockerode')
|
2 | const tarstream = require('tar-stream')
|
3 | const tarfs = require('tar-fs')
|
4 | const async = require('async')
|
5 | const chalk = require('chalk')
|
6 | const moment = require('moment')
|
7 |
|
8 | const docker = new Docker({
|
9 | socketPath: '/var/run/docker.sock'
|
10 | })
|
11 |
|
12 | class 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()
|
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)
|
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 |
|
191 | })
|
192 | })
|
193 | }
|
194 |
|
195 | getEnv() {
|
196 | return {}
|
197 | }
|
198 | }
|
199 |
|
200 | module.exports = Container
|