@flowfuse/driver-docker 2.14.1 → 2.16.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/.github/workflows/build.yml +2 -2
- package/.github/workflows/release-publish.yml +3 -3
- package/CHANGELOG.md +12 -0
- package/README.md +1 -3
- package/docker.js +175 -22
- package/package.json +4 -4
|
@@ -13,9 +13,9 @@ jobs:
|
|
|
13
13
|
matrix:
|
|
14
14
|
node-version: [16.x]
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
17
17
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
18
|
-
uses: actions/setup-node@v4
|
|
18
|
+
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
|
19
19
|
with:
|
|
20
20
|
node-version: ${{ matrix.node-version }}
|
|
21
21
|
- name: Install Dependencies
|
|
@@ -8,11 +8,11 @@ jobs:
|
|
|
8
8
|
publish:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@v4
|
|
12
|
-
- uses: actions/setup-node@v4
|
|
11
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
12
|
+
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
|
13
13
|
with:
|
|
14
14
|
node-version: 18
|
|
15
15
|
- run: npm ci --omit dev
|
|
16
|
-
- uses: JS-DevTools/npm-publish@v3
|
|
16
|
+
- uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c # v3.1.1
|
|
17
17
|
with:
|
|
18
18
|
token: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
#### 2.16.0: Release
|
|
2
|
+
|
|
3
|
+
- chore: fix lint script (#128) @ppawlowski
|
|
4
|
+
- Bump tar-fs and dockerode (#127) @app/dependabot
|
|
5
|
+
- chore: Pin external actions to commit hash (#126) @ppawlowski
|
|
6
|
+
|
|
7
|
+
#### 2.15.0: Release
|
|
8
|
+
|
|
9
|
+
- docs: Remove unused configuration keyfrom README.md (#124) @ppawlowski
|
|
10
|
+
- Mqtt agent support for Docker (#121) @hardillb
|
|
11
|
+
- feat: Use docker volume as persistent storage backend for project instances (#123) @ppawlowski
|
|
12
|
+
|
|
1
13
|
#### 2.14.1: Release
|
|
2
14
|
|
|
3
15
|
|
package/README.md
CHANGED
|
@@ -17,15 +17,13 @@ driver:
|
|
|
17
17
|
logPassthrough: true
|
|
18
18
|
storage:
|
|
19
19
|
enabled: true
|
|
20
|
-
path: /opt/flowfuse/storage
|
|
21
20
|
```
|
|
22
21
|
|
|
23
22
|
- `registry` is the Docker Registry to load Stack Containers from (default: Docker Hub)
|
|
24
23
|
- `socket` is the path to the docker unix domain socket (default: /var/run/docker.sock)
|
|
25
24
|
- `privateCA`: is the fully qualified path to a pem file containing trusted CA cert chain (default: not set)
|
|
26
25
|
- `logPassthrough` Have Node-RED logs printed in JSON format to container stdout (default: false)
|
|
27
|
-
- `storage.enabled` enables
|
|
28
|
-
- `storage.path` is the fully qualified path to the root directory for the storage on the host (default: not set)
|
|
26
|
+
- `storage.enabled` enables volume-based persistent storage for Node-RED instance (default: false)
|
|
29
27
|
|
|
30
28
|
### Configuration via environment variables
|
|
31
29
|
|
package/docker.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const got = require('got')
|
|
2
2
|
const FormData = require('form-data')
|
|
3
3
|
const Docker = require('dockerode')
|
|
4
|
-
const path = require('path')
|
|
5
|
-
const { chownSync, mkdirSync, rmSync } = require('fs')
|
|
6
4
|
|
|
7
5
|
const createContainer = async (project, domain) => {
|
|
8
6
|
const stack = project.ProjectStack.properties
|
|
@@ -105,22 +103,26 @@ const createContainer = async (project, domain) => {
|
|
|
105
103
|
contOptions.Env.push('NODE_EXTRA_CA_CERTS=/usr/local/ssl-certs/chain.pem')
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
if (this._app.config.driver.options?.storage?.enabled
|
|
106
|
+
if (this._app.config.driver.options?.storage?.enabled) {
|
|
109
107
|
try {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
const volumeName = `flowfuse-${project.id}-storage`
|
|
109
|
+
await this._docker.createVolume({
|
|
110
|
+
Name: volumeName,
|
|
111
|
+
Labels: {
|
|
112
|
+
flowfuse: 'project-storage',
|
|
113
|
+
'project-id': project.id
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(contOptions.HostConfig?.Binds)) {
|
|
118
|
+
contOptions.HostConfig.Binds.push(`${volumeName}:/data/storage`)
|
|
119
|
+
} else {
|
|
120
|
+
contOptions.HostConfig.Binds = [
|
|
121
|
+
`${volumeName}:/data/storage`
|
|
122
|
+
]
|
|
123
|
+
}
|
|
114
124
|
} catch (err) {
|
|
115
|
-
this._app.log.
|
|
116
|
-
}
|
|
117
|
-
const projectPath = path.join(this._app.config.driver.options?.storage?.path, project.id)
|
|
118
|
-
if (Array.isArray(contOptions.HostConfig?.Binds)) {
|
|
119
|
-
contOptions.HostConfig.Binds.push(`${projectPath}:/data/storage`)
|
|
120
|
-
} else {
|
|
121
|
-
contOptions.HostConfig.Binds = [
|
|
122
|
-
`${projectPath}:/data/storage`
|
|
123
|
-
]
|
|
125
|
+
this._app.log.error(`[docker] problem creating storage volume for ${project.id}: ${err.message}`)
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -178,6 +180,74 @@ const getStaticFileUrl = async (instance, filePath) => {
|
|
|
178
180
|
return `http://${instance.id}:2880/flowforge/files/_/${encodeURIComponent(filePath)}`
|
|
179
181
|
}
|
|
180
182
|
|
|
183
|
+
const createMQttTopicAgent = async (broker) => {
|
|
184
|
+
const image = this._app.config.driver.options?.mqttSchemaContainer || `${this._app.config.driver.options?.registry ? this._app.config.driver.options.registry + '/' : ''}flowfuse/mqtt-schema-agent`
|
|
185
|
+
const name = `mqtt-schema-agent-${broker.Team.hashid.toLowerCase()}-${broker.hashid.toLowerCase()}`
|
|
186
|
+
const contOptions = {
|
|
187
|
+
Image: image,
|
|
188
|
+
name,
|
|
189
|
+
Env: [],
|
|
190
|
+
Labels: {
|
|
191
|
+
flowforge: 'mqtt-agent'
|
|
192
|
+
},
|
|
193
|
+
AttachStdin: false,
|
|
194
|
+
AttachStdout: false,
|
|
195
|
+
AttachStderr: false,
|
|
196
|
+
HostConfig: {
|
|
197
|
+
NetworkMode: this._network,
|
|
198
|
+
RestartPolicy: {
|
|
199
|
+
Name: 'unless-stopped'
|
|
200
|
+
},
|
|
201
|
+
NanoCpus: ((10 / 100) * (10 ** 9)), // 10%
|
|
202
|
+
Memory: (100 * 1024 * 1024) // 100mb
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { token } = await broker.refreshAuthTokens()
|
|
207
|
+
contOptions.Env.push(`FORGE_TEAM_TOKEN=${token}`)
|
|
208
|
+
contOptions.Env.push(`FORGE_URL=${this._app.config.api_url}`)
|
|
209
|
+
contOptions.Env.push(`FORGE_BROKER_ID=${broker.hashid}`)
|
|
210
|
+
contOptions.Env.push(`FORGE_TEAM_ID=${broker.Team.hashid}`)
|
|
211
|
+
|
|
212
|
+
const containerList = await this._docker.listImages()
|
|
213
|
+
let containerFound = false
|
|
214
|
+
let stackName = image
|
|
215
|
+
if (stackName.indexOf(':') === -1) {
|
|
216
|
+
stackName = stackName + ':latest'
|
|
217
|
+
}
|
|
218
|
+
for (const cont of containerList) {
|
|
219
|
+
if (cont.RepoTags.includes(stackName)) {
|
|
220
|
+
containerFound = true
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!containerFound) {
|
|
225
|
+
this._app.log.info(`Container for MQTT Schema Agent not found, pulling ${stackName}`)
|
|
226
|
+
try {
|
|
227
|
+
await new Promise((resolve, reject) => {
|
|
228
|
+
this._docker.pull(stackName, (err, stream) => {
|
|
229
|
+
if (!err) {
|
|
230
|
+
this._docker.modem.followProgress(stream, onFinished)
|
|
231
|
+
function onFinished (err, output) {
|
|
232
|
+
if (!err) {
|
|
233
|
+
resolve(true)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
reject(err)
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
reject(err)
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
} catch (err) {
|
|
244
|
+
this._app.log.debug(`Error pulling image ${stackName} ${err.message}`)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const container = await this._docker.createContainer(contOptions)
|
|
248
|
+
await container.start()
|
|
249
|
+
}
|
|
250
|
+
|
|
181
251
|
/**
|
|
182
252
|
* Docker Container driver
|
|
183
253
|
*
|
|
@@ -254,7 +324,7 @@ module.exports = {
|
|
|
254
324
|
}
|
|
255
325
|
})
|
|
256
326
|
|
|
257
|
-
this._initialCheckTimeout = setTimeout(() => {
|
|
327
|
+
this._initialCheckTimeout = setTimeout(async () => {
|
|
258
328
|
this._app.log.debug('[docker] Restarting projects')
|
|
259
329
|
projects.forEach(async (project) => {
|
|
260
330
|
try {
|
|
@@ -301,6 +371,50 @@ module.exports = {
|
|
|
301
371
|
this._app.log.error(`[docker] Project ${project.id} - error resuming project: ${err.stack}`)
|
|
302
372
|
}
|
|
303
373
|
})
|
|
374
|
+
|
|
375
|
+
if (this._app.db.models.BrokerCredentials) {
|
|
376
|
+
const brokers = await this._app.db.models.BrokerCredentials.findAll({
|
|
377
|
+
include: [{ model: this._app.db.models.Team }]
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
brokers.forEach(async (broker) => {
|
|
381
|
+
if (broker.Team) {
|
|
382
|
+
if (broker.state === 'running') {
|
|
383
|
+
const name = `mqtt-schema-agent-${broker.Team.hashid.toLowerCase()}-${broker.hashid.toLowerCase()}`
|
|
384
|
+
this._app.log.info(`[docker] Testing MQTT Agent ${name} container exists`)
|
|
385
|
+
this._app.log.debug(`${name}`)
|
|
386
|
+
let container
|
|
387
|
+
try {
|
|
388
|
+
container = await this._docker.listContainers({
|
|
389
|
+
all: true,
|
|
390
|
+
filters: {
|
|
391
|
+
name: [name]
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
if (container[0]) {
|
|
395
|
+
container = await this._docker.getContainer(container[0].Id)
|
|
396
|
+
} else {
|
|
397
|
+
container = undefined
|
|
398
|
+
}
|
|
399
|
+
if (container) {
|
|
400
|
+
const state = await container.inspect()
|
|
401
|
+
if (!state.State.Running) {
|
|
402
|
+
this._app.log.info(`[docker] MQTT Agent ${name} - restarting container [${container.id.substring(0, 12)}]`)
|
|
403
|
+
await container.start()
|
|
404
|
+
} else {
|
|
405
|
+
this._app.log.info(`[docker] MQTT Agent ${name} - already running container [${container.id.substring(0, 12)}]`)
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
this._app.log.info(`[docker] MQTT Agent ${name} - recreating container`)
|
|
409
|
+
createMQttTopicAgent(broker)
|
|
410
|
+
}
|
|
411
|
+
} catch (err) {
|
|
412
|
+
console.log(err)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
}
|
|
304
418
|
}, 1000)
|
|
305
419
|
|
|
306
420
|
return {
|
|
@@ -372,13 +486,12 @@ module.exports = {
|
|
|
372
486
|
} catch (err) {}
|
|
373
487
|
}
|
|
374
488
|
if (this._app.config.driver.options?.storage?.enabled) {
|
|
375
|
-
// need to be sure we have permission to delete the dir and it's contents?
|
|
376
489
|
try {
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
490
|
+
const volumeName = `flowfuse-${project.id}-storage`
|
|
491
|
+
const volume = this._docker.getVolume(volumeName)
|
|
492
|
+
await volume.remove()
|
|
380
493
|
} catch (err) {
|
|
381
|
-
this._app.log.error(`[docker] Project ${project.id} - error
|
|
494
|
+
this._app.log.error(`[docker] Project ${project.id} - error removing storage volume: ${err.stack}`)
|
|
382
495
|
}
|
|
383
496
|
}
|
|
384
497
|
delete this._projects[project.id]
|
|
@@ -612,5 +725,45 @@ module.exports = {
|
|
|
612
725
|
err.statusCode = err.response.statusCode
|
|
613
726
|
throw err
|
|
614
727
|
}
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
// Broker Agent
|
|
731
|
+
startBrokerAgent: async (broker) => {
|
|
732
|
+
createMQttTopicAgent(broker)
|
|
733
|
+
},
|
|
734
|
+
stopBrokerAgent: async (broker) => {
|
|
735
|
+
const name = `mqtt-schema-agent-${broker.Team.hashid.toLowerCase()}-${broker.hashid.toLowerCase()}`
|
|
736
|
+
try {
|
|
737
|
+
const container = await this._docker.getContainer(name)
|
|
738
|
+
await container.stop()
|
|
739
|
+
await container.remove()
|
|
740
|
+
} catch (err) {
|
|
741
|
+
console.log(err)
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
getBrokerAgentState: async (broker) => {
|
|
745
|
+
const name = `mqtt-schema-agent-${broker.Team.hashid.toLowerCase()}-${broker.hashid.toLowerCase()}`
|
|
746
|
+
try {
|
|
747
|
+
const status = await got.get(`http://${name}:3500/api/v1/status`).json()
|
|
748
|
+
return status
|
|
749
|
+
} catch (err) {
|
|
750
|
+
return { error: 'error_getting_status', message: err.toString() }
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
sendBrokerAgentCommand: async (broker, command) => {
|
|
754
|
+
const name = `mqtt-schema-agent-${broker.Team.hashid.toLowerCase()}-${broker.hashid.toLowerCase()}`
|
|
755
|
+
if (command === 'start' || command === 'restart') {
|
|
756
|
+
try {
|
|
757
|
+
await got.post(`http://${name}:3500/api/v1/commands/start`)
|
|
758
|
+
} catch (err) {
|
|
759
|
+
|
|
760
|
+
}
|
|
761
|
+
} else if (command === 'stop') {
|
|
762
|
+
try {
|
|
763
|
+
await got.post(`http://${name}:3500/api/v1/commands/stop`)
|
|
764
|
+
} catch (err) {
|
|
765
|
+
|
|
766
|
+
}
|
|
767
|
+
}
|
|
615
768
|
}
|
|
616
769
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowfuse/driver-docker",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Docker driver for FlowFuse",
|
|
5
5
|
"main": "docker.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
-
"lint": "eslint -c .eslintrc *.js",
|
|
9
|
-
"lint:fix": "eslint -c .eslintrc *.js --fix"
|
|
8
|
+
"lint": "eslint -c .eslintrc \"*.js\"",
|
|
9
|
+
"lint:fix": "eslint -c .eslintrc \"*.js\" --fix"
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"license": "Apache-2.0",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"dockerode": "^
|
|
24
|
+
"dockerode": "^4.0.5",
|
|
25
25
|
"form-data": "^4.0.0",
|
|
26
26
|
"got": "^11.8.0"
|
|
27
27
|
},
|