@flowfuse/driver-kubernetes 2.4.1-c425d12-202406041337.0 → 2.5.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.
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - uses: actions/checkout@v4
12
- - uses: actions/setup-node@v1
12
+ - uses: actions/setup-node@v4
13
13
  with:
14
14
  node-version: 16
15
15
  - run: npm ci
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ #### 2.5.0: Release
2
+
3
+ - Custom Hostname support (#151) @hardillb
4
+ - Bump actions/setup-node from 1 to 4 (#159) @app/dependabot
5
+ - Bump actions/checkout from 1 to 4 (#158) @app/dependabot
6
+ - Enable dependabot for github actions (#154) @ppawlowski
7
+
1
8
  #### 2.4.0: Release
2
9
 
3
10
  - Revert "Add support for user set health check interval" (#152) @Steve-Mcl
package/README.md CHANGED
@@ -21,6 +21,11 @@ driver:
21
21
  k8sDelay: 1000
22
22
  k8sRetries: 10
23
23
  logPassthrough: true
24
+ customHostname:
25
+ enabled: true
26
+ cnameTarget: custom-loadbalancer.example.com
27
+ certManagerIssuer: lets-encrypt
28
+ ingressClass: custom-nginx
24
29
  ```
25
30
 
26
31
  - `registry` is the Docker Registry to load Stack Containers from
@@ -34,6 +39,11 @@ AWS EKS specific annotation for ALB Ingress. or `openshift` to allow running on
34
39
  - `k8sRetries` how many times to retry actions against the K8s API
35
40
  - `k8sDelay` how long to wait (in ms) between retries to the K8s API
36
41
  - `logPassthrough` Have Node-RED logs printed in JSON format to container stdout (default false)
42
+ - `customHostname` Settings linked to allowing instances to have a second hostname
43
+ - `customHostname.enabled` (default false)
44
+ - `customHostname.cnameTarget` The hostname users should configure their DNS entries to point at. Required. (default not set)
45
+ - `customHostname.certManagerIssuer` Name of the Cluster issuer to use to create HTTPS certs for the custom hostname (default not set)
46
+ - `customHostname.ingressClass` Name of the IngressClass to use to expose the custom hostname (default not set)
37
47
 
38
48
  Expects to pick up K8s credentials from the environment
39
49
 
package/kubernetes.js CHANGED
@@ -131,6 +131,36 @@ const ingressTemplate = {
131
131
  }
132
132
  }
133
133
 
134
+ const customIngressTemplate = {
135
+ apiVersion: 'networking.k8s.io/v1',
136
+ kind: 'Ingress',
137
+ metadata: {
138
+ annotations: {}
139
+ },
140
+ spec: {
141
+ rules: [
142
+ {
143
+ http: {
144
+ paths: [
145
+ {
146
+ pathType: 'Prefix',
147
+ path: '/',
148
+ backend: {
149
+ service: {
150
+ port: { number: 1880 }
151
+ }
152
+ }
153
+ }
154
+ ]
155
+ }
156
+ }
157
+ ],
158
+ tls: [
159
+
160
+ ]
161
+ }
162
+ }
163
+
134
164
  const createDeployment = async (project, options) => {
135
165
  const stack = project.ProjectStack.properties
136
166
 
@@ -340,6 +370,50 @@ const createIngress = async (project, options) => {
340
370
  return localIngress
341
371
  }
342
372
 
373
+ const createCustomIngress = async (project, hostname, options) => {
374
+ const prefix = project.safeName.match(/^[0-9]/) ? 'srv-' : ''
375
+ const url = new URL(project.url)
376
+ url.host = hostname
377
+
378
+ // exposedData available for annotation replacements
379
+ const exposedData = {
380
+ serviceName: `${prefix}${project.safeName}`,
381
+ instanceURL: url.href,
382
+ instanceHost: url.host,
383
+ instanceProtocol: url.protocol
384
+ }
385
+
386
+ this._app.log.info('K8S DRIVER: start custom hostname ingress template')
387
+ const customIngress = JSON.parse(JSON.stringify(customIngressTemplate))
388
+
389
+ customIngress.metadata.name = `${project.safeName}-custom`
390
+ customIngress.spec.rules[0].host = hostname
391
+ customIngress.spec.rules[0].http.paths[0].backend.service.name = `${prefix}${project.safeName}`
392
+
393
+ if (this._customHostname?.certManagerIssuer) {
394
+ customIngress.metadata.annotations['cert-manager.io/cluster-issuer'] = this._customHostname.certManagerIssuer
395
+ customIngress.spec.tls = [
396
+ {
397
+ hosts: [
398
+ hostname
399
+ ],
400
+ secretName: `${project.safeName}-custom`
401
+ }
402
+ ]
403
+ }
404
+
405
+ // process annotations with potential replacements
406
+ Object.keys(customIngress.metadata.annotations).forEach((key) => {
407
+ customIngress.metadata.annotations[key] = mustache(customIngress.metadata.annotations[key], exposedData)
408
+ })
409
+
410
+ if (this._customHostname?.ingressClass) {
411
+ customIngress.spec.ingressClassName = `${this._customHostname.ingressClass}`
412
+ }
413
+
414
+ return customIngress
415
+ }
416
+
343
417
  const createProject = async (project, options) => {
344
418
  const namespace = this._app.config.driver.options.projectNamespace || 'flowforge'
345
419
 
@@ -352,7 +426,7 @@ const createProject = async (project, options) => {
352
426
  } catch (err) {
353
427
  if (err.statusCode === 409) {
354
428
  // If deployment exists, perform an upgrade
355
- this._app.log.warn(`[k8s] Deployment for project ${project.id} already exists. Upgrading deployment`)
429
+ this._app.log.warn(`[k8s] Deployment for instance ${project.id} already exists. Upgrading deployment`)
356
430
  const result = await this._k8sAppApi.readNamespacedDeployment(project.safeName, namespace)
357
431
 
358
432
  const existingDeployment = result.body
@@ -363,7 +437,7 @@ const createProject = async (project, options) => {
363
437
  }
364
438
  } else {
365
439
  // Log other errors and rethrow them for additional higher-level handling
366
- this._app.log.error(`[k8s] Unexpected error creating deployment for project ${project.id}.`)
440
+ this._app.log.error(`[k8s] Unexpected error creating deployment for instance ${project.id}.`)
367
441
  this._app.log.error(`[k8s] deployment ${JSON.stringify(localDeployment, undefined, 2)}`)
368
442
  this._app.log.error(err)
369
443
  // rethrow the error so the wrapper knows this hasn't worked
@@ -383,7 +457,7 @@ const createProject = async (project, options) => {
383
457
  counter++
384
458
  if (counter > this._k8sRetries) {
385
459
  clearInterval(pollInterval)
386
- this._app.log.error(`[k8s] Project ${project.id} - timeout waiting for Deployment`)
460
+ this._app.log.error(`[k8s] Instance ${project.id} - timeout waiting for Deployment`)
387
461
  reject(new Error('Timed out to creating Deployment'))
388
462
  }
389
463
  }
@@ -394,10 +468,10 @@ const createProject = async (project, options) => {
394
468
  await this._k8sApi.createNamespacedService(namespace, localService)
395
469
  } catch (err) {
396
470
  if (err.statusCode === 409) {
397
- this._app.log.warn(`[k8s] Service for project ${project.id} already exists, proceeding...`)
471
+ this._app.log.warn(`[k8s] Service for instance ${project.id} already exists, proceeding...`)
398
472
  } else {
399
473
  if (project.state !== 'suspended') {
400
- this._app.log.error(`[k8s] Project ${project.id} - error creating service: ${err.toString()}`)
474
+ this._app.log.error(`[k8s] Instance ${project.id} - error creating service: ${err.toString()}`)
401
475
  throw err
402
476
  }
403
477
  }
@@ -415,7 +489,7 @@ const createProject = async (project, options) => {
415
489
  counter++
416
490
  if (counter > this._k8sRetries) {
417
491
  clearInterval(pollInterval)
418
- this._app.log.error(`[k8s] Project ${project.id} - timeout waiting for Service`)
492
+ this._app.log.error(`[k8s] Instance ${project.id} - timeout waiting for Service`)
419
493
  reject(new Error('Timed out to creating Service'))
420
494
  }
421
495
  }
@@ -426,14 +500,32 @@ const createProject = async (project, options) => {
426
500
  await this._k8sNetApi.createNamespacedIngress(namespace, localIngress)
427
501
  } catch (err) {
428
502
  if (err.statusCode === 409) {
429
- this._app.log.warn(`[k8s] Ingress for project ${project.id} already exists, proceeding...`)
503
+ this._app.log.warn(`[k8s] Ingress for instance ${project.id} already exists, proceeding...`)
430
504
  } else {
431
505
  if (project.state !== 'suspended') {
432
- this._app.log.error(`[k8s] Project ${project.id} - error creating ingress: ${err.toString()}`)
506
+ this._app.log.error(`[k8s] Instance ${project.id} - error creating ingress: ${err.toString()}`)
433
507
  throw err
434
508
  }
435
509
  }
436
510
  }
511
+ if (this._customHostname?.enabled) {
512
+ const customHostname = await project.getSetting('customHostname')
513
+ if (customHostname) {
514
+ const customHostnameIngress = await createCustomIngress(project, customHostname, options)
515
+ try {
516
+ await this._k8sNetApi.createNamespacedIngress(namespace, customHostnameIngress)
517
+ } catch (err) {
518
+ if (err.statusCode === 409) {
519
+ this._app.log.warn(`[k8s] Custom Hostname Ingress for instance ${project.id} already exists, proceeding...`)
520
+ } else {
521
+ if (project.state !== 'suspended') {
522
+ this._app.log.error(`[k8s] Instance ${project.id} - error creating custom hostname ingress: ${err.toString()}`)
523
+ throw err
524
+ }
525
+ }
526
+ }
527
+ }
528
+ }
437
529
 
438
530
  await new Promise((resolve, reject) => {
439
531
  let counter = 0
@@ -446,7 +538,7 @@ const createProject = async (project, options) => {
446
538
  counter++
447
539
  if (counter > this._k8sRetries) {
448
540
  clearInterval(pollInterval)
449
- this._app.log.error(`[k8s] Project ${project.id} - timeout waiting for Ingress`)
541
+ this._app.log.error(`[k8s] Instance ${project.id} - timeout waiting for Ingress`)
450
542
  reject(new Error('Timed out to creating Ingress'))
451
543
  }
452
544
  }
@@ -479,11 +571,11 @@ const getEndpoints = async (project) => {
479
571
 
480
572
  module.exports = {
481
573
  /**
482
- * Initialises this driver
483
- * @param {string} app - the Vue application
484
- * @param {object} options - A set of configuration options for the driver
485
- * @return {forge.containers.ProjectArguments}
486
- */
574
+ * Initialises this driver
575
+ * @param {string} app - the Vue application
576
+ * @param {object} options - A set of configuration options for the driver
577
+ * @return {forge.containers.ProjectArguments}
578
+ */
487
579
  init: async (app, options) => {
488
580
  this._app = app
489
581
  this._projects = {}
@@ -495,6 +587,10 @@ module.exports = {
495
587
  this._certManagerIssuer = this._app.config.driver.options?.certManagerIssuer
496
588
  this._logPassthrough = this._app.config.driver.options?.logPassthrough || false
497
589
  this._cloudProvider = this._app.config.driver.options?.cloudProvider
590
+ if (this._app.config.driver.options?.customHostname?.enabled) {
591
+ this._app.log.info('[k8s] Enabling Custom Hostname Support')
592
+ this._customHostname = this._app.config.driver.options?.customHostname
593
+ }
498
594
 
499
595
  const kc = new k8s.KubeConfig()
500
596
 
@@ -570,7 +666,7 @@ module.exports = {
570
666
  this._app.log.info(`[k8s] deployment ${project.id} in ${namespace} found`)
571
667
  } catch (err) {
572
668
  this._app.log.error(`[k8s] Error while reading namespaced deployment for project '${project.safeName}' ${project.id}. Error msg=${err.message}, stack=${err.stack}`)
573
- this._app.log.info(`[k8s] Project ${project.id} - recreating deployment`)
669
+ this._app.log.info(`[k8s] Instance ${project.id} - recreating deployment`)
574
670
  const fullProject = await this._app.db.models.Project.byId(project.id)
575
671
  await createProject(fullProject, options)
576
672
  }
@@ -581,13 +677,13 @@ module.exports = {
581
677
  await this._k8sApi.readNamespacedPodStatus(project.safeName, namespace)
582
678
  this._app.log.info(`[k8s] pod ${project.id} in ${namespace} found`)
583
679
  } catch (err) {
584
- this._app.log.debug(`[k8s] Project ${project.id} - recreating deployment`)
680
+ this._app.log.debug(`[k8s] Instance ${project.id} - recreating deployment`)
585
681
  const fullProject = await this._app.db.models.Project.byId(project.id)
586
682
  await createProject(fullProject, options)
587
683
  }
588
684
  }
589
685
  } catch (err) {
590
- this._app.log.error(`[k8s] Project ${project.id} - error resuming project: ${err.stack}`)
686
+ this._app.log.error(`[k8s] Instance ${project.id} - error resuming project: ${err.stack}`)
591
687
  }
592
688
  })
593
689
  }, 1000)
@@ -652,14 +748,30 @@ module.exports = {
652
748
  try {
653
749
  await this._k8sNetApi.deleteNamespacedIngress(project.safeName, this._namespace)
654
750
  } catch (err) {
655
- this._app.log.error(`[k8s] Project ${project.id} - error deleting ingress: ${err.toString()}`)
751
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting ingress: ${err.toString()}`)
656
752
  }
657
753
 
658
754
  if (this._certManagerIssuer) {
659
755
  try {
660
756
  await this._k8sApi.deleteNamespacedSecret(project.safeName, this._namespace)
661
757
  } catch (err) {
662
- this._app.log.error(`[k8s] Project ${project.id} - error deleting tls secret: ${err.toString()}`)
758
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()}`)
759
+ }
760
+ }
761
+
762
+ if (this._customHostname?.enabled) {
763
+ try {
764
+ await this._k8sNetApi.deleteNamespacedIngress(`${project.safeName}-custom`, this._namespace)
765
+ } catch (err) {
766
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom ingress: ${err.toString()}`)
767
+ }
768
+
769
+ if (this._customHostname?.certManagerIssuer) {
770
+ try {
771
+ await this._k8sApi.deleteNamespacedSecret(`${project.safeName}-custom`, this._namespace)
772
+ } catch (err) {
773
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom tls secret: ${err.toString()}`)
774
+ }
663
775
  }
664
776
  }
665
777
 
@@ -679,20 +791,20 @@ module.exports = {
679
791
  counter++
680
792
  if (counter > this._k8sRetries) {
681
793
  clearInterval(pollInterval)
682
- this._app.log.error(`[k8s] Project ${project.id} - timed out deleting ingress`)
794
+ this._app.log.error(`[k8s] Instance ${project.id} - timed out deleting ingress`)
683
795
  reject(new Error('Timed out to deleting Ingress'))
684
796
  }
685
797
  }, this._k8sDelay)
686
798
  })
687
799
  } catch (err) {
688
- this._app.log.error(`[k8s] Project ${project.id} - Ingress was not deleted: ${err.toString()}`)
800
+ this._app.log.error(`[k8s] Instance ${project.id} - Ingress was not deleted: ${err.toString()}`)
689
801
  }
690
802
 
691
803
  const prefix = project.safeName.match(/^[0-9]/) ? 'srv-' : ''
692
804
  try {
693
805
  await this._k8sApi.deleteNamespacedService(prefix + project.safeName, this._namespace)
694
806
  } catch (err) {
695
- this._app.log.error(`[k8s] Project ${project.id} - error deleting service: ${err.toString()}`)
807
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting service: ${err.toString()}`)
696
808
  }
697
809
 
698
810
  try {
@@ -708,13 +820,13 @@ module.exports = {
708
820
  counter++
709
821
  if (counter > this._k8sRetries) {
710
822
  clearInterval(pollInterval)
711
- this._app.log.error(`[k8s] Project ${project.id} - timed deleting service`)
823
+ this._app.log.error(`[k8s] Instance ${project.id} - timed deleting service`)
712
824
  reject(new Error('Timed out to deleting Service'))
713
825
  }
714
826
  }, this._k8sDelay)
715
827
  })
716
828
  } catch (err) {
717
- this._app.log.error(`[k8s] Project ${project.id} - Service was not deleted: ${err.toString()}`)
829
+ this._app.log.error(`[k8s] Instance ${project.id} - Service was not deleted: ${err.toString()}`)
718
830
  }
719
831
 
720
832
  const currentType = await project.getSetting('k8sType')
@@ -739,7 +851,7 @@ module.exports = {
739
851
  counter++
740
852
  if (counter > this._k8sRetries) {
741
853
  clearInterval(pollInterval)
742
- this._app.log.error(`[k8s] Project ${project.id} - timed deleting ${pod ? 'Pod' : 'Deployment'}`)
854
+ this._app.log.error(`[k8s] Instance ${project.id} - timed deleting ${pod ? 'Pod' : 'Deployment'}`)
743
855
  reject(new Error('Timed out to deleting Deployment'))
744
856
  }
745
857
  } catch (err) {
@@ -759,13 +871,27 @@ module.exports = {
759
871
  try {
760
872
  await this._k8sNetApi.deleteNamespacedIngress(project.safeName, this._namespace)
761
873
  } catch (err) {
762
- this._app.log.error(`[k8s] Project ${project.id} - error deleting ingress: ${err.toString()}`)
874
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting ingress: ${err.toString()}`)
763
875
  }
764
876
  if (this._certManagerIssuer) {
765
877
  try {
766
878
  await this._k8sApi.deleteNamespacedSecret(project.safeName, this._namespace)
767
879
  } catch (err) {
768
- this._app.log.error(`[k8s] Project ${project.id} - error deleting tls secret: ${err.toString()}`)
880
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()}`)
881
+ }
882
+ }
883
+ if (this._customHostname?.enabled) {
884
+ try {
885
+ await this._k8sNetApi.deleteNamespacedIngress(`${project.safeName}-custom`, this._namespace)
886
+ } catch (err) {
887
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom ingress: ${err.toString()}`)
888
+ }
889
+ if (this._customHostname?.certManagerIssuer) {
890
+ try {
891
+ await this._k8sApi.deleteNamespacedSecret(`${project.safeName}-custom`, this._namespace)
892
+ } catch (err) {
893
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom tls secret: ${err.toString()}`)
894
+ }
769
895
  }
770
896
  }
771
897
  try {
@@ -775,7 +901,7 @@ module.exports = {
775
901
  await this._k8sApi.deleteNamespacedService(project.safeName, this._namespace)
776
902
  }
777
903
  } catch (err) {
778
- this._app.log.error(`[k8s] Project ${project.id} - error deleting service: ${err.toString()}`)
904
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting service: ${err.toString()}`)
779
905
  }
780
906
  const currentType = await project.getSetting('k8sType')
781
907
  try {
@@ -789,9 +915,9 @@ module.exports = {
789
915
  } catch (err) {
790
916
  if (project.state !== 'suspended') {
791
917
  if (currentType === 'deployment') {
792
- this._app.log.error(`[k8s] Project ${project.id} - error deleting deployment: ${err.toString()}`)
918
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting deployment: ${err.toString()}`)
793
919
  } else {
794
- this._app.log.error(`[k8s] Project ${project.id} - error deleting pod: ${err.toString()}`)
920
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting pod: ${err.toString()}`)
795
921
  }
796
922
  }
797
923
  }
@@ -841,7 +967,7 @@ module.exports = {
841
967
  this._projects[project.id].state = info.state
842
968
  return info
843
969
  } catch (err) {
844
- this._app.log.debug(`error getting state from project ${project.id}: ${err}`)
970
+ this._app.log.debug(`error getting state from instance ${project.id}: ${err}`)
845
971
  return {
846
972
  id: project.id,
847
973
  state: 'starting',
@@ -874,7 +1000,7 @@ module.exports = {
874
1000
  this._projects[project.id].state = info.state
875
1001
  return info
876
1002
  } catch (err) {
877
- this._app.log.debug(`error getting state from project ${project.id}: ${err}`)
1003
+ this._app.log.debug(`error getting state from instance ${project.id}: ${err}`)
878
1004
  return {
879
1005
  id: project.id,
880
1006
  state: 'starting',
@@ -891,7 +1017,7 @@ module.exports = {
891
1017
  }
892
1018
  }
893
1019
  } catch (err) {
894
- this._app.log.debug(`error getting pod status for project ${project.id}: ${err}`)
1020
+ this._app.log.debug(`error getting pod status for instance ${project.id}: ${err}`)
895
1021
  return {
896
1022
  id: project?.id,
897
1023
  error: err,
@@ -1013,7 +1139,7 @@ module.exports = {
1013
1139
  * @return {forge.Status}
1014
1140
  */
1015
1141
  revokeUserToken: async (project, token) => { // logout:nodered(step-3)
1016
- this._app.log.debug(`[k8s] Project ${project.id} - logging out node-red instance`)
1142
+ this._app.log.debug(`[k8s] Instance ${project.id} - logging out node-red instance`)
1017
1143
  const endpoints = await getEndpoints(project)
1018
1144
  const commands = []
1019
1145
  for (const address in endpoints) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowfuse/driver-kubernetes",
3
- "version": "2.4.1-c425d12-202406041337.0",
3
+ "version": "2.5.0",
4
4
  "description": "Kubernetes driver for FlowFuse",
5
5
  "main": "kubernetes.js",
6
6
  "scripts": {