@flowfuse/driver-docker 2.14.0 → 2.15.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/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ #### 2.15.0: Release
2
+
3
+ - docs: Remove unused configuration keyfrom README.md (#124) @ppawlowski
4
+ - Mqtt agent support for Docker (#121) @hardillb
5
+ - feat: Use docker volume as persistent storage backend for project instances (#123) @ppawlowski
6
+
7
+ #### 2.14.1: Release
8
+
9
+
1
10
  #### 2.14.0: Release
2
11
 
3
12
 
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 mounting a directory into each Node-RED instance as persistence storage (default: false)
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 && this._app.config.driver.options?.storage?.path) {
106
+ if (this._app.config.driver.options?.storage?.enabled) {
109
107
  try {
110
- const localPath = path.join('/opt/persistent-storage', project.id)
111
- console.log(`Creating dir in container ${localPath}`)
112
- mkdirSync(localPath)
113
- chownSync(localPath, 1000, 1000)
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.info(`[docker] problem creating persistent storage for ${project.id}`)
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
- // This is better and assumes that directory is mounted on `/opt/storage`
378
- const projectPersistentPath = path.join('/opt/persistent-storage', project.id)
379
- rmSync(projectPersistentPath, { recursive: true, force: true })
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 deleting persistent storage: ${err.stack}`)
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowfuse/driver-docker",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "Docker driver for FlowFuse",
5
5
  "main": "docker.js",
6
6
  "scripts": {