@flowfuse/driver-kubernetes 2.22.2-d3f9800-202510171249.0 → 2.23.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
12
- - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
12
+ - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
13
13
  with:
14
14
  node-version: 18
15
15
  - run: npm ci
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ #### 2.23.0: Release
2
+
3
+ - Bump actions/setup-node from 5.0.0 to 6.0.0 (#251)
4
+ - Use new cache system for in-memory state (#243) @hardillb
5
+ - feat: Improve projects ingress annotations logic (#242) @ppawlowski
6
+ - feat: Add possibility to configure container security context (#250) @ppawlowski
7
+ - feat: Add possibility to configure probes (#248) @ppawlowski
8
+ - Update container name validator (#247) @hardillb
9
+
1
10
  #### 2.22.1: Release
2
11
 
3
12
  - Bump JS-DevTools/npm-publish from 4.0.1 to 4.1.1 (#240)
package/README.md CHANGED
@@ -65,6 +65,7 @@ AWS EKS specific annotation for ALB Ingress. or `openshift` to allow running on
65
65
  - `storage.storageClassEFSTag` Used instead of `storage.storageClass` when needing to shard across multiple EFS file systems (default not set)
66
66
  - `storage.size` Size of the volume to request (default not set)
67
67
  - `podSecurityContext` Settings linked to the [security context of the pod](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)
68
+ - `containerSecurityContext` Settings linked to the [security context of the container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)
68
69
  - `service.type` Type of service to create for the editor (allowed `ClusterIP` or `NodePort`, default `ClusterIP`)
69
70
 
70
71
  Expects to pick up K8s credentials from the environment
package/kubernetes.js CHANGED
@@ -194,6 +194,14 @@ const createDeployment = async (project, options) => {
194
194
  this._app.log.info('[k8s] OpenShift, removing PodSecurityContext')
195
195
  }
196
196
 
197
+ if (this._app.config.driver.options?.containerSecurityContext) {
198
+ localPod.spec.containers[0].securityContext = this._app.config.driver.options.containerSecurityContext
199
+ this._app.log.info(`[k8s] Using custom ContainerSecurityContext ${JSON.stringify(this._app.config.driver.options.containerSecurityContext)}`)
200
+ } else if (this._app.license.active() && this._cloudProvider === 'openshift') {
201
+ localPod.spec.containers[0].securityContext = {}
202
+ this._app.log.info('[k8s] OpenShift, removing ContainerSecurityContext')
203
+ }
204
+
197
205
  if (stack.memory && stack.cpu) {
198
206
  localPod.spec.containers[0].resources.requests.memory = `${stack.memory}Mi`
199
207
  // increase limit to give npm more room to run in
@@ -271,8 +279,37 @@ const createIngress = async (project, options) => {
271
279
 
272
280
  const localIngress = JSON.parse(JSON.stringify(ingressTemplate))
273
281
 
282
+ let addIngressTls = false
283
+
274
284
  if (this._certManagerIssuer) {
275
285
  localIngress.metadata.annotations['cert-manager.io/cluster-issuer'] = this._certManagerIssuer
286
+ addIngressTls = true
287
+
288
+ // Add non-cert-manager annotations from projectIngressAnnotations if they exist
289
+ if (this._projectIngressAnnotations) {
290
+ Object.keys(this._projectIngressAnnotations).forEach((key) => {
291
+ if (!key.startsWith('cert-manager.io/')) {
292
+ localIngress.metadata.annotations[key] = this._projectIngressAnnotations[key]
293
+ }
294
+ })
295
+ }
296
+ } else if (this._projectIngressAnnotations) {
297
+ const hasCertManagerAnnotation = Object.keys(this._projectIngressAnnotations).some(key =>
298
+ key.startsWith('cert-manager.io/')
299
+ )
300
+
301
+ if (hasCertManagerAnnotation) {
302
+ addIngressTls = true
303
+ }
304
+
305
+ // Add all annotations from projectIngressAnnotations
306
+ Object.keys(this._projectIngressAnnotations).forEach((key) => {
307
+ localIngress.metadata.annotations[key] = this._projectIngressAnnotations[key]
308
+ })
309
+ }
310
+
311
+ // Add TLS configuration if needed
312
+ if (addIngressTls) {
276
313
  localIngress.spec.tls = [
277
314
  {
278
315
  hosts: [
@@ -319,8 +356,37 @@ const createCustomIngress = async (project, hostname, options) => {
319
356
  customIngress.spec.rules[0].host = hostname
320
357
  customIngress.spec.rules[0].http.paths[0].backend.service.name = `${prefix}${project.safeName}`
321
358
 
359
+ let addCustomIngressTls = false
360
+
322
361
  if (this._customHostname?.certManagerIssuer) {
323
362
  customIngress.metadata.annotations['cert-manager.io/cluster-issuer'] = this._customHostname.certManagerIssuer
363
+ addCustomIngressTls = true
364
+
365
+ // Add non-cert-manager annotations from projectIngressAnnotations if they exist
366
+ if (this._customHostname?.ingressAnnotations) {
367
+ Object.keys(this._customHostname?.ingressAnnotations).forEach((key) => {
368
+ if (!key.startsWith('cert-manager.io/')) {
369
+ customIngress.metadata.annotations[key] = this._customHostname?.ingressAnnotations[key]
370
+ }
371
+ })
372
+ }
373
+ } else if (this._customHostname?.ingressAnnotations) {
374
+ const hasCertManagerAnnotation = Object.keys(this._customHostname?.ingressAnnotations).some(key =>
375
+ key.startsWith('cert-manager.io/')
376
+ )
377
+
378
+ if (hasCertManagerAnnotation) {
379
+ addCustomIngressTls = true
380
+ }
381
+
382
+ // Add all annotations from projectIngressAnnotations
383
+ Object.keys(this._customHostname?.ingressAnnotations).forEach((key) => {
384
+ customIngress.metadata.annotations[key] = this._customHostname?.ingressAnnotations[key]
385
+ })
386
+ }
387
+
388
+ // Add TLS configuration if needed
389
+ if (addCustomIngressTls) {
324
390
  customIngress.spec.tls = [
325
391
  {
326
392
  hosts: [
@@ -535,7 +601,9 @@ const createProject = async (project, options) => {
535
601
  project.state = 'running'
536
602
  await project.save()
537
603
 
538
- this._projects[project.id].state = 'starting'
604
+ const cachedProject = await this._projects.get(project.id)
605
+ cachedProject.state = 'starting'
606
+ await this._projects.set(project.id, cachedProject)
539
607
  }
540
608
 
541
609
  const getEndpoints = async (project) => {
@@ -631,13 +699,14 @@ module.exports = {
631
699
  throw Error('Failed to load Kubernetes node client', { cause: err })
632
700
  }
633
701
  this._app = app
634
- this._projects = {}
702
+ this._projects = app.caches.getCache('driver-k8s-projects') // {}
635
703
  this._options = options
636
704
 
637
705
  this._namespace = this._app.config.driver.options?.projectNamespace || 'flowforge'
638
706
  this._k8sDelay = this._app.config.driver.options?.k8sDelay || 1000
639
707
  this._k8sRetries = this._app.config.driver.options?.k8sRetries || 10
640
708
  this._certManagerIssuer = this._app.config.driver.options?.certManagerIssuer
709
+ this._projectIngressAnnotations = this._app.config.driver.options?.projectIngressAnnotations
641
710
  this._logPassthrough = this._app.config.driver.options?.logPassthrough || false
642
711
  this._cloudProvider = this._app.config.driver.options?.cloudProvider
643
712
  if (this._app.config.driver.options?.customHostname?.enabled) {
@@ -673,22 +742,25 @@ module.exports = {
673
742
  'TeamId'
674
743
  ]
675
744
  })
676
- projects.forEach(async (project) => {
677
- if (this._projects[project.id] === undefined) {
678
- this._projects[project.id] = {
745
+ for (const project of projects) {
746
+ if (await this._projects.get(project.id) === undefined) {
747
+ await this._projects.set(project.id, {
679
748
  state: 'unknown'
680
- }
749
+ })
681
750
  }
682
- })
751
+ }
683
752
 
684
753
  this._initialCheckTimeout = setTimeout(async () => {
685
754
  this._app.log.debug('[k8s] Restarting projects')
686
755
  const namespace = this._namespace
687
- projects.forEach(async (project) => {
756
+ for (const project of projects) {
688
757
  try {
689
758
  if (project.state === 'suspended') {
690
759
  // Do not restart suspended projects
691
- return
760
+ const cachedProject = await this._projects.get(project.id)
761
+ cachedProject.state = 'suspened'
762
+ await this._projects.set(project.id, cachedProject)
763
+ continue
692
764
  }
693
765
 
694
766
  // need to upgrade bare pods to deployments
@@ -742,7 +814,7 @@ module.exports = {
742
814
  } catch (err) {
743
815
  this._app.log.error(`[k8s] Instance ${project.id} - error resuming project: ${err.stack}`)
744
816
  }
745
- })
817
+ }
746
818
 
747
819
  // get list of all MQTTBrokers
748
820
  if (this._app.db.models.BrokerCredentials) {
@@ -751,7 +823,7 @@ module.exports = {
751
823
  })
752
824
 
753
825
  // Check restarting MQTT-Schema-Agent
754
- brokers.forEach(async (broker) => {
826
+ for (const broker of brokers) {
755
827
  const agent = broker.constructor.name === 'TeamBrokerAgent'
756
828
  if (broker.Team && broker.state === 'running') {
757
829
  try {
@@ -765,9 +837,9 @@ module.exports = {
765
837
  await createMQTTTopicAgent(broker)
766
838
  }
767
839
  }
768
- })
840
+ }
769
841
  }
770
- }, 1000)
842
+ }, Math.floor(1000 + (Math.random() * 5))) // space this out so if 2 instances running they shouldn't run at the same time
771
843
 
772
844
  // need to work out what we can expose for K8s
773
845
  return {
@@ -802,9 +874,9 @@ module.exports = {
802
874
  * @return {forge.containers.Project}
803
875
  */
804
876
  start: async (project) => {
805
- this._projects[project.id] = {
877
+ await this._projects.set(project.id, {
806
878
  state: 'starting'
807
- }
879
+ })
808
880
 
809
881
  // Rather than await this promise, we return it. That allows the wrapper
810
882
  // to respond to the create request much quicker and the create can happen
@@ -824,7 +896,9 @@ module.exports = {
824
896
  */
825
897
  stop: async (project) => {
826
898
  // Stop the project
827
- this._projects[project.id].state = 'stopping'
899
+ const cachedProject = await this._projects.get(project.id)
900
+ cachedProject.state = 'stopping'
901
+ await this._projects.set(project.id, cachedProject)
828
902
 
829
903
  try {
830
904
  await this._k8sNetApi.deleteNamespacedIngress({ name: project.safeName, namespace: this._namespace })
@@ -838,6 +912,17 @@ module.exports = {
838
912
  } catch (err) {
839
913
  this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()} ${err.stack}`)
840
914
  }
915
+ } else if (this._projectIngressAnnotations) {
916
+ const hasCertManagerAnnotation = Object.keys(this._projectIngressAnnotations).some(key =>
917
+ key.startsWith('cert-manager.io/')
918
+ )
919
+ if (hasCertManagerAnnotation) {
920
+ try {
921
+ await this._k8sApi.deleteNamespacedSecret({ name: project.safeName, namespace: this._namespace })
922
+ } catch (err) {
923
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()} ${err.stack}`)
924
+ }
925
+ }
841
926
  }
842
927
 
843
928
  if (this._customHostname?.enabled) {
@@ -928,7 +1013,8 @@ module.exports = {
928
1013
  // }
929
1014
  // }
930
1015
 
931
- this._projects[project.id].state = 'suspended'
1016
+ cachedProject.state = 'suspended'
1017
+ await this._projects.set(project.id, cachedProject)
932
1018
  return new Promise((resolve, reject) => {
933
1019
  let counter = 0
934
1020
  const pollInterval = setInterval(async () => {
@@ -969,6 +1055,17 @@ module.exports = {
969
1055
  } catch (err) {
970
1056
  this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()}`)
971
1057
  }
1058
+ } else if (this._projectIngressAnnotations) {
1059
+ const hasCertManagerAnnotation = Object.keys(this._projectIngressAnnotations).some(key =>
1060
+ key.startsWith('cert-manager.io/')
1061
+ )
1062
+ if (hasCertManagerAnnotation) {
1063
+ try {
1064
+ await this._k8sApi.deleteNamespacedSecret({ name: project.safeName, namespace: this._namespace })
1065
+ } catch (err) {
1066
+ this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()}`)
1067
+ }
1068
+ }
972
1069
  }
973
1070
  if (this._customHostname?.enabled) {
974
1071
  try {
@@ -976,7 +1073,7 @@ module.exports = {
976
1073
  } catch (err) {
977
1074
  this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom ingress: ${err.toString()}`)
978
1075
  }
979
- if (this._customHostname?.certManagerIssuer) {
1076
+ if (this._customHostname?.certManagerIssuer || this._customHostname?.certManagerAnnotations) {
980
1077
  try {
981
1078
  await this._k8sApi.deleteNamespacedSecret({ name: `${project.safeName}-custom`, namespace: this._namespace })
982
1079
  } catch (err) {
@@ -1019,7 +1116,7 @@ module.exports = {
1019
1116
  // console.log(err)
1020
1117
  }
1021
1118
  }
1022
- delete this._projects[project.id]
1119
+ await this._projects.del(project.id)
1023
1120
  },
1024
1121
  /**
1025
1122
  * Retrieves details of a project's container
@@ -1027,14 +1124,15 @@ module.exports = {
1027
1124
  * @return {Object}
1028
1125
  */
1029
1126
  details: async (project) => {
1030
- if (this._projects[project.id] === undefined) {
1127
+ const cachedProject = await this._projects.get(project.id)
1128
+ if (cachedProject === undefined) {
1031
1129
  return { state: 'unknown' }
1032
1130
  }
1033
- if (this._projects[project.id].state === 'suspended') {
1131
+ if (cachedProject.state === 'suspended') {
1034
1132
  // We should only poll the launcher if we think it is running.
1035
1133
  // Otherwise, return our cached state
1036
1134
  return {
1037
- state: this._projects[project.id].state
1135
+ state: cachedProject.state
1038
1136
  }
1039
1137
  }
1040
1138
  const prefix = project.safeName.match(/^[0-9]/) ? 'srv-' : ''
@@ -1048,7 +1146,8 @@ module.exports = {
1048
1146
  details = await this._k8sAppApi.readNamespacedDeployment({ name: project.safeName, namespace: this._namespace })
1049
1147
  if (details.status?.conditions[0].status === 'False') {
1050
1148
  // return "starting" status until pod it running
1051
- this._projects[project.id].state = 'starting'
1149
+ cachedProject.state = 'starting'
1150
+ await this._projects.set(project.id, cachedProject)
1052
1151
  return {
1053
1152
  id: project.id,
1054
1153
  state: 'starting',
@@ -1062,7 +1161,8 @@ module.exports = {
1062
1161
  const infoURL = `http://${prefix}${project.safeName}.${this._namespace}:2880/flowforge/info`
1063
1162
  try {
1064
1163
  const info = JSON.parse((await got.get(infoURL, { timeout: { request: 1000 } })).body)
1065
- this._projects[project.id].state = info.state
1164
+ cachedProject.state = info.state
1165
+ await this._projects.set(project.id, cachedProject)
1066
1166
  return info
1067
1167
  } catch (err) {
1068
1168
  this._app.log.debug(`error getting state from instance ${project.id}: ${err}`)
@@ -1084,7 +1184,8 @@ module.exports = {
1084
1184
  details = await this._k8sApi.readNamespacedPodStatus({ name: project.safeName, namespace: this._namespace })
1085
1185
  if (details.status?.phase === 'Pending') {
1086
1186
  // return "starting" status until pod it running
1087
- this._projects[project.id].state = 'starting'
1187
+ cachedProject.state = 'starting'
1188
+ this._projects.set(project.id, cachedProject)
1088
1189
  return {
1089
1190
  id: project.id,
1090
1191
  state: 'starting',
@@ -1095,7 +1196,8 @@ module.exports = {
1095
1196
  const infoURL = `http://${prefix}${project.safeName}.${this._namespace}:2880/flowforge/info`
1096
1197
  try {
1097
1198
  const info = JSON.parse((await got.get(infoURL, { timeout: { request: 1000 } })).body)
1098
- this._projects[project.id].state = info.state
1199
+ cachedProject.state = info.state
1200
+ await this._projects.set(project.id, cachedProject)
1099
1201
  return info
1100
1202
  } catch (err) {
1101
1203
  this._app.log.debug(`error getting state from instance ${project.id}: ${err}`)
@@ -1145,7 +1247,8 @@ module.exports = {
1145
1247
  * @return {forge.Status}
1146
1248
  */
1147
1249
  startFlows: async (project) => {
1148
- if (this._projects[project.id] === undefined) {
1250
+ const cachedProject = await this._projects.get(project.id)
1251
+ if (cachedProject === undefined) {
1149
1252
  return { state: 'unknown' }
1150
1253
  }
1151
1254
  const endpoints = await getEndpoints(project)
@@ -1167,7 +1270,8 @@ module.exports = {
1167
1270
  * @return {forge.Status}
1168
1271
  */
1169
1272
  stopFlows: async (project) => {
1170
- if (this._projects[project.id] === undefined) {
1273
+ const cachedProject = await this._projects.get(project.id)
1274
+ if (cachedProject === undefined) {
1171
1275
  return { state: 'unknown' }
1172
1276
  }
1173
1277
  const endpoints = await getEndpoints(project)
@@ -1189,7 +1293,8 @@ module.exports = {
1189
1293
  * @return {array} logs
1190
1294
  */
1191
1295
  logs: async (project) => {
1192
- if (this._projects[project.id] === undefined) {
1296
+ const cachedProject = await this._projects.get(project.id)
1297
+ if (cachedProject === undefined) {
1193
1298
  return { state: 'unknown' }
1194
1299
  }
1195
1300
  if (await project.getSetting('ha')) {
@@ -1215,7 +1320,8 @@ module.exports = {
1215
1320
  * @return {forge.Status}
1216
1321
  */
1217
1322
  restartFlows: async (project) => {
1218
- if (this._projects[project.id] === undefined) {
1323
+ const cachedProject = await this._projects.get(project.id)
1324
+ if (cachedProject === undefined) {
1219
1325
  return { state: 'unknown' }
1220
1326
  }
1221
1327
  const endpoints = await getEndpoints(project)
@@ -1370,7 +1476,8 @@ module.exports = {
1370
1476
 
1371
1477
  // Resouces api
1372
1478
  resources: async (project) => {
1373
- if (this._projects[project.id] === undefined) {
1479
+ const cachedProject = await this._projects.get(project.id)
1480
+ if (cachedProject === undefined) {
1374
1481
  return { state: 'unknown' }
1375
1482
  }
1376
1483
  if (await project.getSetting('ha')) {
@@ -1403,7 +1510,8 @@ module.exports = {
1403
1510
  }
1404
1511
  },
1405
1512
  resourcesStream: async (project, socket) => {
1406
- if (this._projects[project.id] === undefined) {
1513
+ const cachedProject = await this._projects.get(project.id)
1514
+ if (cachedProject === undefined) {
1407
1515
  throw new Error('Cannot get instance resources')
1408
1516
  }
1409
1517
  if (await project.getSetting('ha')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowfuse/driver-kubernetes",
3
- "version": "2.22.2-d3f9800-202510171249.0",
3
+ "version": "2.23.0",
4
4
  "description": "Kubernetes driver for FlowFuse",
5
5
  "main": "kubernetes.js",
6
6
  "scripts": {