@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.
- package/.github/workflows/release-publish.yml +1 -1
- package/CHANGELOG.md +9 -0
- package/kubernetes.js +132 -32
- package/package.json +1 -1
|
@@ -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@
|
|
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
|
|
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
|
-
|
|
685
|
-
if (this._projects
|
|
686
|
-
this._projects
|
|
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
|
-
|
|
756
|
+
for (const project of projects) {
|
|
696
757
|
try {
|
|
697
758
|
if (project.state === 'suspended') {
|
|
698
759
|
// Do not restart suspended projects
|
|
699
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1127
|
+
const cachedProject = await this._projects.get(project.id)
|
|
1128
|
+
if (cachedProject === undefined) {
|
|
1039
1129
|
return { state: 'unknown' }
|
|
1040
1130
|
}
|
|
1041
|
-
if (
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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')) {
|