@flowfuse/driver-kubernetes 2.22.2-d74c830-202510171251.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/kubernetes.js CHANGED
@@ -279,8 +279,37 @@ const createIngress = async (project, options) => {
279
279
 
280
280
  const localIngress = JSON.parse(JSON.stringify(ingressTemplate))
281
281
 
282
+ let addIngressTls = false
283
+
282
284
  if (this._certManagerIssuer) {
283
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) {
284
313
  localIngress.spec.tls = [
285
314
  {
286
315
  hosts: [
@@ -327,8 +356,37 @@ const createCustomIngress = async (project, hostname, options) => {
327
356
  customIngress.spec.rules[0].host = hostname
328
357
  customIngress.spec.rules[0].http.paths[0].backend.service.name = `${prefix}${project.safeName}`
329
358
 
359
+ let addCustomIngressTls = false
360
+
330
361
  if (this._customHostname?.certManagerIssuer) {
331
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) {
332
390
  customIngress.spec.tls = [
333
391
  {
334
392
  hosts: [
@@ -543,7 +601,9 @@ const createProject = async (project, options) => {
543
601
  project.state = 'running'
544
602
  await project.save()
545
603
 
546
- 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)
547
607
  }
548
608
 
549
609
  const getEndpoints = async (project) => {
@@ -639,13 +699,14 @@ module.exports = {
639
699
  throw Error('Failed to load Kubernetes node client', { cause: err })
640
700
  }
641
701
  this._app = app
642
- this._projects = {}
702
+ this._projects = app.caches.getCache('driver-k8s-projects') // {}
643
703
  this._options = options
644
704
 
645
705
  this._namespace = this._app.config.driver.options?.projectNamespace || 'flowforge'
646
706
  this._k8sDelay = this._app.config.driver.options?.k8sDelay || 1000
647
707
  this._k8sRetries = this._app.config.driver.options?.k8sRetries || 10
648
708
  this._certManagerIssuer = this._app.config.driver.options?.certManagerIssuer
709
+ this._projectIngressAnnotations = this._app.config.driver.options?.projectIngressAnnotations
649
710
  this._logPassthrough = this._app.config.driver.options?.logPassthrough || false
650
711
  this._cloudProvider = this._app.config.driver.options?.cloudProvider
651
712
  if (this._app.config.driver.options?.customHostname?.enabled) {
@@ -681,22 +742,25 @@ module.exports = {
681
742
  'TeamId'
682
743
  ]
683
744
  })
684
- projects.forEach(async (project) => {
685
- if (this._projects[project.id] === undefined) {
686
- 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, {
687
748
  state: 'unknown'
688
- }
749
+ })
689
750
  }
690
- })
751
+ }
691
752
 
692
753
  this._initialCheckTimeout = setTimeout(async () => {
693
754
  this._app.log.debug('[k8s] Restarting projects')
694
755
  const namespace = this._namespace
695
- projects.forEach(async (project) => {
756
+ for (const project of projects) {
696
757
  try {
697
758
  if (project.state === 'suspended') {
698
759
  // Do not restart suspended projects
699
- return
760
+ const cachedProject = await this._projects.get(project.id)
761
+ cachedProject.state = 'suspened'
762
+ await this._projects.set(project.id, cachedProject)
763
+ continue
700
764
  }
701
765
 
702
766
  // need to upgrade bare pods to deployments
@@ -750,7 +814,7 @@ module.exports = {
750
814
  } catch (err) {
751
815
  this._app.log.error(`[k8s] Instance ${project.id} - error resuming project: ${err.stack}`)
752
816
  }
753
- })
817
+ }
754
818
 
755
819
  // get list of all MQTTBrokers
756
820
  if (this._app.db.models.BrokerCredentials) {
@@ -759,7 +823,7 @@ module.exports = {
759
823
  })
760
824
 
761
825
  // Check restarting MQTT-Schema-Agent
762
- brokers.forEach(async (broker) => {
826
+ for (const broker of brokers) {
763
827
  const agent = broker.constructor.name === 'TeamBrokerAgent'
764
828
  if (broker.Team && broker.state === 'running') {
765
829
  try {
@@ -773,9 +837,9 @@ module.exports = {
773
837
  await createMQTTTopicAgent(broker)
774
838
  }
775
839
  }
776
- })
840
+ }
777
841
  }
778
- }, 1000)
842
+ }, Math.floor(1000 + (Math.random() * 5))) // space this out so if 2 instances running they shouldn't run at the same time
779
843
 
780
844
  // need to work out what we can expose for K8s
781
845
  return {
@@ -810,9 +874,9 @@ module.exports = {
810
874
  * @return {forge.containers.Project}
811
875
  */
812
876
  start: async (project) => {
813
- this._projects[project.id] = {
877
+ await this._projects.set(project.id, {
814
878
  state: 'starting'
815
- }
879
+ })
816
880
 
817
881
  // Rather than await this promise, we return it. That allows the wrapper
818
882
  // to respond to the create request much quicker and the create can happen
@@ -832,7 +896,9 @@ module.exports = {
832
896
  */
833
897
  stop: async (project) => {
834
898
  // Stop the project
835
- 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)
836
902
 
837
903
  try {
838
904
  await this._k8sNetApi.deleteNamespacedIngress({ name: project.safeName, namespace: this._namespace })
@@ -846,6 +912,17 @@ module.exports = {
846
912
  } catch (err) {
847
913
  this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()} ${err.stack}`)
848
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
+ }
849
926
  }
850
927
 
851
928
  if (this._customHostname?.enabled) {
@@ -936,7 +1013,8 @@ module.exports = {
936
1013
  // }
937
1014
  // }
938
1015
 
939
- this._projects[project.id].state = 'suspended'
1016
+ cachedProject.state = 'suspended'
1017
+ await this._projects.set(project.id, cachedProject)
940
1018
  return new Promise((resolve, reject) => {
941
1019
  let counter = 0
942
1020
  const pollInterval = setInterval(async () => {
@@ -977,6 +1055,17 @@ module.exports = {
977
1055
  } catch (err) {
978
1056
  this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()}`)
979
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
+ }
980
1069
  }
981
1070
  if (this._customHostname?.enabled) {
982
1071
  try {
@@ -984,7 +1073,7 @@ module.exports = {
984
1073
  } catch (err) {
985
1074
  this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom ingress: ${err.toString()}`)
986
1075
  }
987
- if (this._customHostname?.certManagerIssuer) {
1076
+ if (this._customHostname?.certManagerIssuer || this._customHostname?.certManagerAnnotations) {
988
1077
  try {
989
1078
  await this._k8sApi.deleteNamespacedSecret({ name: `${project.safeName}-custom`, namespace: this._namespace })
990
1079
  } catch (err) {
@@ -1027,7 +1116,7 @@ module.exports = {
1027
1116
  // console.log(err)
1028
1117
  }
1029
1118
  }
1030
- delete this._projects[project.id]
1119
+ await this._projects.del(project.id)
1031
1120
  },
1032
1121
  /**
1033
1122
  * Retrieves details of a project's container
@@ -1035,14 +1124,15 @@ module.exports = {
1035
1124
  * @return {Object}
1036
1125
  */
1037
1126
  details: async (project) => {
1038
- if (this._projects[project.id] === undefined) {
1127
+ const cachedProject = await this._projects.get(project.id)
1128
+ if (cachedProject === undefined) {
1039
1129
  return { state: 'unknown' }
1040
1130
  }
1041
- if (this._projects[project.id].state === 'suspended') {
1131
+ if (cachedProject.state === 'suspended') {
1042
1132
  // We should only poll the launcher if we think it is running.
1043
1133
  // Otherwise, return our cached state
1044
1134
  return {
1045
- state: this._projects[project.id].state
1135
+ state: cachedProject.state
1046
1136
  }
1047
1137
  }
1048
1138
  const prefix = project.safeName.match(/^[0-9]/) ? 'srv-' : ''
@@ -1056,7 +1146,8 @@ module.exports = {
1056
1146
  details = await this._k8sAppApi.readNamespacedDeployment({ name: project.safeName, namespace: this._namespace })
1057
1147
  if (details.status?.conditions[0].status === 'False') {
1058
1148
  // return "starting" status until pod it running
1059
- this._projects[project.id].state = 'starting'
1149
+ cachedProject.state = 'starting'
1150
+ await this._projects.set(project.id, cachedProject)
1060
1151
  return {
1061
1152
  id: project.id,
1062
1153
  state: 'starting',
@@ -1070,7 +1161,8 @@ module.exports = {
1070
1161
  const infoURL = `http://${prefix}${project.safeName}.${this._namespace}:2880/flowforge/info`
1071
1162
  try {
1072
1163
  const info = JSON.parse((await got.get(infoURL, { timeout: { request: 1000 } })).body)
1073
- this._projects[project.id].state = info.state
1164
+ cachedProject.state = info.state
1165
+ await this._projects.set(project.id, cachedProject)
1074
1166
  return info
1075
1167
  } catch (err) {
1076
1168
  this._app.log.debug(`error getting state from instance ${project.id}: ${err}`)
@@ -1092,7 +1184,8 @@ module.exports = {
1092
1184
  details = await this._k8sApi.readNamespacedPodStatus({ name: project.safeName, namespace: this._namespace })
1093
1185
  if (details.status?.phase === 'Pending') {
1094
1186
  // return "starting" status until pod it running
1095
- this._projects[project.id].state = 'starting'
1187
+ cachedProject.state = 'starting'
1188
+ this._projects.set(project.id, cachedProject)
1096
1189
  return {
1097
1190
  id: project.id,
1098
1191
  state: 'starting',
@@ -1103,7 +1196,8 @@ module.exports = {
1103
1196
  const infoURL = `http://${prefix}${project.safeName}.${this._namespace}:2880/flowforge/info`
1104
1197
  try {
1105
1198
  const info = JSON.parse((await got.get(infoURL, { timeout: { request: 1000 } })).body)
1106
- this._projects[project.id].state = info.state
1199
+ cachedProject.state = info.state
1200
+ await this._projects.set(project.id, cachedProject)
1107
1201
  return info
1108
1202
  } catch (err) {
1109
1203
  this._app.log.debug(`error getting state from instance ${project.id}: ${err}`)
@@ -1153,7 +1247,8 @@ module.exports = {
1153
1247
  * @return {forge.Status}
1154
1248
  */
1155
1249
  startFlows: async (project) => {
1156
- if (this._projects[project.id] === undefined) {
1250
+ const cachedProject = await this._projects.get(project.id)
1251
+ if (cachedProject === undefined) {
1157
1252
  return { state: 'unknown' }
1158
1253
  }
1159
1254
  const endpoints = await getEndpoints(project)
@@ -1175,7 +1270,8 @@ module.exports = {
1175
1270
  * @return {forge.Status}
1176
1271
  */
1177
1272
  stopFlows: async (project) => {
1178
- if (this._projects[project.id] === undefined) {
1273
+ const cachedProject = await this._projects.get(project.id)
1274
+ if (cachedProject === undefined) {
1179
1275
  return { state: 'unknown' }
1180
1276
  }
1181
1277
  const endpoints = await getEndpoints(project)
@@ -1197,7 +1293,8 @@ module.exports = {
1197
1293
  * @return {array} logs
1198
1294
  */
1199
1295
  logs: async (project) => {
1200
- if (this._projects[project.id] === undefined) {
1296
+ const cachedProject = await this._projects.get(project.id)
1297
+ if (cachedProject === undefined) {
1201
1298
  return { state: 'unknown' }
1202
1299
  }
1203
1300
  if (await project.getSetting('ha')) {
@@ -1223,7 +1320,8 @@ module.exports = {
1223
1320
  * @return {forge.Status}
1224
1321
  */
1225
1322
  restartFlows: async (project) => {
1226
- if (this._projects[project.id] === undefined) {
1323
+ const cachedProject = await this._projects.get(project.id)
1324
+ if (cachedProject === undefined) {
1227
1325
  return { state: 'unknown' }
1228
1326
  }
1229
1327
  const endpoints = await getEndpoints(project)
@@ -1378,7 +1476,8 @@ module.exports = {
1378
1476
 
1379
1477
  // Resouces api
1380
1478
  resources: async (project) => {
1381
- if (this._projects[project.id] === undefined) {
1479
+ const cachedProject = await this._projects.get(project.id)
1480
+ if (cachedProject === undefined) {
1382
1481
  return { state: 'unknown' }
1383
1482
  }
1384
1483
  if (await project.getSetting('ha')) {
@@ -1411,7 +1510,8 @@ module.exports = {
1411
1510
  }
1412
1511
  },
1413
1512
  resourcesStream: async (project, socket) => {
1414
- if (this._projects[project.id] === undefined) {
1513
+ const cachedProject = await this._projects.get(project.id)
1514
+ if (cachedProject === undefined) {
1415
1515
  throw new Error('Cannot get instance resources')
1416
1516
  }
1417
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-d74c830-202510171251.0",
3
+ "version": "2.23.0",
4
4
  "description": "Kubernetes driver for FlowFuse",
5
5
  "main": "kubernetes.js",
6
6
  "scripts": {