@uscreen.de/dev-service 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/check.js CHANGED
@@ -1,9 +1,11 @@
1
1
  'use strict'
2
2
 
3
+ import { exec } from 'node:child_process'
4
+ import path from 'node:path'
3
5
  import fs from 'fs-extra'
4
- import path from 'path'
5
- import { exec } from 'child_process'
6
6
  import YAML from 'yaml'
7
+ import { COMPOSE_DIR } from './constants.js'
8
+
7
9
  import {
8
10
  checkComposeDir,
9
11
  escape,
@@ -13,60 +15,6 @@ import {
13
15
  warning
14
16
  } from './utils.js'
15
17
 
16
- import { COMPOSE_DIR } from './constants.js'
17
-
18
- /**
19
- * Get paths to other running dev-service instances
20
- */
21
- export const checkOtherServices = async () => {
22
- const paths = (await getComposePaths())
23
- .filter((p) => p !== COMPOSE_DIR)
24
- .filter((p) => p.endsWith('/services/.compose'))
25
- .map((p) => p.replace(/\/services\/.compose$/, ''))
26
-
27
- if (paths.length > 0) {
28
- warning(
29
- [
30
- 'dev-service is already running, started in following folder(s):',
31
- ...paths.map((p) => ` ${p}`)
32
- ].join('\n')
33
- )
34
- }
35
- }
36
-
37
- /**
38
- * check for processes using needed ports
39
- */
40
- export const checkUsedPorts = async (service) => {
41
- if (!checkComposeDir()) {
42
- throw Error('No services found. Try running `service install`')
43
- }
44
-
45
- const requiredPorts = getRequiredPorts(service)
46
- const ownPorts = await getOwnPorts()
47
- const ports = requiredPorts.filter((p) => !ownPorts.includes(p))
48
-
49
- const portsToPids = await getPIDs(ports)
50
-
51
- if (Object.keys(portsToPids).length === 0) return // everything ok
52
-
53
- const pids = [...new Set(Object.values(portsToPids))]
54
- const pidsToProcesses = await getProcesses(pids)
55
-
56
- throw Error(
57
- [
58
- 'Required port(s) are already allocated:',
59
- ...Object.entries(portsToPids).map(
60
- ([port, pid]) =>
61
- `- port ${port} is used by process with pid ${pid}` +
62
- (pidsToProcesses[pid] && pidsToProcesses[pid].cmd
63
- ? ` (${pidsToProcesses[pid].cmd})`
64
- : '')
65
- )
66
- ].join('\n')
67
- )
68
- }
69
-
70
18
  /**
71
19
  * Get all ports required (by given service)
72
20
  */
@@ -74,24 +22,51 @@ const getRequiredPorts = (service) => {
74
22
  const files = getComposeFiles()
75
23
  const ports = []
76
24
  for (const f of files) {
77
- if (service && `${service}.yml` !== f) continue
25
+ if (service && `${service}.yml` !== f) {
26
+ continue
27
+ }
78
28
 
79
29
  const yaml = YAML.parse(
80
30
  fs.readFileSync(path.resolve(COMPOSE_DIR, f), 'utf8')
81
31
  )
82
32
  ports.push(
83
33
  ...Object.values(yaml.services)
84
- .map((v) => v.ports)
85
- .filter((p) => p)
34
+ .map(v => v.ports)
35
+ .filter(p => p)
86
36
  .flat()
87
37
  )
88
38
  }
89
39
 
90
- const uniquePorts = [...new Set(ports.map((p) => p.split(':')[0]))]
40
+ const uniquePorts = [...new Set(ports.map(p => p.split(':')[0]))]
91
41
 
92
42
  return uniquePorts
93
43
  }
94
44
 
45
+ /**
46
+ * Get own ports
47
+ */
48
+ const getContainerPorts = containerId =>
49
+ new Promise((resolve, reject) => {
50
+ exec(`docker port ${containerId}`, (err, stdout, stderr) => {
51
+ if (err) {
52
+ return reject(err)
53
+ }
54
+
55
+ const errMessage = stderr.toString().trim()
56
+ if (errMessage) {
57
+ return reject(new Error(errMessage))
58
+ }
59
+
60
+ const lines = stdout.split('\n').filter(l => l)
61
+ const ports = lines
62
+ .map(l => l.match(/.*:(\d+)/))
63
+ .map(m => (m.length >= 1 ? m[1] : null))
64
+ .filter(p => p)
65
+
66
+ resolve(ports)
67
+ })
68
+ })
69
+
95
70
  /**
96
71
  * get ports currently used by this services instance
97
72
  */
@@ -109,13 +84,17 @@ const getOwnPorts = () =>
109
84
 
110
85
  ps.push('ps', '-q')
111
86
 
112
- exec(`docker-compose ${ps.join(' ')}`, function (err, stdout, stderr) {
113
- if (err) return reject(err)
87
+ exec(`docker-compose ${ps.join(' ')}`, (err, stdout, stderr) => {
88
+ if (err) {
89
+ return reject(err)
90
+ }
114
91
 
115
92
  const errMessage = stderr.toString().trim()
116
- if (errMessage) return reject(Error(errMessage))
93
+ if (errMessage) {
94
+ return reject(new Error(errMessage))
95
+ }
117
96
 
118
- const ids = stdout.split('\n').filter((id) => id)
97
+ const ids = stdout.split('\n').filter(id => id)
119
98
 
120
99
  Promise.all(ids.map(getContainerPorts)).then((ps) => {
121
100
  const ports = [].concat(...ps)
@@ -125,23 +104,27 @@ const getOwnPorts = () =>
125
104
  })
126
105
 
127
106
  /**
128
- * Get own ports
107
+ * Find process listening to given port
129
108
  */
130
- const getContainerPorts = (containerId) =>
131
- new Promise((resolve, reject) => {
132
- exec(`docker port ${containerId}`, function (err, stdout, stderr) {
133
- if (err) return reject(err)
109
+ const getPID = port =>
110
+ new Promise((resolve) => {
111
+ exec(`lsof -nP -i:${port}`, (_, stdout) => {
112
+ // `lsof` already returns a non-zero exit code if it did not find any running
113
+ // process for the given port. Therefore we refrain from rejecting this Promise
114
+ // if an error was handed over.
134
115
 
135
- const errMessage = stderr.toString().trim()
136
- if (errMessage) return reject(Error(errMessage))
116
+ const process = stdout
117
+ .toString()
118
+ .split(/\n/)
119
+ .filter(r => r)
120
+ .map(r => r.split(/\s+/))
121
+ .find(r => (r[9] || '').match(/(LISTEN)/))
137
122
 
138
- const lines = stdout.split('\n').filter((l) => l)
139
- const ports = lines
140
- .map((l) => l.match(/.*:(\d+)/))
141
- .map((m) => (m.length >= 1 ? m[1] : null))
142
- .filter((p) => p)
123
+ if (!process) {
124
+ return resolve(null)
125
+ }
143
126
 
144
- resolve(ports)
127
+ resolve(process[1])
145
128
  })
146
129
  })
147
130
 
@@ -162,26 +145,39 @@ const getPIDs = async (ports) => {
162
145
  }
163
146
 
164
147
  /**
165
- * Find process listening to given port
148
+ * Get process details for given pid
166
149
  */
167
- const getPID = (port) =>
150
+ const getProcess = async pid =>
168
151
  new Promise((resolve, reject) => {
169
- exec(`lsof -nP -i:${port}`, function (_, stdout, stderr) {
170
- // `lsof` already returns a non-zero exit code if it did not find any running
171
- // process for the given port. Therefore we refrain from rejecting this Promise
172
- // if an error was handed over.
152
+ exec(
153
+ `ps -p ${pid} -ww -o pid,ppid,uid,gid,args`,
154
+ (err, stdout, stderr) => {
155
+ if (err) {
156
+ return reject(err)
157
+ }
173
158
 
174
- const process = stdout
175
- .toString()
176
- .split(/\n/)
177
- .filter((r) => r)
178
- .map((r) => r.split(/\s+/))
179
- .find((r) => (r[9] || '').match(/(LISTEN)/))
159
+ const errMessage = stderr.toString().trim()
160
+ if (errMessage) {
161
+ return reject(new Error(errMessage))
162
+ }
180
163
 
181
- if (!process) return resolve(null)
164
+ const processes = stdout
165
+ .toString()
166
+ .split(/\n/)
167
+ .slice(1) // skip headers
168
+ .filter(r => r)
169
+ .map(r => r.split(/\s+/))
170
+ .map(([pid, ppid, uid, gid, ...args]) => ({
171
+ pid,
172
+ ppid,
173
+ uid,
174
+ gid,
175
+ cmd: args.join(' ')
176
+ }))
182
177
 
183
- resolve(process[1])
184
- })
178
+ resolve(processes[0])
179
+ }
180
+ )
185
181
  })
186
182
 
187
183
  /**
@@ -201,33 +197,56 @@ const getProcesses = async (pids) => {
201
197
  }
202
198
 
203
199
  /**
204
- * Get process details for given pid
200
+ * Get paths to other running dev-service instances
205
201
  */
206
- const getProcess = async (pid) =>
207
- new Promise((resolve, reject) => {
208
- exec(
209
- `ps -p ${pid} -ww -o pid,ppid,uid,gid,args`,
210
- function (err, stdout, stderr) {
211
- if (err) return reject(err)
202
+ export const checkOtherServices = async () => {
203
+ const paths = (await getComposePaths())
204
+ .filter(p => p !== COMPOSE_DIR)
205
+ .filter(p => p.endsWith('/services/.compose'))
206
+ .map(p => p.replace(/\/services\/.compose$/, ''))
212
207
 
213
- const errMessage = stderr.toString().trim()
214
- if (errMessage) return reject(Error(errMessage))
208
+ if (paths.length > 0) {
209
+ warning(
210
+ [
211
+ 'dev-service is already running, started in following folder(s):',
212
+ ...paths.map(p => ` ${p}`)
213
+ ].join('\n')
214
+ )
215
+ }
216
+ }
215
217
 
216
- const processes = stdout
217
- .toString()
218
- .split(/\n/)
219
- .slice(1) // skip headers
220
- .filter((r) => r)
221
- .map((r) => r.split(/\s+/))
222
- .map(([pid, ppid, uid, gid, ...args]) => ({
223
- pid,
224
- ppid,
225
- uid,
226
- gid,
227
- cmd: args.join(' ')
228
- }))
218
+ /**
219
+ * check for processes using needed ports
220
+ */
221
+ export const checkUsedPorts = async (service) => {
222
+ if (!checkComposeDir()) {
223
+ throw new Error('No services found. Try running `service install`')
224
+ }
229
225
 
230
- resolve(processes[0])
231
- }
232
- )
233
- })
226
+ const requiredPorts = getRequiredPorts(service)
227
+ const ownPorts = await getOwnPorts()
228
+ const ports = requiredPorts.filter(p => !ownPorts.includes(p))
229
+
230
+ const portsToPids = await getPIDs(ports)
231
+
232
+ if (Object.keys(portsToPids).length === 0) {
233
+ // everything ok
234
+ return
235
+ }
236
+
237
+ const pids = [...new Set(Object.values(portsToPids))]
238
+ const pidsToProcesses = await getProcesses(pids)
239
+
240
+ throw new Error(
241
+ [
242
+ 'Required port(s) are already allocated:',
243
+ ...Object.entries(portsToPids).map(
244
+ ([port, pid]) =>
245
+ `- port ${port} is used by process with pid ${pid}${
246
+ pidsToProcesses[pid] && pidsToProcesses[pid].cmd
247
+ ? ` (${pidsToProcesses[pid].cmd})`
248
+ : ''}`
249
+ )
250
+ ].join('\n')
251
+ )
252
+ }
package/src/constants.js CHANGED
@@ -1,8 +1,9 @@
1
1
  'use strict'
2
2
 
3
- import path from 'path'
4
- import { createRequire } from 'module'
5
- import { fileURLToPath } from 'url'
3
+ import { createRequire } from 'node:module'
4
+ import path from 'node:path'
5
+ import process from 'node:process'
6
+ import { fileURLToPath } from 'node:url'
6
7
 
7
8
  const require = createRequire(import.meta.url)
8
9
  const __filename = fileURLToPath(import.meta.url)
@@ -14,6 +15,7 @@ const __dirname = path.dirname(__filename)
14
15
 
15
16
  // Dev-Service
16
17
  export const { version } = require('../package.json')
18
+
17
19
  export const TEMPLATES_DIR = path.resolve(__dirname, '../templates')
18
20
 
19
21
  // Project
package/src/install.js CHANGED
@@ -1,26 +1,26 @@
1
1
  'use strict'
2
2
 
3
- import path from 'path'
4
- import os from 'os'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
5
  import fs from 'fs-extra'
6
- import YAML from 'yaml'
7
- import parseJson from 'parse-json'
8
6
  import { customAlphabet } from 'nanoid'
7
+ import parseJson from 'parse-json'
8
+ import YAML from 'yaml'
9
9
 
10
10
  import {
11
- TEMPLATES_DIR,
12
- SERVICES_DIR,
13
- COMPOSE_DIR,
14
- VOLUMES_DIR
15
- } from './constants.js'
16
-
17
- import {
18
- readPackageJson,
19
- escape,
20
11
  docker,
12
+ escape,
13
+ readPackageJson,
21
14
  resetComposeDir
22
15
  } from '../src/utils.js'
23
16
 
17
+ import {
18
+ COMPOSE_DIR,
19
+ SERVICES_DIR,
20
+ TEMPLATES_DIR,
21
+ VOLUMES_DIR
22
+ } from './constants.js'
23
+
24
24
  const nanoid = customAlphabet(
25
25
  '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
26
26
  12
@@ -32,7 +32,7 @@ const OPTIONS_PATH = path.resolve(SERVICES_DIR, '.options')
32
32
  * Helper methods
33
33
  */
34
34
  const getName = (service) => {
35
- const withoutTag = service.replace(/:[a-z0-9_][a-z0-9_.-]{0,127}$/i, '')
35
+ const withoutTag = service.replace(/:\w[\w.-]{0,127}$/, '')
36
36
  const [name] = withoutTag.split('/').slice(-1)
37
37
 
38
38
  return name
@@ -59,9 +59,9 @@ const fillTemplate = (template, data, removeSections, keepSections) => {
59
59
  }
60
60
 
61
61
  const getOptions = () => {
62
- const raw =
63
- fs.existsSync(OPTIONS_PATH) &&
64
- fs.readFileSync(OPTIONS_PATH, { encoding: 'utf-8' })
62
+ const raw
63
+ = fs.existsSync(OPTIONS_PATH)
64
+ && fs.readFileSync(OPTIONS_PATH, { encoding: 'utf-8' })
65
65
 
66
66
  return raw ? parseJson(raw) : {}
67
67
  }
@@ -78,20 +78,26 @@ const ensureVolumesDir = async () => {
78
78
 
79
79
  const ensureNamedVolumes = async (content) => {
80
80
  const data = YAML.parse(content)
81
- if (!data || !data.volumes) return
81
+ if (!data || !data.volumes) {
82
+ return
83
+ }
82
84
 
83
85
  const volumes = []
84
86
  for (const key in data.volumes) {
85
- if (!data.volumes[key]) continue
87
+ if (!data.volumes[key]) {
88
+ continue
89
+ }
86
90
 
87
91
  const name = data.volumes[key].name
88
- if (!name) continue
92
+ if (!name) {
93
+ continue
94
+ }
89
95
 
90
96
  volumes.push(name)
91
97
  }
92
98
 
93
99
  await Promise.all(
94
- volumes.map((v) =>
100
+ volumes.map(v =>
95
101
  docker('volume', 'create', `--name=${v}`, '--label=keep')
96
102
  )
97
103
  )
@@ -112,14 +118,6 @@ const copyAdditionalFiles = (name) => {
112
118
  }
113
119
  }
114
120
 
115
- const readServiceData = (service) => {
116
- if (typeof service === 'string') {
117
- return readStandardServiceData(service)
118
- } else if (typeof service === 'object') {
119
- return readCustomServiceData(service)
120
- }
121
- }
122
-
123
121
  const readCustomServiceData = (service) => {
124
122
  const image = service.image
125
123
  const name = getName(image)
@@ -133,14 +131,18 @@ const readCustomServiceData = (service) => {
133
131
  const volumeArray = volume.split(':')
134
132
 
135
133
  // volume is unnamed:
136
- if (volumeArray.length === 1) continue
134
+ if (volumeArray.length === 1) {
135
+ continue
136
+ }
137
137
 
138
138
  // => volume is named or mapped to a host path:
139
139
  const [volumeName] = volumeArray
140
140
 
141
141
  // volume has invalid volume name / volume is mapped to a host path
142
142
  // (@see https://github.com/moby/moby/issues/21786):
143
- if (!volumeName.match(/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/)) continue
143
+ if (!volumeName.match(/^[a-z0-9][\w.-]+$/i)) {
144
+ continue
145
+ }
144
146
 
145
147
  // volume is named => we add it to top level "volumes" directive:
146
148
  volumes[volumeName] = {
@@ -172,14 +174,25 @@ const readStandardServiceData = (service) => {
172
174
  const result = { image: service, name }
173
175
 
174
176
  const exists = fs.existsSync(src)
175
- if (exists) result.template = fs.readFileSync(src, { encoding: 'utf8' })
177
+ if (exists) {
178
+ result.template = fs.readFileSync(src, { encoding: 'utf8' })
179
+ }
176
180
 
177
181
  return result
178
182
  }
179
183
 
184
+ const readServiceData = (service) => {
185
+ if (typeof service === 'string') {
186
+ return readStandardServiceData(service)
187
+ }
188
+ else if (typeof service === 'object') {
189
+ return readCustomServiceData(service)
190
+ }
191
+ }
192
+
180
193
  const serviceInstall = async (data, projectname, volumeType, volumesPrefix) => {
181
194
  const removeSections = ['mapped-volumes', 'named-volumes'].filter(
182
- (e) => e !== volumeType
195
+ e => e !== volumeType
183
196
  )
184
197
  const keepSections = [volumeType]
185
198
 
@@ -213,14 +226,14 @@ export const install = async (opts) => {
213
226
  const projectname = escape(name)
214
227
 
215
228
  // cleanse services from falsy values:
216
- const services = all.filter((s) => s)
229
+ const services = all.filter(s => s)
217
230
 
218
231
  // validate custom services:
219
- const invalid = services.filter((s) => typeof s === 'object' && !s.image)
232
+ const invalid = services.filter(s => typeof s === 'object' && !s.image)
220
233
  if (invalid.length > 0) {
221
- throw Error(
234
+ throw new Error(
222
235
  `Invalid custom services:\n${invalid
223
- .map((i) => JSON.stringify(i, null, 2))
236
+ .map(i => JSON.stringify(i, null, 2))
224
237
  .join(',\n')}`
225
238
  )
226
239
  }
@@ -229,9 +242,9 @@ export const install = async (opts) => {
229
242
  const data = services.map(readServiceData)
230
243
 
231
244
  // exit if not all services' images are supported:
232
- const unsupported = data.filter((d) => !d.template).map((d) => d.name)
245
+ const unsupported = data.filter(d => !d.template).map(d => d.name)
233
246
  if (unsupported.length > 0) {
234
- throw Error(`Unsupported services: ${unsupported.join(', ')}`)
247
+ throw new Error(`Unsupported services: ${unsupported.join(', ')}`)
235
248
  }
236
249
 
237
250
  // install services:
@@ -267,18 +280,20 @@ export const install = async (opts) => {
267
280
  if (options.volumes && options.volumes.mode === 'volumes-id') {
268
281
  const volumesPrefix = options.volumes.id
269
282
  await Promise.all(
270
- data.map((d) =>
283
+ data.map(d =>
271
284
  serviceInstall(d, projectname, 'named-volumes', volumesPrefix)
272
285
  )
273
286
  )
274
- } else if (options.volumes && options.volumes.mode === 'mapped-volumes') {
287
+ }
288
+ else if (options.volumes && options.volumes.mode === 'mapped-volumes') {
275
289
  await Promise.all(
276
- data.map((d) => serviceInstall(d, projectname, 'mapped-volumes'))
290
+ data.map(d => serviceInstall(d, projectname, 'mapped-volumes'))
277
291
  )
278
- } else {
292
+ }
293
+ else {
279
294
  const volumesPrefix = projectname
280
295
  await Promise.all(
281
- data.map((d) =>
296
+ data.map(d =>
282
297
  serviceInstall(d, projectname, 'named-volumes', volumesPrefix)
283
298
  )
284
299
  )
package/src/status.js ADDED
@@ -0,0 +1,63 @@
1
+ 'use strict'
2
+
3
+ import { exec } from 'node:child_process'
4
+ import chalk from 'chalk'
5
+ import { getComposeCommand } from './utils.js'
6
+
7
+ const execOutput = cmd =>
8
+ new Promise((resolve) => {
9
+ exec(cmd, (err, stdout) => {
10
+ resolve(err ? null : stdout.trim())
11
+ })
12
+ })
13
+
14
+ export const status = async () => {
15
+ const preferredCmd = await getComposeCommand()
16
+
17
+ const [
18
+ dockerPath,
19
+ dockerVersion,
20
+ composePluginVersion,
21
+ dockerComposePath,
22
+ dockerComposeVersion
23
+ ] = await Promise.all([
24
+ execOutput('which docker'),
25
+ execOutput('docker --version'),
26
+ execOutput('docker compose version --short'),
27
+ execOutput('which docker-compose'),
28
+ execOutput('docker-compose --version')
29
+ ])
30
+
31
+ console.log('\nDetected tools:\n')
32
+
33
+ if (dockerPath) {
34
+ console.log(` docker ${chalk.dim(dockerPath)}`)
35
+ console.log(` ${dockerVersion || chalk.yellow('version unknown')}`)
36
+ }
37
+ else {
38
+ console.log(` docker ${chalk.red('not found')}`)
39
+ }
40
+
41
+ console.log('')
42
+
43
+ const pluginActive = preferredCmd === 'docker compose'
44
+
45
+ if (composePluginVersion) {
46
+ const label = pluginActive ? chalk.green(' [active]') : ''
47
+ console.log(` docker compose plugin ${composePluginVersion}${label}`)
48
+ }
49
+ else {
50
+ console.log(` docker compose ${chalk.dim('not available')}`)
51
+ }
52
+
53
+ if (dockerComposePath) {
54
+ const label = !pluginActive ? chalk.green(' [active]') : ''
55
+ console.log(` docker-compose ${chalk.dim(dockerComposePath)}`)
56
+ console.log(` ${dockerComposeVersion || chalk.yellow('version unknown')}${label}`)
57
+ }
58
+ else {
59
+ console.log(` docker-compose ${chalk.dim('not found')}`)
60
+ }
61
+
62
+ console.log('')
63
+ }