@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/README.md +88 -8
- package/bin/cli-check.js +6 -3
- package/bin/cli-install.js +5 -3
- package/bin/cli-list.js +3 -1
- package/bin/cli-logs.js +5 -2
- package/bin/cli-pull.js +5 -2
- package/bin/cli-restart.js +5 -2
- package/bin/cli-start.js +6 -3
- package/bin/cli-status.js +19 -0
- package/bin/cli-stop.js +5 -2
- package/bin/cli.js +3 -1
- package/package.json +27 -22
- package/src/check.js +137 -118
- package/src/constants.js +5 -3
- package/src/install.js +59 -44
- package/src/status.js +63 -0
- package/src/utils.js +63 -28
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)
|
|
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(
|
|
85
|
-
.filter(
|
|
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(
|
|
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(' ')}`,
|
|
113
|
-
if (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)
|
|
93
|
+
if (errMessage) {
|
|
94
|
+
return reject(new Error(errMessage))
|
|
95
|
+
}
|
|
117
96
|
|
|
118
|
-
const ids = stdout.split('\n').filter(
|
|
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
|
-
*
|
|
107
|
+
* Find process listening to given port
|
|
129
108
|
*/
|
|
130
|
-
const
|
|
131
|
-
new Promise((resolve
|
|
132
|
-
exec(`
|
|
133
|
-
if
|
|
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
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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(
|
|
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
|
-
*
|
|
148
|
+
* Get process details for given pid
|
|
166
149
|
*/
|
|
167
|
-
const
|
|
150
|
+
const getProcess = async pid =>
|
|
168
151
|
new Promise((resolve, reject) => {
|
|
169
|
-
exec(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
200
|
+
* Get paths to other running dev-service instances
|
|
205
201
|
*/
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
|
4
|
-
import
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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])
|
|
87
|
+
if (!data.volumes[key]) {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
86
90
|
|
|
87
91
|
const name = data.volumes[key].name
|
|
88
|
-
if (!name)
|
|
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(
|
|
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)
|
|
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-
|
|
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)
|
|
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
|
-
|
|
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(
|
|
229
|
+
const services = all.filter(s => s)
|
|
217
230
|
|
|
218
231
|
// validate custom services:
|
|
219
|
-
const invalid = services.filter(
|
|
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(
|
|
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(
|
|
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(
|
|
283
|
+
data.map(d =>
|
|
271
284
|
serviceInstall(d, projectname, 'named-volumes', volumesPrefix)
|
|
272
285
|
)
|
|
273
286
|
)
|
|
274
|
-
}
|
|
287
|
+
}
|
|
288
|
+
else if (options.volumes && options.volumes.mode === 'mapped-volumes') {
|
|
275
289
|
await Promise.all(
|
|
276
|
-
data.map(
|
|
290
|
+
data.map(d => serviceInstall(d, projectname, 'mapped-volumes'))
|
|
277
291
|
)
|
|
278
|
-
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
279
294
|
const volumesPrefix = projectname
|
|
280
295
|
await Promise.all(
|
|
281
|
-
data.map(
|
|
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
|
+
}
|