@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.
- package/.github/workflows/release-publish.yml +1 -1
- package/CHANGELOG.md +9 -0
- package/README.md +1 -0
- package/kubernetes.js +140 -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/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
|
|
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
|
-
|
|
677
|
-
if (this._projects
|
|
678
|
-
this._projects
|
|
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
|
-
|
|
756
|
+
for (const project of projects) {
|
|
688
757
|
try {
|
|
689
758
|
if (project.state === 'suspended') {
|
|
690
759
|
// Do not restart suspended projects
|
|
691
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1127
|
+
const cachedProject = await this._projects.get(project.id)
|
|
1128
|
+
if (cachedProject === undefined) {
|
|
1031
1129
|
return { state: 'unknown' }
|
|
1032
1130
|
}
|
|
1033
|
-
if (
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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')) {
|