@highstate/backend 0.4.4 → 0.4.6
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/dist/index.d.ts +93 -47
- package/dist/index.mjs +907 -393
- package/dist/library/worker/main.mjs +3 -8
- package/dist/shared/index.d.ts +80 -16
- package/dist/shared/index.mjs +52 -2
- package/dist/terminal-C1HuyJ6e.mjs +512 -0
- package/dist/terminal-_gg6j6_K.d.ts +1417 -0
- package/package.json +6 -5
- package/dist/input-hash-C8HEDMjz.mjs +0 -292
- package/dist/operation-8k4Tv4dw.d.ts +0 -841
package/dist/index.mjs
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
import { z } from 'zod';
|
2
|
-
import { pickBy, mapKeys, mapValues, funnel } from 'remeda';
|
2
|
+
import { pickBy, mapKeys, mapValues, funnel, omit, pick } from 'remeda';
|
3
3
|
import { BetterLock } from 'better-lock';
|
4
|
-
import { consola } from 'consola';
|
5
4
|
import { basename, relative, dirname, resolve as resolve$1 } from 'node:path';
|
6
5
|
import { findWorkspaceDir, readPackageJSON } from 'pkg-types';
|
7
6
|
import { fileURLToPath } from 'node:url';
|
@@ -11,8 +10,9 @@ import Watcher from 'watcher';
|
|
11
10
|
import { resolve } from 'import-meta-resolve';
|
12
11
|
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
|
13
12
|
import { getInstanceId, isUnitModel, parseInstanceId } from '@highstate/contract';
|
14
|
-
import {
|
15
|
-
import 'crypto-hash';
|
13
|
+
import { h as hubModelSchema, i as instanceModelSchema, c as createInputResolver, a as createInputHashResolver, b as createInstanceState, d as instanceTerminalSchema, e as instancePageSchema, f as instanceFileSchema, g as instanceStatusFieldSchema, j as instanceTriggerSchema, k as compositeInstanceSchema, p as projectOperationSchema, l as instanceStateSchema, m as isFinalOperationStatus, t as terminalSessionSchema, n as applyPartialInstanceState, o as createInstanceStateFrontendPatch } from './terminal-C1HuyJ6e.mjs';
|
14
|
+
import { sha256 } from 'crypto-hash';
|
15
|
+
import 'ajv';
|
16
16
|
import { Readable, PassThrough } from 'node:stream';
|
17
17
|
import { tmpdir, homedir } from 'node:os';
|
18
18
|
import spawn from 'nano-spawn';
|
@@ -57,6 +57,12 @@ function errorToString(error) {
|
|
57
57
|
}
|
58
58
|
return JSON.stringify(error);
|
59
59
|
}
|
60
|
+
function arrayAccumulator(values, value) {
|
61
|
+
if (!values) {
|
62
|
+
return [value];
|
63
|
+
}
|
64
|
+
return [...values, value];
|
65
|
+
}
|
60
66
|
|
61
67
|
class LocalPulumiHost {
|
62
68
|
constructor(logger) {
|
@@ -74,7 +80,7 @@ class LocalPulumiHost {
|
|
74
80
|
}
|
75
81
|
}
|
76
82
|
async runInline(projectId, pulumiProjectName, pulumiStackName, program, fn) {
|
77
|
-
return this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
|
83
|
+
return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
|
78
84
|
const { LocalWorkspace } = await import('@pulumi/pulumi/automation/index.js');
|
79
85
|
const stack = await LocalWorkspace.createOrSelectStack(
|
80
86
|
{
|
@@ -87,13 +93,16 @@ class LocalPulumiHost {
|
|
87
93
|
name: pulumiProjectName,
|
88
94
|
runtime: "nodejs"
|
89
95
|
},
|
90
|
-
envVars: {
|
96
|
+
envVars: {
|
97
|
+
PULUMI_CONFIG_PASSPHRASE: this.getPassword(projectId),
|
98
|
+
PULUMI_K8S_AWAIT_ALL: "true"
|
99
|
+
}
|
91
100
|
}
|
92
101
|
);
|
93
102
|
try {
|
94
103
|
return await runWithRetryOnError(
|
95
104
|
() => fn(stack),
|
96
|
-
(error) =>
|
105
|
+
(error) => this.tryUnlockStack(stack, error)
|
97
106
|
);
|
98
107
|
} catch (e) {
|
99
108
|
if (e instanceof Error && e.message.includes("canceled")) {
|
@@ -104,12 +113,12 @@ class LocalPulumiHost {
|
|
104
113
|
});
|
105
114
|
}
|
106
115
|
async runEmpty(projectId, pulumiProjectName, pulumiStackName, fn) {
|
107
|
-
return this.runInline(projectId, pulumiProjectName, pulumiStackName, async () => {
|
116
|
+
return await this.runInline(projectId, pulumiProjectName, pulumiStackName, async () => {
|
108
117
|
}, fn);
|
109
118
|
}
|
110
119
|
// TODO: extract args to options object
|
111
120
|
async runLocal(projectId, pulumiProjectName, pulumiStackName, programPathResolver, fn, stackConfig) {
|
112
|
-
return this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
|
121
|
+
return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
|
113
122
|
const { LocalWorkspace } = await import('@pulumi/pulumi/automation/index.js');
|
114
123
|
const stack = await LocalWorkspace.createOrSelectStack(
|
115
124
|
{
|
@@ -125,14 +134,17 @@ class LocalPulumiHost {
|
|
125
134
|
[pulumiStackName]: {
|
126
135
|
config: stackConfig
|
127
136
|
}
|
128
|
-
} :
|
129
|
-
envVars: {
|
137
|
+
} : void 0,
|
138
|
+
envVars: {
|
139
|
+
PULUMI_CONFIG_PASSPHRASE: this.getPassword(projectId),
|
140
|
+
PULUMI_K8S_AWAIT_ALL: "true"
|
141
|
+
}
|
130
142
|
}
|
131
143
|
);
|
132
144
|
try {
|
133
145
|
return await runWithRetryOnError(
|
134
146
|
() => fn(stack),
|
135
|
-
(error) =>
|
147
|
+
(error) => this.tryUnlockStack(stack, error)
|
136
148
|
);
|
137
149
|
} catch (e) {
|
138
150
|
if (e instanceof Error && e.message.includes("canceled")) {
|
@@ -153,9 +165,9 @@ class LocalPulumiHost {
|
|
153
165
|
getPassword(projectId) {
|
154
166
|
return this.sharedPassword || this.passwords.get(projectId) || "";
|
155
167
|
}
|
156
|
-
|
168
|
+
async tryUnlockStack(stack, error) {
|
157
169
|
if (error instanceof Error && error.message.includes("the stack is currently locked")) {
|
158
|
-
|
170
|
+
this.logger.warn({ stackName: stack.name }, "inlocking stack");
|
159
171
|
await stack.cancel();
|
160
172
|
return true;
|
161
173
|
}
|
@@ -446,7 +458,8 @@ const localProjectBackendConfig = z.object({
|
|
446
458
|
HIGHSTATE_BACKEND_PROJECT_PROJECTS_DIR: z.string().optional()
|
447
459
|
});
|
448
460
|
const projectModelSchema = z.object({
|
449
|
-
instances: z.record(instanceModelSchema)
|
461
|
+
instances: z.record(instanceModelSchema),
|
462
|
+
hubs: z.record(hubModelSchema)
|
450
463
|
});
|
451
464
|
class LocalProjectBackend {
|
452
465
|
constructor(projectsDir) {
|
@@ -460,10 +473,13 @@ class LocalProjectBackend {
|
|
460
473
|
throw new Error("Failed to get project names", { cause: error });
|
461
474
|
}
|
462
475
|
}
|
463
|
-
async
|
476
|
+
async getProject(projectId) {
|
464
477
|
try {
|
465
478
|
const project = await this.loadProject(projectId);
|
466
|
-
return
|
479
|
+
return {
|
480
|
+
instances: Object.values(project.instances),
|
481
|
+
hubs: Object.values(project.hubs)
|
482
|
+
};
|
467
483
|
} catch (error) {
|
468
484
|
throw new Error("Failed to get project instances", { cause: error });
|
469
485
|
}
|
@@ -481,34 +497,40 @@ class LocalProjectBackend {
|
|
481
497
|
throw new Error("Failed to create project instance", { cause: error });
|
482
498
|
}
|
483
499
|
}
|
484
|
-
async
|
485
|
-
try {
|
486
|
-
return await this.withInstance(projectId, instanceId, (instance) => {
|
487
|
-
instance.position = position;
|
488
|
-
return instance;
|
489
|
-
});
|
490
|
-
} catch (error) {
|
491
|
-
throw new Error("Failed to move project instance", { cause: error });
|
492
|
-
}
|
493
|
-
}
|
494
|
-
async updateInstanceArgs(projectId, instanceId, args) {
|
495
|
-
try {
|
496
|
-
return await this.withInstance(projectId, instanceId, (instance) => {
|
497
|
-
instance.args = args;
|
498
|
-
return instance;
|
499
|
-
});
|
500
|
-
} catch (error) {
|
501
|
-
throw new Error("Failed to update project instance arguments", { cause: error });
|
502
|
-
}
|
503
|
-
}
|
504
|
-
async updateInstanceInputs(projectId, instanceId, inputs) {
|
500
|
+
async updateInstance(projectId, instanceId, patch) {
|
505
501
|
try {
|
506
502
|
return await this.withInstance(projectId, instanceId, (instance) => {
|
507
|
-
|
503
|
+
if (patch.args) {
|
504
|
+
instance.args = patch.args;
|
505
|
+
}
|
506
|
+
if (patch.position) {
|
507
|
+
instance.position = patch.position;
|
508
|
+
}
|
509
|
+
if (patch.inputs) {
|
510
|
+
if (Object.keys(patch.inputs).length > 0) {
|
511
|
+
instance.inputs = patch.inputs;
|
512
|
+
} else {
|
513
|
+
delete instance.inputs;
|
514
|
+
}
|
515
|
+
}
|
516
|
+
if (patch.hubInputs) {
|
517
|
+
if (Object.keys(patch.hubInputs).length > 0) {
|
518
|
+
instance.hubInputs = patch.hubInputs;
|
519
|
+
} else {
|
520
|
+
delete instance.hubInputs;
|
521
|
+
}
|
522
|
+
}
|
523
|
+
if (patch.injectionInputs) {
|
524
|
+
if (patch.injectionInputs.length > 0) {
|
525
|
+
instance.injectionInputs = patch.injectionInputs;
|
526
|
+
} else {
|
527
|
+
delete instance.injectionInputs;
|
528
|
+
}
|
529
|
+
}
|
508
530
|
return instance;
|
509
531
|
});
|
510
532
|
} catch (error) {
|
511
|
-
throw new Error("Failed to update project instance
|
533
|
+
throw new Error("Failed to update project instance", { cause: error });
|
512
534
|
}
|
513
535
|
}
|
514
536
|
async deleteInstance(projectId, instanceId) {
|
@@ -519,14 +541,21 @@ class LocalProjectBackend {
|
|
519
541
|
}
|
520
542
|
delete project.instances[instanceId];
|
521
543
|
for (const otherInstance of Object.values(project.instances)) {
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
544
|
+
if (!otherInstance.inputs) {
|
545
|
+
continue;
|
546
|
+
}
|
547
|
+
this.deleteInstanceReferences(otherInstance.inputs, instanceId);
|
548
|
+
if (Object.keys(otherInstance.inputs).length === 0) {
|
549
|
+
delete otherInstance.inputs;
|
550
|
+
}
|
551
|
+
}
|
552
|
+
for (const hub of Object.values(project.hubs)) {
|
553
|
+
if (!hub.inputs) {
|
554
|
+
continue;
|
555
|
+
}
|
556
|
+
hub.inputs = hub.inputs.filter((input) => input.instanceId !== instanceId);
|
557
|
+
if (hub.inputs.length === 0) {
|
558
|
+
delete hub.inputs;
|
530
559
|
}
|
531
560
|
}
|
532
561
|
});
|
@@ -534,6 +563,14 @@ class LocalProjectBackend {
|
|
534
563
|
throw new Error("Failed to delete project instance", { cause: error });
|
535
564
|
}
|
536
565
|
}
|
566
|
+
deleteInstanceReferences(inputs, instanceId) {
|
567
|
+
for (const [inputKey, input] of Object.entries(inputs)) {
|
568
|
+
inputs[inputKey] = input.filter((inputItem) => inputItem.instanceId !== instanceId);
|
569
|
+
if (inputs[inputKey].length === 0) {
|
570
|
+
delete inputs[inputKey];
|
571
|
+
}
|
572
|
+
}
|
573
|
+
}
|
537
574
|
async renameInstance(projectId, instanceId, newName) {
|
538
575
|
try {
|
539
576
|
return await this.withProject(projectId, (project) => {
|
@@ -550,24 +587,114 @@ class LocalProjectBackend {
|
|
550
587
|
instance.name = newName;
|
551
588
|
project.instances[newInstanceId] = instance;
|
552
589
|
for (const otherInstance of Object.values(project.instances)) {
|
553
|
-
for (const
|
554
|
-
|
555
|
-
for (const inputItem of input) {
|
556
|
-
if (inputItem.instanceId === instanceId) {
|
557
|
-
inputItem.instanceId = instance.id;
|
558
|
-
}
|
559
|
-
}
|
560
|
-
} else if (input.instanceId === instanceId) {
|
561
|
-
input.instanceId = instance.id;
|
562
|
-
}
|
590
|
+
for (const inputs of Object.values(otherInstance.inputs ?? {})) {
|
591
|
+
this.renameInstanceReferences(inputs, instanceId, instance.id);
|
563
592
|
}
|
564
593
|
}
|
594
|
+
for (const hub of Object.values(project.hubs)) {
|
595
|
+
this.renameInstanceReferences(hub.inputs ?? [], instanceId, instance.id);
|
596
|
+
}
|
565
597
|
return instance;
|
566
598
|
});
|
567
599
|
} catch (error) {
|
568
600
|
throw new Error("Failed to rename project instance", { cause: error });
|
569
601
|
}
|
570
602
|
}
|
603
|
+
renameInstanceReferences(inputs, instanceId, newInstanceId) {
|
604
|
+
for (const input of inputs) {
|
605
|
+
if (input.instanceId === instanceId) {
|
606
|
+
input.instanceId = newInstanceId;
|
607
|
+
}
|
608
|
+
}
|
609
|
+
}
|
610
|
+
async createHub(projectId, hub) {
|
611
|
+
try {
|
612
|
+
return await this.withProject(projectId, (project) => {
|
613
|
+
if (project.hubs[hub.id]) {
|
614
|
+
throw new Error(`Hub ${hub.id} already exists`);
|
615
|
+
}
|
616
|
+
project.hubs[hub.id] = hub;
|
617
|
+
return hub;
|
618
|
+
});
|
619
|
+
} catch (error) {
|
620
|
+
throw new Error("Failed to create project hub", { cause: error });
|
621
|
+
}
|
622
|
+
}
|
623
|
+
async updateHub(projectId, hubId, patch) {
|
624
|
+
try {
|
625
|
+
return await this.withProject(projectId, (project) => {
|
626
|
+
const hub = project.hubs[hubId];
|
627
|
+
if (!hub) {
|
628
|
+
throw new Error(`Hub ${hubId} not found`);
|
629
|
+
}
|
630
|
+
if (patch.position) {
|
631
|
+
hub.position = patch.position;
|
632
|
+
}
|
633
|
+
if (patch.inputs) {
|
634
|
+
if (patch.inputs.length > 0) {
|
635
|
+
hub.inputs = patch.inputs;
|
636
|
+
} else {
|
637
|
+
delete hub.inputs;
|
638
|
+
}
|
639
|
+
}
|
640
|
+
if (patch.injectionInputs) {
|
641
|
+
if (patch.injectionInputs.length > 0) {
|
642
|
+
hub.injectionInputs = patch.injectionInputs;
|
643
|
+
} else {
|
644
|
+
delete hub.injectionInputs;
|
645
|
+
}
|
646
|
+
}
|
647
|
+
return hub;
|
648
|
+
});
|
649
|
+
} catch (error) {
|
650
|
+
throw new Error("Failed to update project hub", { cause: error });
|
651
|
+
}
|
652
|
+
}
|
653
|
+
async deleteHub(projectId, hubId) {
|
654
|
+
try {
|
655
|
+
await this.withProject(projectId, (project) => {
|
656
|
+
if (!project.hubs[hubId]) {
|
657
|
+
throw new Error(`Hub ${hubId} not found`);
|
658
|
+
}
|
659
|
+
delete project.hubs[hubId];
|
660
|
+
for (const instance of Object.values(project.instances)) {
|
661
|
+
if (instance.hubInputs) {
|
662
|
+
this.deleteHubReferences(instance.hubInputs, hubId);
|
663
|
+
if (Object.keys(instance.hubInputs).length === 0) {
|
664
|
+
delete instance.hubInputs;
|
665
|
+
}
|
666
|
+
}
|
667
|
+
if (instance.injectionInputs) {
|
668
|
+
instance.injectionInputs = instance.injectionInputs.filter(
|
669
|
+
(input) => input.hubId !== hubId
|
670
|
+
);
|
671
|
+
if (instance.injectionInputs.length === 0) {
|
672
|
+
delete instance.injectionInputs;
|
673
|
+
}
|
674
|
+
}
|
675
|
+
}
|
676
|
+
for (const otherHub of Object.values(project.hubs)) {
|
677
|
+
if (!otherHub.injectionInputs) {
|
678
|
+
continue;
|
679
|
+
}
|
680
|
+
otherHub.injectionInputs = otherHub.injectionInputs.filter((input) => input.hubId !== hubId);
|
681
|
+
if (otherHub.injectionInputs.length === 0) {
|
682
|
+
delete otherHub.injectionInputs;
|
683
|
+
}
|
684
|
+
}
|
685
|
+
});
|
686
|
+
} catch (error) {
|
687
|
+
throw new Error("Failed to delete project hub", { cause: error });
|
688
|
+
}
|
689
|
+
}
|
690
|
+
deleteHubReferences(inputs, hubId) {
|
691
|
+
for (const [inputKey, input] of Object.entries(inputs)) {
|
692
|
+
inputs[inputKey] = input.filter((inputItem) => inputItem.hubId !== hubId);
|
693
|
+
if (inputs[inputKey].length === 0) {
|
694
|
+
delete inputs[inputKey];
|
695
|
+
}
|
696
|
+
}
|
697
|
+
}
|
571
698
|
getProjectPath(projectId) {
|
572
699
|
return `${this.projectsDir}/${projectId}.json`;
|
573
700
|
}
|
@@ -578,14 +705,14 @@ class LocalProjectBackend {
|
|
578
705
|
return projectModelSchema.parse(JSON.parse(content));
|
579
706
|
} catch (error) {
|
580
707
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
581
|
-
return { instances: {} };
|
708
|
+
return { instances: {}, hubs: {} };
|
582
709
|
}
|
583
710
|
throw error;
|
584
711
|
}
|
585
712
|
}
|
586
713
|
async writeProject(projectId, project) {
|
587
714
|
const projectPath = this.getProjectPath(projectId);
|
588
|
-
const content = JSON.stringify(project,
|
715
|
+
const content = JSON.stringify(project, void 0, 2);
|
589
716
|
await writeFile(projectPath, content);
|
590
717
|
}
|
591
718
|
async withInstance(projectId, instanceId, callback) {
|
@@ -653,19 +780,10 @@ class ProjectManager {
|
|
653
780
|
this.logger = logger;
|
654
781
|
}
|
655
782
|
async getCompositeInstance(projectId, instanceId) {
|
656
|
-
const
|
657
|
-
const instance = instances.find((instance2) => instance2.id === instanceId);
|
658
|
-
const instanceMap = /* @__PURE__ */ new Map();
|
659
|
-
for (const instance2 of instances) {
|
660
|
-
instanceMap.set(instance2.id, instance2);
|
661
|
-
}
|
662
|
-
if (!instance) {
|
663
|
-
throw new Error(`instance not found: ${instanceId}`);
|
664
|
-
}
|
665
|
-
const calculator = new InputHashCalculator(instanceMap);
|
783
|
+
const { resolveInputHash } = await this.prepareInputHashResolver(projectId, instanceId);
|
666
784
|
const [compositeInstance, actualInputHash] = await Promise.all([
|
667
785
|
this.stateBackend.getCompositeInstance(projectId, instanceId),
|
668
|
-
|
786
|
+
resolveInputHash(instanceId)
|
669
787
|
]);
|
670
788
|
if (compositeInstance && compositeInstance.inputHash === actualInputHash) {
|
671
789
|
return compositeInstance;
|
@@ -678,7 +796,7 @@ class ProjectManager {
|
|
678
796
|
projectId,
|
679
797
|
instanceId,
|
680
798
|
actualInputHash,
|
681
|
-
|
799
|
+
resolveInputHash
|
682
800
|
);
|
683
801
|
await this.operationManager.launch({
|
684
802
|
type: "evaluate",
|
@@ -692,18 +810,8 @@ class ProjectManager {
|
|
692
810
|
await this.updateInstanceChildren(projectId, createdInstance);
|
693
811
|
return createdInstance;
|
694
812
|
}
|
695
|
-
async
|
696
|
-
const instance = await this.projectBackend.
|
697
|
-
await this.updateInstanceChildren(projectId, instance);
|
698
|
-
return instance;
|
699
|
-
}
|
700
|
-
async updateInstanceInputs(projectId, instanceId, inputs) {
|
701
|
-
const instance = await this.projectBackend.updateInstanceInputs(projectId, instanceId, inputs);
|
702
|
-
await this.updateInstanceChildren(projectId, instance);
|
703
|
-
return instance;
|
704
|
-
}
|
705
|
-
async moveInstance(projectId, instanceId, position) {
|
706
|
-
const instance = await this.projectBackend.moveInstance(projectId, instanceId, position);
|
813
|
+
async updateInstance(projectId, instanceId, patch) {
|
814
|
+
const instance = await this.projectBackend.updateInstance(projectId, instanceId, patch);
|
707
815
|
await this.updateInstanceChildren(projectId, instance);
|
708
816
|
return instance;
|
709
817
|
}
|
@@ -719,7 +827,10 @@ class ProjectManager {
|
|
719
827
|
]);
|
720
828
|
}
|
721
829
|
async updateInstanceChildren(projectId, instance) {
|
722
|
-
const library = await this.
|
830
|
+
const { resolveInputHash, library } = await this.prepareInputHashResolver(
|
831
|
+
projectId,
|
832
|
+
instance.id
|
833
|
+
);
|
723
834
|
const component = library.components[instance.type];
|
724
835
|
if (!component) {
|
725
836
|
return;
|
@@ -727,14 +838,8 @@ class ProjectManager {
|
|
727
838
|
if (isUnitModel(component)) {
|
728
839
|
return;
|
729
840
|
}
|
730
|
-
const
|
731
|
-
const instanceMap = /* @__PURE__ */ new Map();
|
732
|
-
for (const instance2 of instances) {
|
733
|
-
instanceMap.set(instance2.id, instance2);
|
734
|
-
}
|
735
|
-
const inputHashCalculator = new InputHashCalculator(instanceMap);
|
841
|
+
const expectedInputHash = await resolveInputHash(instance.id);
|
736
842
|
const inputHash = await this.stateBackend.getCompositeInstanceInputHash(projectId, instance.id);
|
737
|
-
const expectedInputHash = await inputHashCalculator.calculate(instance);
|
738
843
|
if (inputHash !== expectedInputHash) {
|
739
844
|
this.logger.info("re-evaluating instance since input hash has changed", {
|
740
845
|
projectId,
|
@@ -747,13 +852,12 @@ class ProjectManager {
|
|
747
852
|
});
|
748
853
|
}
|
749
854
|
}
|
750
|
-
async waitForCompositeInstance(projectId, instanceId, expectedInputHash,
|
855
|
+
async waitForCompositeInstance(projectId, instanceId, expectedInputHash, resolveInputHash) {
|
751
856
|
for await (const instance of this.operationManager.watchCompositeInstance(
|
752
857
|
projectId,
|
753
858
|
instanceId
|
754
859
|
)) {
|
755
|
-
const
|
756
|
-
const actualInputHash = await calculator.calculate(instance.instance);
|
860
|
+
const actualInputHash = await resolveInputHash(instanceId);
|
757
861
|
if (actualInputHash !== expectedInputHash) {
|
758
862
|
throw new Error("Composite instance input hash changed while waiting for evaluation");
|
759
863
|
}
|
@@ -761,6 +865,54 @@ class ProjectManager {
|
|
761
865
|
}
|
762
866
|
throw new Error("Composite instance stream ended without receiving the expected instance");
|
763
867
|
}
|
868
|
+
async prepareInputHashResolver(projectId, instanceId) {
|
869
|
+
const { instances, hubs } = await this.projectBackend.getProject(projectId);
|
870
|
+
const library = await this.library.loadLibrary();
|
871
|
+
const filteredInstances = instances.filter((instance2) => instance2.type in library.components);
|
872
|
+
const states = await this.stateBackend.getInstanceStates(projectId);
|
873
|
+
const stateMap = new Map(states.map((state) => [state.id, state]));
|
874
|
+
const instance = filteredInstances.find((instance2) => instance2.id === instanceId);
|
875
|
+
if (!instance) {
|
876
|
+
throw new Error(`Instance not found: ${instanceId}`);
|
877
|
+
}
|
878
|
+
const inputResolverNodes = /* @__PURE__ */ new Map();
|
879
|
+
for (const instance2 of filteredInstances) {
|
880
|
+
inputResolverNodes.set(`instance:${instance2.id}`, {
|
881
|
+
kind: "instance",
|
882
|
+
instance: instance2,
|
883
|
+
component: library.components[instance2.type]
|
884
|
+
});
|
885
|
+
}
|
886
|
+
for (const hub of hubs) {
|
887
|
+
inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub });
|
888
|
+
}
|
889
|
+
const resolveInputs = createInputResolver(
|
890
|
+
inputResolverNodes,
|
891
|
+
this.logger.child({ resolver: "input-resolver" })
|
892
|
+
);
|
893
|
+
const inputHashNodes = /* @__PURE__ */ new Map();
|
894
|
+
for (const instance2 of filteredInstances) {
|
895
|
+
const output = await resolveInputs(`instance:${instance2.id}`);
|
896
|
+
if (output.kind !== "instance") {
|
897
|
+
throw new Error("Expected instance node");
|
898
|
+
}
|
899
|
+
inputHashNodes.set(instance2.id, {
|
900
|
+
instance: instance2,
|
901
|
+
resolvedInputs: output.resolvedInputs,
|
902
|
+
state: stateMap.get(instance2.id),
|
903
|
+
sourceHash: void 0
|
904
|
+
// implement source hash
|
905
|
+
});
|
906
|
+
}
|
907
|
+
const resolveInputHash = createInputHashResolver(
|
908
|
+
inputHashNodes,
|
909
|
+
this.logger.child({ resolver: "input-hash-resolver" })
|
910
|
+
);
|
911
|
+
return {
|
912
|
+
resolveInputHash,
|
913
|
+
library
|
914
|
+
};
|
915
|
+
}
|
764
916
|
static create(projectBackend, stateBackend, operationManager, library, logger) {
|
765
917
|
return new ProjectManager(
|
766
918
|
projectBackend,
|
@@ -788,8 +940,13 @@ done
|
|
788
940
|
|
789
941
|
# Create files
|
790
942
|
for key in "\${filesKeys[@]}"; do
|
791
|
-
|
792
|
-
|
943
|
+
isBinary=$(jq -r ".files[\\"$key\\"].isBinary // false" <<<"$data")
|
944
|
+
content=$(jq -r ".files[\\"$key\\"].content" <<<"$data")
|
945
|
+
if [ "$isBinary" = "true" ]; then
|
946
|
+
echo -n "$content" | base64 -d > "$key"
|
947
|
+
else
|
948
|
+
echo -n "$content" > "$key"
|
949
|
+
fi
|
793
950
|
done
|
794
951
|
|
795
952
|
# Execute the command, keeping stdin/stdout open
|
@@ -863,82 +1020,151 @@ function createTerminalBackend(config, logger) {
|
|
863
1020
|
|
864
1021
|
const notAttachedTerminalLifetime = 30 * 1e3;
|
865
1022
|
class TerminalManager {
|
866
|
-
constructor(terminalBackend, logger) {
|
1023
|
+
constructor(terminalBackend, stateBackend, runnerBackend, logger) {
|
867
1024
|
this.terminalBackend = terminalBackend;
|
1025
|
+
this.stateBackend = stateBackend;
|
1026
|
+
this.runnerBackend = runnerBackend;
|
868
1027
|
this.logger = logger;
|
869
1028
|
}
|
870
|
-
|
871
|
-
|
872
|
-
const
|
1029
|
+
managedTerminals = /* @__PURE__ */ new Map();
|
1030
|
+
async createSession(projectId, instanceId, terminalName) {
|
1031
|
+
const terminalSession = {
|
873
1032
|
id: randomUUID(),
|
874
|
-
|
875
|
-
attached: false,
|
876
|
-
stdin: new PassThrough(),
|
877
|
-
stdout: new PassThrough(),
|
878
|
-
history: []
|
1033
|
+
terminalName
|
879
1034
|
};
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
return
|
1035
|
+
await this.stateBackend.putTerminalSession(projectId, instanceId, terminalSession);
|
1036
|
+
this.logger.info({ msg: "terminal session created", id: terminalSession.id });
|
1037
|
+
await this.createManagedTerminal(projectId, instanceId, terminalSession);
|
1038
|
+
return terminalSession;
|
1039
|
+
}
|
1040
|
+
async ensureSessionCreated(projectId, instanceId, terminalName) {
|
1041
|
+
const terminalSessions = await this.stateBackend.getTerminalSessions(projectId, instanceId);
|
1042
|
+
let session = terminalSessions.find((session2) => session2.terminalName === terminalName);
|
1043
|
+
if (session) {
|
1044
|
+
this.logger.info({ msg: "reusing existing terminal session", id: session.id });
|
1045
|
+
} else {
|
1046
|
+
this.logger.info("creating new terminal session");
|
1047
|
+
session = await this.createSession(projectId, instanceId, terminalName);
|
1048
|
+
}
|
1049
|
+
if (!this.managedTerminals.has(session.id)) {
|
1050
|
+
this.logger.info({ msg: "no managed terminal found, creating new one", id: session.id });
|
1051
|
+
await this.createManagedTerminal(projectId, instanceId, session);
|
1052
|
+
}
|
1053
|
+
return session;
|
899
1054
|
}
|
900
|
-
close(
|
901
|
-
|
902
|
-
|
903
|
-
|
1055
|
+
close(sessionId) {
|
1056
|
+
this.logger.info({ msg: "closing terminal session", id: sessionId });
|
1057
|
+
const managedTerminal = this.managedTerminals.get(sessionId);
|
1058
|
+
if (managedTerminal) {
|
1059
|
+
managedTerminal.abortController.abort();
|
904
1060
|
}
|
905
|
-
terminal.abortController.abort();
|
906
|
-
this.terminals.delete(id);
|
907
|
-
this.logger.info("terminal closed", { id });
|
908
1061
|
}
|
909
|
-
attach(
|
910
|
-
|
1062
|
+
async attach(projectId, instanceId, sessionId, stdin, stdout, signal) {
|
1063
|
+
this.logger.info({ msg: "attaching terminal", projectId, instanceId, sessionId });
|
1064
|
+
let terminal = this.managedTerminals.get(sessionId);
|
911
1065
|
if (!terminal) {
|
912
|
-
|
1066
|
+
this.logger.info({ msg: "no managed terminal found, creating new one", sessionId });
|
1067
|
+
const session = await this.stateBackend.getTerminalSession(projectId, instanceId, sessionId);
|
1068
|
+
if (!session) {
|
1069
|
+
throw new Error(`Terminal session "${sessionId}" not found`);
|
1070
|
+
}
|
1071
|
+
terminal = await this.createManagedTerminal(projectId, instanceId, session);
|
1072
|
+
terminal.history = await this.stateBackend.getTerminalSessionHistory(sessionId);
|
1073
|
+
}
|
1074
|
+
if (terminal.attached) {
|
1075
|
+
throw new Error(`Terminal session "${sessionId}" is already attached`);
|
913
1076
|
}
|
914
1077
|
terminal.attached = true;
|
915
1078
|
for (const line of terminal.history) {
|
916
|
-
stdout.write(line
|
1079
|
+
stdout.write(line);
|
917
1080
|
}
|
918
|
-
this.logger.info("history replayed",
|
1081
|
+
this.logger.info({ msg: "history replayed", count: terminal.history.length, sessionId });
|
919
1082
|
stdin.pipe(terminal.stdin);
|
920
1083
|
terminal.stdout.pipe(stdout);
|
921
|
-
this.logger.info("terminal attached",
|
1084
|
+
this.logger.info({ msg: "terminal attached", id: sessionId });
|
922
1085
|
signal.addEventListener("abort", () => {
|
923
1086
|
terminal.attached = false;
|
924
|
-
this.logger.info("terminal detached",
|
1087
|
+
this.logger.info({ msg: "terminal detached", id: sessionId });
|
925
1088
|
});
|
926
1089
|
}
|
1090
|
+
async createManagedTerminal(projectId, instanceId, terminalSession) {
|
1091
|
+
const [instanceType, instanceName] = parseInstanceId(instanceId);
|
1092
|
+
const factory = await this.runnerBackend.getTerminalFactory(
|
1093
|
+
{ projectId, instanceType, instanceName },
|
1094
|
+
terminalSession.terminalName
|
1095
|
+
);
|
1096
|
+
if (!factory) {
|
1097
|
+
throw new Error(
|
1098
|
+
`Terminal factory for instance "${instanceId}" with name "${terminalSession.terminalName}" not found`
|
1099
|
+
);
|
1100
|
+
}
|
1101
|
+
const managedTerminal = {
|
1102
|
+
sessionId: terminalSession.id,
|
1103
|
+
abortController: new AbortController(),
|
1104
|
+
attached: false,
|
1105
|
+
stdin: new PassThrough(),
|
1106
|
+
stdout: new PassThrough(),
|
1107
|
+
history: []
|
1108
|
+
};
|
1109
|
+
managedTerminal.stdout.on("data", (data) => {
|
1110
|
+
const line = String(data);
|
1111
|
+
managedTerminal.history.push(String(data));
|
1112
|
+
void this.persistHistory.call([terminalSession.id, line]);
|
1113
|
+
});
|
1114
|
+
this.managedTerminals.set(managedTerminal.sessionId, managedTerminal);
|
1115
|
+
void this.terminalBackend.run({
|
1116
|
+
factory,
|
1117
|
+
stdin: managedTerminal.stdin,
|
1118
|
+
stdout: managedTerminal.stdout,
|
1119
|
+
signal: managedTerminal.abortController.signal
|
1120
|
+
}).catch((error) => {
|
1121
|
+
this.logger.error({
|
1122
|
+
msg: "managed terminal failed",
|
1123
|
+
id: managedTerminal.sessionId,
|
1124
|
+
error
|
1125
|
+
});
|
1126
|
+
}).finally(() => {
|
1127
|
+
this.logger.info({ msg: "managed terminal closed", id: managedTerminal.sessionId });
|
1128
|
+
this.managedTerminals.delete(managedTerminal.sessionId);
|
1129
|
+
});
|
1130
|
+
setTimeout(() => this.closeTerminalIfNotAttached(managedTerminal), notAttachedTerminalLifetime);
|
1131
|
+
this.logger.info({ msg: "managed terminal created", id: managedTerminal.sessionId });
|
1132
|
+
return managedTerminal;
|
1133
|
+
}
|
927
1134
|
closeTerminalIfNotAttached(terminal) {
|
928
|
-
if (!this.
|
1135
|
+
if (!this.managedTerminals.has(terminal.sessionId)) {
|
929
1136
|
return;
|
930
1137
|
}
|
931
1138
|
if (!terminal.attached) {
|
932
|
-
this.logger.info(
|
1139
|
+
this.logger.info({
|
1140
|
+
msg: "terminal not attached for too long, closing",
|
1141
|
+
id: terminal.sessionId
|
1142
|
+
});
|
933
1143
|
terminal.abortController.abort();
|
934
|
-
this.
|
1144
|
+
this.managedTerminals.delete(terminal.sessionId);
|
935
1145
|
return;
|
936
1146
|
}
|
937
1147
|
setTimeout(() => this.closeTerminalIfNotAttached(terminal), notAttachedTerminalLifetime);
|
938
1148
|
}
|
939
|
-
static create(terminalBackend, logger) {
|
940
|
-
return new TerminalManager(
|
1149
|
+
static create(terminalBackend, stateBackend, runnerBackend, logger) {
|
1150
|
+
return new TerminalManager(
|
1151
|
+
terminalBackend,
|
1152
|
+
stateBackend,
|
1153
|
+
runnerBackend,
|
1154
|
+
logger.child({ service: "TerminalManager" })
|
1155
|
+
);
|
941
1156
|
}
|
1157
|
+
persistHistory = funnel(
|
1158
|
+
(entries) => {
|
1159
|
+
this.logger.trace({ msg: "persisting history lines", count: entries.length });
|
1160
|
+
void this.stateBackend.appendTerminalSessionHistory(entries);
|
1161
|
+
},
|
1162
|
+
{
|
1163
|
+
minQuietPeriodMs: 100,
|
1164
|
+
maxBurstDurationMs: 200,
|
1165
|
+
reducer: arrayAccumulator
|
1166
|
+
}
|
1167
|
+
);
|
942
1168
|
}
|
943
1169
|
|
944
1170
|
class InvalidInstanceStatusError extends Error {
|
@@ -1008,31 +1234,60 @@ class LocalRunnerBackend {
|
|
1008
1234
|
}
|
1009
1235
|
);
|
1010
1236
|
}
|
1011
|
-
getTerminalFactory(options) {
|
1237
|
+
getTerminalFactory(options, terminalName) {
|
1012
1238
|
return this.pulumiProjectHost.runEmpty(
|
1013
1239
|
options.projectId,
|
1014
1240
|
options.instanceType,
|
1015
1241
|
options.instanceName,
|
1016
1242
|
async (stack) => {
|
1017
1243
|
const outputs = await stack.outputs();
|
1018
|
-
if (!outputs["$
|
1244
|
+
if (!outputs["$terminals"]) {
|
1019
1245
|
return null;
|
1020
1246
|
}
|
1021
|
-
|
1247
|
+
const terminals = z.array(instanceTerminalSchema).parse(outputs["$terminals"].value);
|
1248
|
+
const terminal = terminals.find((t) => t.name === terminalName);
|
1249
|
+
if (!terminal) {
|
1250
|
+
return null;
|
1251
|
+
}
|
1252
|
+
return terminal;
|
1022
1253
|
}
|
1023
1254
|
);
|
1024
1255
|
}
|
1025
|
-
|
1256
|
+
getPageContent(options, pageName) {
|
1026
1257
|
return this.pulumiProjectHost.runEmpty(
|
1027
1258
|
options.projectId,
|
1028
1259
|
options.instanceType,
|
1029
1260
|
options.instanceName,
|
1030
1261
|
async (stack) => {
|
1031
1262
|
const outputs = await stack.outputs();
|
1032
|
-
if (!outputs["$
|
1263
|
+
if (!outputs["$pages"]) {
|
1264
|
+
return null;
|
1265
|
+
}
|
1266
|
+
const pages = z.array(instancePageSchema).parse(outputs["$pages"].value);
|
1267
|
+
const page = pages.find((p) => p.name === pageName);
|
1268
|
+
if (!page) {
|
1033
1269
|
return null;
|
1034
1270
|
}
|
1035
|
-
return
|
1271
|
+
return page.content;
|
1272
|
+
}
|
1273
|
+
);
|
1274
|
+
}
|
1275
|
+
getFileContent(options, fileName) {
|
1276
|
+
return this.pulumiProjectHost.runEmpty(
|
1277
|
+
options.projectId,
|
1278
|
+
options.instanceType,
|
1279
|
+
options.instanceName,
|
1280
|
+
async (stack) => {
|
1281
|
+
const outputs = await stack.outputs();
|
1282
|
+
if (!outputs["$files"]) {
|
1283
|
+
return null;
|
1284
|
+
}
|
1285
|
+
const files = z.array(instanceFileSchema).parse(outputs["$files"].value);
|
1286
|
+
const file = files.find((f) => f.name === fileName);
|
1287
|
+
if (!file) {
|
1288
|
+
return null;
|
1289
|
+
}
|
1290
|
+
return file.content;
|
1036
1291
|
}
|
1037
1292
|
);
|
1038
1293
|
}
|
@@ -1050,23 +1305,26 @@ class LocalRunnerBackend {
|
|
1050
1305
|
...mapValues(options.config, (value) => ({ value })),
|
1051
1306
|
...mapValues(options.secrets, (value) => ({ value, secret: true }))
|
1052
1307
|
};
|
1053
|
-
void this.
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1308
|
+
void this.updateWorker(options, configMap);
|
1309
|
+
}
|
1310
|
+
async updateWorker(options, configMap) {
|
1311
|
+
const instanceId = LocalRunnerBackend.getInstanceId(options);
|
1312
|
+
try {
|
1313
|
+
await this.pulumiProjectHost.runLocal(
|
1314
|
+
options.projectId,
|
1315
|
+
options.instanceType,
|
1316
|
+
options.instanceName,
|
1317
|
+
() => this.resolveProjectPath(options.source),
|
1318
|
+
async (stack) => {
|
1319
|
+
await stack.setAllConfig(configMap);
|
1320
|
+
this.updateState({
|
1321
|
+
id: instanceId,
|
1322
|
+
status: "updating",
|
1323
|
+
currentResourceCount: 0,
|
1324
|
+
totalResourceCount: 0
|
1325
|
+
});
|
1326
|
+
let currentResourceCount = 0;
|
1327
|
+
let totalResourceCount = 0;
|
1070
1328
|
await runWithRetryOnError(
|
1071
1329
|
async () => {
|
1072
1330
|
await stack.up({
|
@@ -1090,7 +1348,7 @@ class LocalRunnerBackend {
|
|
1090
1348
|
}
|
1091
1349
|
},
|
1092
1350
|
onOutput: (message) => {
|
1093
|
-
this.updateState({ id: instanceId, message });
|
1351
|
+
this.updateState({ id: instanceId, logLine: message });
|
1094
1352
|
if (this.printOutput) {
|
1095
1353
|
console.log(message);
|
1096
1354
|
}
|
@@ -1101,21 +1359,28 @@ class LocalRunnerBackend {
|
|
1101
1359
|
this.updateState({
|
1102
1360
|
id: instanceId,
|
1103
1361
|
status: "created",
|
1362
|
+
totalResourceCount: currentResourceCount,
|
1104
1363
|
...extraOutputs
|
1105
1364
|
});
|
1106
1365
|
},
|
1107
|
-
(error) =>
|
1366
|
+
async (error) => {
|
1367
|
+
const isUnlocked = await this.pulumiProjectHost.tryUnlockStack(stack, error);
|
1368
|
+
if (isUnlocked) return true;
|
1369
|
+
const isResolved = await this.tryInstallMissingDependencies(error);
|
1370
|
+
if (isResolved) return true;
|
1371
|
+
return false;
|
1372
|
+
}
|
1108
1373
|
);
|
1109
|
-
}
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1374
|
+
},
|
1375
|
+
configMap
|
1376
|
+
);
|
1377
|
+
} catch (error) {
|
1378
|
+
this.updateState({
|
1379
|
+
id: instanceId,
|
1380
|
+
status: "error",
|
1381
|
+
error: errorToString(error)
|
1382
|
+
});
|
1383
|
+
}
|
1119
1384
|
}
|
1120
1385
|
async destroy(options) {
|
1121
1386
|
const currentStatus = await this.validateStatus(options, [
|
@@ -1124,24 +1389,27 @@ class LocalRunnerBackend {
|
|
1124
1389
|
"created",
|
1125
1390
|
"error"
|
1126
1391
|
]);
|
1127
|
-
if (currentStatus === "destroying"
|
1392
|
+
if (currentStatus === "destroying") {
|
1128
1393
|
return;
|
1129
1394
|
}
|
1130
|
-
void this.
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
currentResourceCount
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1395
|
+
void this.destroyWorker(options);
|
1396
|
+
}
|
1397
|
+
async destroyWorker(options) {
|
1398
|
+
const instanceId = LocalRunnerBackend.getInstanceId(options);
|
1399
|
+
try {
|
1400
|
+
await this.pulumiProjectHost.runEmpty(
|
1401
|
+
options.projectId,
|
1402
|
+
options.instanceType,
|
1403
|
+
options.instanceName,
|
1404
|
+
async (stack) => {
|
1405
|
+
const summary = await stack.workspace.stack();
|
1406
|
+
let currentResourceCount = summary?.resourceCount ?? 0;
|
1407
|
+
this.updateState({
|
1408
|
+
id: instanceId,
|
1409
|
+
status: "destroying",
|
1410
|
+
currentResourceCount,
|
1411
|
+
totalResourceCount: currentResourceCount
|
1412
|
+
});
|
1145
1413
|
await runWithRetryOnError(
|
1146
1414
|
async () => {
|
1147
1415
|
await stack.destroy({
|
@@ -1157,7 +1425,7 @@ class LocalRunnerBackend {
|
|
1157
1425
|
}
|
1158
1426
|
},
|
1159
1427
|
onOutput: (message) => {
|
1160
|
-
this.updateState({ id: instanceId, message });
|
1428
|
+
this.updateState({ id: instanceId, logLine: message });
|
1161
1429
|
if (this.printOutput) {
|
1162
1430
|
console.log(message);
|
1163
1431
|
}
|
@@ -1168,20 +1436,21 @@ class LocalRunnerBackend {
|
|
1168
1436
|
this.updateState({
|
1169
1437
|
id: instanceId,
|
1170
1438
|
status: "not_created",
|
1439
|
+
totalResourceCount: currentResourceCount,
|
1171
1440
|
...extraOutputs
|
1172
1441
|
});
|
1173
1442
|
},
|
1174
|
-
(error) =>
|
1443
|
+
(error) => this.pulumiProjectHost.tryUnlockStack(stack, error)
|
1175
1444
|
);
|
1176
|
-
} catch (e) {
|
1177
|
-
this.updateState({
|
1178
|
-
id: instanceId,
|
1179
|
-
status: "error",
|
1180
|
-
error: e instanceof Error ? e.message : String(e)
|
1181
|
-
});
|
1182
1445
|
}
|
1183
|
-
|
1184
|
-
)
|
1446
|
+
);
|
1447
|
+
} catch (error) {
|
1448
|
+
this.updateState({
|
1449
|
+
id: instanceId,
|
1450
|
+
status: "error",
|
1451
|
+
error: errorToString(error)
|
1452
|
+
});
|
1453
|
+
}
|
1185
1454
|
}
|
1186
1455
|
async refresh(options) {
|
1187
1456
|
const currentStatus = await this.validateStatus(options, [
|
@@ -1193,26 +1462,38 @@ class LocalRunnerBackend {
|
|
1193
1462
|
if (currentStatus === "refreshing") {
|
1194
1463
|
return;
|
1195
1464
|
}
|
1196
|
-
void this.
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
currentResourceCount
|
1208
|
-
totalResourceCount
|
1209
|
-
|
1210
|
-
|
1465
|
+
void this.refreshWorker(options);
|
1466
|
+
}
|
1467
|
+
async refreshWorker(options) {
|
1468
|
+
const instanceId = LocalRunnerBackend.getInstanceId(options);
|
1469
|
+
try {
|
1470
|
+
await this.pulumiProjectHost.runEmpty(
|
1471
|
+
options.projectId,
|
1472
|
+
options.instanceType,
|
1473
|
+
options.instanceName,
|
1474
|
+
async (stack) => {
|
1475
|
+
const summary = await stack.workspace.stack();
|
1476
|
+
let currentResourceCount = 0;
|
1477
|
+
let totalResourceCount = summary?.resourceCount ?? 0;
|
1478
|
+
this.updateState({
|
1479
|
+
id: instanceId,
|
1480
|
+
status: "refreshing",
|
1481
|
+
currentResourceCount,
|
1482
|
+
totalResourceCount
|
1483
|
+
});
|
1211
1484
|
await runWithRetryOnError(
|
1212
1485
|
async () => {
|
1213
1486
|
await stack.refresh({
|
1214
1487
|
color: "always",
|
1215
1488
|
onEvent: (event) => {
|
1489
|
+
if (event.resourcePreEvent) {
|
1490
|
+
totalResourceCount = updateResourceCount(
|
1491
|
+
event.resourcePreEvent.metadata.op,
|
1492
|
+
totalResourceCount
|
1493
|
+
);
|
1494
|
+
this.updateState({ id: instanceId, totalResourceCount });
|
1495
|
+
return;
|
1496
|
+
}
|
1216
1497
|
if (event.resOutputsEvent) {
|
1217
1498
|
currentResourceCount = updateResourceCount(
|
1218
1499
|
event.resOutputsEvent.metadata.op,
|
@@ -1223,7 +1504,7 @@ class LocalRunnerBackend {
|
|
1223
1504
|
}
|
1224
1505
|
},
|
1225
1506
|
onOutput: (message) => {
|
1226
|
-
this.updateState({ id: instanceId, message });
|
1507
|
+
this.updateState({ id: instanceId, logLine: message });
|
1227
1508
|
if (this.printOutput) {
|
1228
1509
|
console.log(message);
|
1229
1510
|
}
|
@@ -1234,52 +1515,62 @@ class LocalRunnerBackend {
|
|
1234
1515
|
this.updateState({
|
1235
1516
|
id: instanceId,
|
1236
1517
|
status: "created",
|
1518
|
+
totalResourceCount: currentResourceCount,
|
1237
1519
|
...extraOutputs
|
1238
1520
|
});
|
1239
1521
|
},
|
1240
|
-
(error) =>
|
1522
|
+
(error) => this.pulumiProjectHost.tryUnlockStack(stack, error)
|
1241
1523
|
);
|
1242
|
-
} catch (e) {
|
1243
|
-
this.updateState({
|
1244
|
-
id: instanceId,
|
1245
|
-
status: "error",
|
1246
|
-
error: e instanceof Error ? e.message : String(e)
|
1247
|
-
});
|
1248
1524
|
}
|
1249
|
-
|
1250
|
-
)
|
1525
|
+
);
|
1526
|
+
} catch (error) {
|
1527
|
+
this.updateState({
|
1528
|
+
id: instanceId,
|
1529
|
+
status: "error",
|
1530
|
+
error: errorToString(error)
|
1531
|
+
});
|
1532
|
+
}
|
1251
1533
|
}
|
1252
1534
|
async getExtraOutputsStatePatch(stack) {
|
1253
1535
|
const outputs = await stack.outputs();
|
1254
1536
|
const patch = {};
|
1255
1537
|
if (outputs["$status"]) {
|
1256
|
-
patch.statusFields =
|
1538
|
+
patch.statusFields = z.array(instanceStatusFieldSchema).parse(outputs["$status"].value);
|
1257
1539
|
} else {
|
1258
|
-
patch.statusFields =
|
1540
|
+
patch.statusFields = [];
|
1259
1541
|
}
|
1260
|
-
if (outputs["$
|
1261
|
-
const
|
1262
|
-
|
1263
|
-
|
1264
|
-
patch.
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1542
|
+
if (outputs["$pages"]) {
|
1543
|
+
const pages = z.array(instancePageSchema).parse(outputs["$pages"].value);
|
1544
|
+
patch.pages = pages.map((page) => omit(page, ["content"]));
|
1545
|
+
} else {
|
1546
|
+
patch.pages = [];
|
1547
|
+
}
|
1548
|
+
if (outputs["$files"]) {
|
1549
|
+
const files = z.array(instanceFileSchema).parse(outputs["$files"].value);
|
1550
|
+
patch.files = files.map((file) => omit(file, ["content"]));
|
1269
1551
|
} else {
|
1270
|
-
patch.
|
1552
|
+
patch.files = [];
|
1271
1553
|
}
|
1272
|
-
if (outputs["$
|
1273
|
-
|
1274
|
-
patch.
|
1554
|
+
if (outputs["$terminals"]) {
|
1555
|
+
const terminals = z.array(instanceTerminalSchema).parse(outputs["$terminals"].value);
|
1556
|
+
patch.terminals = terminals.map((terminal) => pick(terminal, ["name", "title", "description"]));
|
1275
1557
|
} else {
|
1276
|
-
patch.
|
1558
|
+
patch.terminals = [];
|
1559
|
+
}
|
1560
|
+
if (outputs["$triggers"]) {
|
1561
|
+
patch.triggers = z.array(instanceTriggerSchema).parse(outputs["$triggers"].value);
|
1562
|
+
} else {
|
1563
|
+
patch.triggers = [];
|
1277
1564
|
}
|
1278
1565
|
if (outputs["$secrets"]) {
|
1279
|
-
patch.secrets =
|
1566
|
+
patch.secrets = pickBy(
|
1567
|
+
z.record(z.string().nullish()).parse(outputs["$secrets"].value),
|
1568
|
+
(v) => !!v
|
1569
|
+
);
|
1280
1570
|
} else {
|
1281
1571
|
patch.secrets = null;
|
1282
1572
|
}
|
1573
|
+
patch.outputHash = await sha256(JSON.stringify(outputs));
|
1283
1574
|
return patch;
|
1284
1575
|
}
|
1285
1576
|
updateState(patch) {
|
@@ -1306,26 +1597,36 @@ class LocalRunnerBackend {
|
|
1306
1597
|
if (!source.path) {
|
1307
1598
|
throw new Error("Source path is required for local units");
|
1308
1599
|
}
|
1309
|
-
return resolve(this.sourceBasePath, source.
|
1600
|
+
return resolve$1(this.sourceBasePath, source.path);
|
1310
1601
|
}
|
1311
1602
|
if (!this.skipSourceCheck) {
|
1312
1603
|
const packageName = source.version ? `${source.package}@${source.version}` : source.package;
|
1313
1604
|
await ensureDependencyInstalled(packageName);
|
1314
1605
|
}
|
1315
|
-
|
1316
|
-
throw new Error("Source path is required for npm units");
|
1317
|
-
}
|
1318
|
-
const fullPath = `${source.package}/${source.path}`;
|
1606
|
+
const fullPath = source.path ? `${source.package}/${source.path}` : source.package;
|
1319
1607
|
const url = resolve(fullPath, import.meta.url);
|
1320
1608
|
const path = fileURLToPath(url);
|
1321
1609
|
const projectPath = dirname(path);
|
1322
1610
|
return projectPath;
|
1323
1611
|
}
|
1612
|
+
async tryInstallMissingDependencies(error) {
|
1613
|
+
if (!(error instanceof Error)) {
|
1614
|
+
return false;
|
1615
|
+
}
|
1616
|
+
const pattern = /Cannot find module '(.*)'/;
|
1617
|
+
const match = error.message.match(pattern);
|
1618
|
+
if (!match) {
|
1619
|
+
return false;
|
1620
|
+
}
|
1621
|
+
const packageName = match[1];
|
1622
|
+
await ensureDependencyInstalled(packageName);
|
1623
|
+
return true;
|
1624
|
+
}
|
1324
1625
|
static async create(config, pulumiProjectHost) {
|
1325
1626
|
let sourceBasePath = config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SOURCE_BASE_PATH;
|
1326
1627
|
if (!sourceBasePath) {
|
1327
1628
|
const [projectPath] = await resolveMainLocalProject();
|
1328
|
-
sourceBasePath = resolve(projectPath, "units");
|
1629
|
+
sourceBasePath = resolve$1(projectPath, "units");
|
1329
1630
|
}
|
1330
1631
|
return new LocalRunnerBackend(
|
1331
1632
|
config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_SOURCE_CHECK,
|
@@ -1363,75 +1664,53 @@ class LocalStateBackend {
|
|
1363
1664
|
this.logger.debug({ msg: "initialized", dbLocation: db.location });
|
1364
1665
|
}
|
1365
1666
|
async getActiveOperations() {
|
1366
|
-
const sublevel = this.
|
1367
|
-
|
1368
|
-
for await (const operation of sublevel.values()) {
|
1369
|
-
result.push(projectOperationSchema.parse(operation));
|
1370
|
-
}
|
1371
|
-
return result;
|
1667
|
+
const sublevel = this.getActiveOperationsSublevel();
|
1668
|
+
return this.getSublevelItems(sublevel, projectOperationSchema);
|
1372
1669
|
}
|
1373
1670
|
async getOperations(projectId, beforeOperationId) {
|
1374
|
-
const sublevel = this.
|
1375
|
-
const result = [];
|
1671
|
+
const sublevel = this.getProjectOperationsSublevel(projectId);
|
1376
1672
|
const pageSize = 10;
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
1381
|
-
}
|
1382
|
-
}
|
1383
|
-
return result;
|
1673
|
+
return await this.getSublevelItems(sublevel, projectOperationSchema, pageSize, {
|
1674
|
+
lt: beforeOperationId,
|
1675
|
+
reverse: true
|
1676
|
+
});
|
1384
1677
|
}
|
1385
1678
|
async getInstanceStates(projectId) {
|
1386
|
-
const sublevel = this.
|
1387
|
-
|
1388
|
-
for await (const state of sublevel.values()) {
|
1389
|
-
result.push(instanceStateSchema.parse(state));
|
1390
|
-
}
|
1391
|
-
return result;
|
1679
|
+
const sublevel = this.getProjectInstanceStatesSublevel(projectId);
|
1680
|
+
return await this.getSublevelItems(sublevel, instanceStateSchema);
|
1392
1681
|
}
|
1393
1682
|
async getInstanceState(projectId, instanceID) {
|
1394
|
-
const sublevel = this.
|
1395
|
-
|
1396
|
-
if (!state) {
|
1397
|
-
return undefined;
|
1398
|
-
}
|
1399
|
-
return instanceStateSchema.parse(state);
|
1683
|
+
const sublevel = this.getProjectInstanceStatesSublevel(projectId);
|
1684
|
+
return await this.getSublevelItem(sublevel, instanceStateSchema, instanceID);
|
1400
1685
|
}
|
1401
1686
|
async getAffectedInstanceStates(operationId) {
|
1402
|
-
const sublevel = this.
|
1403
|
-
|
1404
|
-
for await (const state of sublevel.values()) {
|
1405
|
-
result.push(instanceStateSchema.parse(state));
|
1406
|
-
}
|
1407
|
-
return result;
|
1687
|
+
const sublevel = this.getOperationInstanceStatesSublevel(operationId);
|
1688
|
+
return await this.getSublevelItems(sublevel, instanceStateSchema);
|
1408
1689
|
}
|
1409
1690
|
async getInstanceLogs(operationId, instanceId) {
|
1410
|
-
const sublevel = this.
|
1411
|
-
|
1412
|
-
for await (const line of sublevel.values()) {
|
1413
|
-
result.push(line);
|
1414
|
-
}
|
1415
|
-
return result;
|
1691
|
+
const sublevel = this.getOperationInstanceLogsSublevel(operationId, instanceId);
|
1692
|
+
return await Array.fromAsync(sublevel.values());
|
1416
1693
|
}
|
1417
1694
|
async putOperation(operation) {
|
1418
|
-
|
1419
|
-
const
|
1420
|
-
|
1695
|
+
this.validateItem(projectOperationSchema, operation);
|
1696
|
+
const operationsSublevel = this.getProjectOperationsSublevel(operation.projectId);
|
1697
|
+
const activeOperationsSublevel = this.getActiveOperationsSublevel();
|
1698
|
+
if (!isFinalOperationStatus(operation.status)) {
|
1421
1699
|
await this.db.batch([
|
1422
|
-
{ type: "put", key: operation.id, value: operation, sublevel },
|
1700
|
+
{ type: "put", key: operation.id, value: operation, sublevel: operationsSublevel },
|
1423
1701
|
{ type: "put", key: operation.id, value: operation, sublevel: activeOperationsSublevel }
|
1424
1702
|
]);
|
1425
1703
|
} else {
|
1426
1704
|
await this.db.batch([
|
1427
|
-
{ type: "put", key: operation.id, value: operation, sublevel },
|
1705
|
+
{ type: "put", key: operation.id, value: operation, sublevel: operationsSublevel },
|
1428
1706
|
{ type: "del", key: operation.id, sublevel: activeOperationsSublevel }
|
1429
1707
|
]);
|
1430
1708
|
}
|
1431
1709
|
}
|
1432
1710
|
async putAffectedInstanceStates(projectId, operationId, states) {
|
1433
|
-
|
1434
|
-
const
|
1711
|
+
this.validateArray(instanceStateSchema, states);
|
1712
|
+
const operationInstanceStatesSublevel = this.getOperationInstanceStatesSublevel(operationId);
|
1713
|
+
const projectInstanceStatesSublevel = this.getProjectInstanceStatesSublevel(projectId);
|
1435
1714
|
await this.db.batch(
|
1436
1715
|
// put the states to both the operation and project sublevels
|
1437
1716
|
// denormalization is cool
|
@@ -1441,19 +1720,20 @@ class LocalStateBackend {
|
|
1441
1720
|
type: "put",
|
1442
1721
|
key: state.id,
|
1443
1722
|
value: state,
|
1444
|
-
sublevel
|
1723
|
+
sublevel: operationInstanceStatesSublevel
|
1445
1724
|
},
|
1446
1725
|
{
|
1447
1726
|
type: "put",
|
1448
1727
|
key: state.id,
|
1449
1728
|
value: state,
|
1450
|
-
sublevel:
|
1729
|
+
sublevel: projectInstanceStatesSublevel
|
1451
1730
|
}
|
1452
1731
|
])
|
1453
1732
|
);
|
1454
1733
|
}
|
1455
1734
|
async putInstanceStates(projectId, states) {
|
1456
|
-
|
1735
|
+
this.validateArray(instanceStateSchema, states);
|
1736
|
+
const sublevel = this.getProjectInstanceStatesSublevel(projectId);
|
1457
1737
|
await sublevel.batch(
|
1458
1738
|
// as i told before, we update the instance states without operations
|
1459
1739
|
// this method is used when upstream instance state changes are detected
|
@@ -1470,7 +1750,7 @@ class LocalStateBackend {
|
|
1470
1750
|
if (sublevels.has(instanceId)) {
|
1471
1751
|
continue;
|
1472
1752
|
}
|
1473
|
-
const sublevel = this.
|
1753
|
+
const sublevel = this.getOperationInstanceLogsSublevel(operationId, instanceId);
|
1474
1754
|
sublevels.set(instanceId, sublevel);
|
1475
1755
|
}
|
1476
1756
|
await this.db.batch(
|
@@ -1483,22 +1763,18 @@ class LocalStateBackend {
|
|
1483
1763
|
);
|
1484
1764
|
}
|
1485
1765
|
async getCompositeInstance(projectId, instanceId) {
|
1486
|
-
const sublevel = this.
|
1487
|
-
|
1488
|
-
if (!instance) {
|
1489
|
-
return null;
|
1490
|
-
}
|
1491
|
-
return evaluatedCompositeInstanceSchema.parse(instance);
|
1766
|
+
const sublevel = this.getProjectCompositeInstancesSublevel(projectId);
|
1767
|
+
return this.getSublevelItem(sublevel, evaluatedCompositeInstanceSchema, instanceId);
|
1492
1768
|
}
|
1493
1769
|
async getCompositeInstanceInputHash(projectId, instanceId) {
|
1494
|
-
const sublevel = this.
|
1495
|
-
|
1770
|
+
const sublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId);
|
1771
|
+
const inputHash = await sublevel.get(instanceId);
|
1772
|
+
return inputHash ?? null;
|
1496
1773
|
}
|
1497
1774
|
async putCompositeInstances(projectId, instances) {
|
1498
|
-
|
1499
|
-
const
|
1500
|
-
|
1501
|
-
);
|
1775
|
+
this.validateArray(evaluatedCompositeInstanceSchema, instances);
|
1776
|
+
const sublevel = this.getProjectCompositeInstancesSublevel(projectId);
|
1777
|
+
const inputHashesSublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId);
|
1502
1778
|
await this.db.batch(
|
1503
1779
|
instances.flatMap((instance) => [
|
1504
1780
|
{
|
@@ -1517,10 +1793,8 @@ class LocalStateBackend {
|
|
1517
1793
|
);
|
1518
1794
|
}
|
1519
1795
|
async clearCompositeInstances(projectId, instanceIds) {
|
1520
|
-
const sublevel = this.
|
1521
|
-
const inputHashesSublevel = this.
|
1522
|
-
`projects/${projectId}/compositeInstanceInputHashes`
|
1523
|
-
);
|
1796
|
+
const sublevel = this.getProjectCompositeInstancesSublevel(projectId);
|
1797
|
+
const inputHashesSublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId);
|
1524
1798
|
await this.db.batch(
|
1525
1799
|
instanceIds.flatMap((instanceId) => [
|
1526
1800
|
{ type: "del", key: instanceId, sublevel },
|
@@ -1528,6 +1802,138 @@ class LocalStateBackend {
|
|
1528
1802
|
])
|
1529
1803
|
);
|
1530
1804
|
}
|
1805
|
+
async getTerminalSessions(projectId, instanceId) {
|
1806
|
+
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId);
|
1807
|
+
return await this.getSublevelItems(sublevel, terminalSessionSchema);
|
1808
|
+
}
|
1809
|
+
async getTerminalSession(projectId, instanceId, sessionId) {
|
1810
|
+
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId);
|
1811
|
+
return await this.getSublevelItem(sublevel, terminalSessionSchema, sessionId);
|
1812
|
+
}
|
1813
|
+
async putTerminalSession(projectId, instanceId, session) {
|
1814
|
+
this.validateItem(terminalSessionSchema, session);
|
1815
|
+
const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId);
|
1816
|
+
await sublevel.put(session.id, session);
|
1817
|
+
}
|
1818
|
+
async getTerminalSessionHistory(sessionId) {
|
1819
|
+
const sublevel = this.getTerminalSessionHistorySublevel(sessionId);
|
1820
|
+
return await Array.fromAsync(sublevel.values());
|
1821
|
+
}
|
1822
|
+
async appendTerminalSessionHistory(lines) {
|
1823
|
+
const sublevels = /* @__PURE__ */ new Map();
|
1824
|
+
for (const [sessionId] of lines) {
|
1825
|
+
if (sublevels.has(sessionId)) {
|
1826
|
+
continue;
|
1827
|
+
}
|
1828
|
+
const sublevel = this.getTerminalSessionHistorySublevel(sessionId);
|
1829
|
+
sublevels.set(sessionId, sublevel);
|
1830
|
+
}
|
1831
|
+
await this.db.batch(
|
1832
|
+
lines.map(([sessionId, line]) => ({
|
1833
|
+
type: "put",
|
1834
|
+
key: uuidv7(),
|
1835
|
+
value: line,
|
1836
|
+
sublevel: sublevels.get(sessionId)
|
1837
|
+
}))
|
1838
|
+
);
|
1839
|
+
}
|
1840
|
+
async getSublevelItems(sublevel, schema, limit, options) {
|
1841
|
+
const result = [];
|
1842
|
+
const iterator = options ? sublevel.iterator(options) : sublevel.iterator();
|
1843
|
+
const invalidKeys = [];
|
1844
|
+
for await (const [key, value] of iterator) {
|
1845
|
+
const parseResult = schema.safeParse(value);
|
1846
|
+
if (!parseResult.success) {
|
1847
|
+
this.logger.warn({
|
1848
|
+
msg: "failed to parse item, it will be deleted",
|
1849
|
+
error: parseResult.error,
|
1850
|
+
sublevel: sublevel.prefix,
|
1851
|
+
key
|
1852
|
+
});
|
1853
|
+
invalidKeys.push(key);
|
1854
|
+
continue;
|
1855
|
+
}
|
1856
|
+
result.push(parseResult.data);
|
1857
|
+
if (limit && result.length >= limit) {
|
1858
|
+
break;
|
1859
|
+
}
|
1860
|
+
}
|
1861
|
+
if (invalidKeys.length > 0) {
|
1862
|
+
this.logger.info({
|
1863
|
+
msg: "deleting invalid items",
|
1864
|
+
sublevel: sublevel.prefix,
|
1865
|
+
keyCount: invalidKeys.length
|
1866
|
+
});
|
1867
|
+
await sublevel.batch(invalidKeys.map((key) => ({ type: "del", key })));
|
1868
|
+
}
|
1869
|
+
return result;
|
1870
|
+
}
|
1871
|
+
async getSublevelItem(sublevel, schema, key) {
|
1872
|
+
const value = await sublevel.get(key);
|
1873
|
+
if (!value) {
|
1874
|
+
return null;
|
1875
|
+
}
|
1876
|
+
const parseResult = schema.safeParse(value);
|
1877
|
+
if (!parseResult.success) {
|
1878
|
+
this.logger.warn({
|
1879
|
+
msg: "failed to parse item, it will be deleted",
|
1880
|
+
error: parseResult.error,
|
1881
|
+
sublevel: sublevel.prefix,
|
1882
|
+
key
|
1883
|
+
});
|
1884
|
+
await sublevel.del(key);
|
1885
|
+
return null;
|
1886
|
+
}
|
1887
|
+
return parseResult.data;
|
1888
|
+
}
|
1889
|
+
validateItem(schema, item) {
|
1890
|
+
const parseResult = schema.safeParse(item);
|
1891
|
+
if (!parseResult.success) {
|
1892
|
+
this.logger.error({
|
1893
|
+
msg: "failed to validate item",
|
1894
|
+
error: parseResult.error,
|
1895
|
+
item
|
1896
|
+
});
|
1897
|
+
throw new Error(`Failed to validate item: ${parseResult.error.errors[0].message}`, {
|
1898
|
+
cause: parseResult.error
|
1899
|
+
});
|
1900
|
+
}
|
1901
|
+
}
|
1902
|
+
validateArray(schema, items) {
|
1903
|
+
for (const item of items) {
|
1904
|
+
this.validateItem(schema, item);
|
1905
|
+
}
|
1906
|
+
}
|
1907
|
+
getActiveOperationsSublevel() {
|
1908
|
+
return this.getJsonSublevel("activeOperations");
|
1909
|
+
}
|
1910
|
+
getProjectOperationsSublevel(projectId) {
|
1911
|
+
return this.getJsonSublevel(`projects/${projectId}/operations`);
|
1912
|
+
}
|
1913
|
+
getProjectCompositeInstancesSublevel(projectId) {
|
1914
|
+
return this.getJsonSublevel(`projects/${projectId}/compositeInstances`);
|
1915
|
+
}
|
1916
|
+
getProjectCompositeInstanceInputHashesSublevel(projectId) {
|
1917
|
+
return this.getStringSublevel(`projects/${projectId}/compositeInstanceInputHashes`);
|
1918
|
+
}
|
1919
|
+
getProjectInstanceStatesSublevel(projectId) {
|
1920
|
+
return this.getJsonSublevel(`projects/${projectId}/instanceStates`);
|
1921
|
+
}
|
1922
|
+
getOperationInstanceStatesSublevel(operationId) {
|
1923
|
+
return this.getJsonSublevel(`operations/${operationId}/instanceStates`);
|
1924
|
+
}
|
1925
|
+
getOperationInstanceLogsSublevel(operationId, instanceId) {
|
1926
|
+
return this.getStringSublevel(`operations/${operationId}/instances/${instanceId}/logs`);
|
1927
|
+
}
|
1928
|
+
getTerminalSessionsSublevel(projectId, instanceId) {
|
1929
|
+
return this.getJsonSublevel(`projects/${projectId}/instances/${instanceId}/terminalSessions`);
|
1930
|
+
}
|
1931
|
+
getTerminalSessionHistorySublevel(sessionId) {
|
1932
|
+
return this.getStringSublevel(`terminalSessions/${sessionId}/history`);
|
1933
|
+
}
|
1934
|
+
getStringSublevel(path) {
|
1935
|
+
return this.db.sublevel(path, { valueEncoding: "utf8" });
|
1936
|
+
}
|
1531
1937
|
getJsonSublevel(path) {
|
1532
1938
|
return this.db.sublevel(path, { valueEncoding: "json" });
|
1533
1939
|
}
|
@@ -1655,19 +2061,22 @@ class RuntimeOperation {
|
|
1655
2061
|
}
|
1656
2062
|
abortController = new AbortController();
|
1657
2063
|
instanceMap = /* @__PURE__ */ new Map();
|
2064
|
+
hubMap = /* @__PURE__ */ new Map();
|
2065
|
+
resolvedInstanceInputs = /* @__PURE__ */ new Map();
|
1658
2066
|
compositeInstanceLock = new BetterLock();
|
1659
2067
|
compositeInstanceMap = /* @__PURE__ */ new Map();
|
1660
2068
|
initialStatusMap = /* @__PURE__ */ new Map();
|
1661
2069
|
stateMap = /* @__PURE__ */ new Map();
|
1662
2070
|
instancePromiseMap = /* @__PURE__ */ new Map();
|
1663
2071
|
childrenStateMap = /* @__PURE__ */ new Map();
|
2072
|
+
dependentStateMap = /* @__PURE__ */ new Map();
|
1664
2073
|
library;
|
1665
|
-
|
2074
|
+
resolveInputHash;
|
1666
2075
|
async operateSafe() {
|
1667
2076
|
try {
|
1668
2077
|
await this.operate();
|
1669
2078
|
} catch (error) {
|
1670
|
-
if (isAbortError(error)) {
|
2079
|
+
if (RuntimeOperation.isAbortError(error)) {
|
1671
2080
|
this.logger.info("the operation was cancelled");
|
1672
2081
|
this.operation.status = "cancelled";
|
1673
2082
|
await this.updateOperation();
|
@@ -1692,27 +2101,75 @@ class RuntimeOperation {
|
|
1692
2101
|
if (state.status !== "pending") {
|
1693
2102
|
continue;
|
1694
2103
|
}
|
1695
|
-
|
2104
|
+
let initialStatus = this.initialStatusMap.get(state.id);
|
2105
|
+
let error = state.error;
|
2106
|
+
if (initialStatus === "destroying" || initialStatus === "pending" || initialStatus === "refreshing" || initialStatus === "updating") {
|
2107
|
+
initialStatus = "error";
|
2108
|
+
error ??= "unexpected progressing instance status";
|
2109
|
+
}
|
1696
2110
|
if (initialStatus) {
|
1697
|
-
this.updateInstanceState({ id: state.id, status: initialStatus });
|
2111
|
+
this.updateInstanceState({ id: state.id, status: initialStatus, error });
|
1698
2112
|
}
|
1699
2113
|
}
|
1700
2114
|
}
|
1701
2115
|
async operate() {
|
1702
2116
|
this.logger.info("starting operation");
|
1703
|
-
const
|
2117
|
+
const { instances: projectInstances, hubs } = await this.projectBackend.getProject(
|
2118
|
+
this.operation.projectId
|
2119
|
+
);
|
1704
2120
|
this.abortController.signal.throwIfAborted();
|
1705
2121
|
const states = await this.stateBackend.getInstanceStates(this.operation.projectId);
|
1706
2122
|
this.abortController.signal.throwIfAborted();
|
2123
|
+
this.library = await this.libraryBackend.loadLibrary();
|
2124
|
+
this.logger.info("library ready");
|
2125
|
+
this.abortController.signal.throwIfAborted();
|
2126
|
+
const allInstances = projectInstances.filter((instance) => {
|
2127
|
+
return instance.type in this.library.components;
|
2128
|
+
});
|
2129
|
+
const inputResolverNodes = /* @__PURE__ */ new Map();
|
2130
|
+
for (const hub of hubs) {
|
2131
|
+
this.hubMap.set(hub.id, hub);
|
2132
|
+
inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub });
|
2133
|
+
}
|
1707
2134
|
for (const instance of allInstances) {
|
1708
2135
|
this.instanceMap.set(instance.id, instance);
|
2136
|
+
inputResolverNodes.set(`instance:${instance.id}`, {
|
2137
|
+
kind: "instance",
|
2138
|
+
instance,
|
2139
|
+
component: this.library.components[instance.type]
|
2140
|
+
});
|
2141
|
+
}
|
2142
|
+
const resolveInputs = createInputResolver(
|
2143
|
+
inputResolverNodes,
|
2144
|
+
this.logger.child({ resolver: "input-resolver" })
|
2145
|
+
);
|
2146
|
+
for (const instance of allInstances) {
|
2147
|
+
const output = await resolveInputs(`instance:${instance.id}`);
|
2148
|
+
if (output.kind !== "instance") {
|
2149
|
+
throw new Error("Unexpected output kind, expected instance");
|
2150
|
+
}
|
2151
|
+
this.resolvedInstanceInputs.set(instance.id, output.resolvedInputs);
|
1709
2152
|
}
|
1710
|
-
this.inputHashCalculator = new InputHashCalculator(this.instanceMap);
|
1711
2153
|
for (const state of states) {
|
1712
2154
|
this.stateMap.set(state.id, state);
|
1713
2155
|
this.initialStatusMap.set(state.id, state.status);
|
1714
2156
|
this.tryAddStateToParent(state);
|
2157
|
+
this.addDependentState(state);
|
2158
|
+
}
|
2159
|
+
const inputHashNodes = /* @__PURE__ */ new Map();
|
2160
|
+
for (const instance of allInstances) {
|
2161
|
+
inputHashNodes.set(instance.id, {
|
2162
|
+
instance,
|
2163
|
+
resolvedInputs: this.resolvedInstanceInputs.get(instance.id),
|
2164
|
+
state: this.stateMap.get(instance.id),
|
2165
|
+
sourceHash: void 0
|
2166
|
+
// TODO: implement source hash
|
2167
|
+
});
|
1715
2168
|
}
|
2169
|
+
this.resolveInputHash = createInputHashResolver(
|
2170
|
+
inputHashNodes,
|
2171
|
+
this.logger.child({ resolver: "input-hash-resolver" })
|
2172
|
+
);
|
1716
2173
|
if (this.operation.type === "update") {
|
1717
2174
|
await this.extendWithNotCreatedDependencies();
|
1718
2175
|
await this.updateOperation();
|
@@ -1726,26 +2183,25 @@ class RuntimeOperation {
|
|
1726
2183
|
});
|
1727
2184
|
if (this.operation.type === "evaluate") {
|
1728
2185
|
this.logger.info("evaluating instances");
|
1729
|
-
await this.evaluateCompositeInstances(
|
2186
|
+
await this.evaluateCompositeInstances();
|
1730
2187
|
} else {
|
1731
|
-
this.library = await this.libraryBackend.loadLibrary();
|
1732
|
-
this.logger.info("library ready");
|
1733
|
-
this.abortController.signal.throwIfAborted();
|
1734
2188
|
const promises = [];
|
1735
2189
|
for (const instanceId of this.operation.instanceIds) {
|
1736
2190
|
promises.push(this.getInstancePromiseForOperation(instanceId));
|
1737
2191
|
}
|
1738
|
-
this.logger.info("all units started");
|
2192
|
+
this.logger.info("all units operations started");
|
1739
2193
|
this.operation.status = "running";
|
1740
2194
|
await this.updateOperation();
|
1741
2195
|
await Promise.all(promises);
|
1742
2196
|
this.logger.info("all units completed");
|
1743
2197
|
}
|
1744
2198
|
this.operation.status = "completed";
|
2199
|
+
this.operation.error = null;
|
1745
2200
|
await this.updateOperation();
|
1746
2201
|
this.logger.info("operation completed");
|
1747
2202
|
}
|
1748
2203
|
cancel() {
|
2204
|
+
this.logger.info("cancelling operation");
|
1749
2205
|
this.abortController.abort();
|
1750
2206
|
}
|
1751
2207
|
getInstancePromiseForOperation(instanceId) {
|
@@ -1771,7 +2227,7 @@ class RuntimeOperation {
|
|
1771
2227
|
return this.recreateUnit(instance, component);
|
1772
2228
|
}
|
1773
2229
|
case "destroy": {
|
1774
|
-
return this.destroyUnit(instance.id);
|
2230
|
+
return this.destroyUnit(instance.id, component);
|
1775
2231
|
}
|
1776
2232
|
case "refresh": {
|
1777
2233
|
return this.refreshUnit(instance.id);
|
@@ -1809,7 +2265,7 @@ class RuntimeOperation {
|
|
1809
2265
|
status: "created"
|
1810
2266
|
});
|
1811
2267
|
} catch (error) {
|
1812
|
-
if (isAbortError(error)) {
|
2268
|
+
if (RuntimeOperation.isAbortError(error)) {
|
1813
2269
|
this.updateInstanceState({
|
1814
2270
|
id: instance.id,
|
1815
2271
|
status: initialStatus
|
@@ -1841,7 +2297,7 @@ class RuntimeOperation {
|
|
1841
2297
|
continue;
|
1842
2298
|
}
|
1843
2299
|
logger.info("waiting for dependency", { dependencyId: dependency.id });
|
1844
|
-
dependencyPromises.push(this.
|
2300
|
+
dependencyPromises.push(this.getInstancePromiseForOperation(dependency.id));
|
1845
2301
|
}
|
1846
2302
|
await Promise.all(dependencyPromises);
|
1847
2303
|
this.abortController.signal.throwIfAborted();
|
@@ -1869,13 +2325,46 @@ class RuntimeOperation {
|
|
1869
2325
|
finalStatuses: ["created", "error"]
|
1870
2326
|
});
|
1871
2327
|
await this.watchStateStream(stream);
|
1872
|
-
const inputHash = await this.
|
2328
|
+
const inputHash = await this.resolveInputHash(instance.id);
|
1873
2329
|
this.updateInstanceState({ id: instance.id, inputHash });
|
1874
2330
|
logger.debug("input hash after update", { inputHash });
|
1875
2331
|
logger.info("unit updated");
|
1876
2332
|
});
|
1877
2333
|
}
|
1878
|
-
async
|
2334
|
+
async processBeforeDestroyTriggers(state, component, logger) {
|
2335
|
+
const instance = this.instanceMap.get(state.id);
|
2336
|
+
if (!instance) {
|
2337
|
+
throw new Error(`Instance not found: ${state.id}`);
|
2338
|
+
}
|
2339
|
+
const triggers = state.triggers.filter((trigger) => trigger.spec.type === "before-destroy");
|
2340
|
+
if (triggers.length === 0) {
|
2341
|
+
return;
|
2342
|
+
}
|
2343
|
+
const invokedTriggers = triggers.map((trigger) => ({ name: trigger.name }));
|
2344
|
+
logger.info("updating unit to process before-destroy triggers...");
|
2345
|
+
const secrets = await this.secretBackend.get(this.operation.projectId, instance.id);
|
2346
|
+
this.abortController.signal.throwIfAborted();
|
2347
|
+
logger.debug("secrets loaded", { count: Object.keys(secrets).length });
|
2348
|
+
await this.runnerBackend.update({
|
2349
|
+
projectId: this.operation.projectId,
|
2350
|
+
instanceType: instance.type,
|
2351
|
+
instanceName: instance.name,
|
2352
|
+
config: await this.prepareUnitConfig(instance, invokedTriggers),
|
2353
|
+
secrets: mapValues(secrets, (value) => valueToString(value)),
|
2354
|
+
source: this.resolveUnitSource(component),
|
2355
|
+
signal: this.abortController.signal
|
2356
|
+
});
|
2357
|
+
logger.debug("unit update requested");
|
2358
|
+
const stream = this.runnerBackend.watch({
|
2359
|
+
projectId: this.operation.projectId,
|
2360
|
+
instanceType: instance.type,
|
2361
|
+
instanceName: instance.name,
|
2362
|
+
finalStatuses: ["created", "error"]
|
2363
|
+
});
|
2364
|
+
await this.watchStateStream(stream);
|
2365
|
+
logger.debug("before-destroy triggers processed");
|
2366
|
+
}
|
2367
|
+
async destroyUnit(instanceId, component) {
|
1879
2368
|
const logger = this.logger.child({ instanceId });
|
1880
2369
|
return this.getInstancePromise(instanceId, async () => {
|
1881
2370
|
this.updateInstanceState({
|
@@ -1891,12 +2380,13 @@ class RuntimeOperation {
|
|
1891
2380
|
return;
|
1892
2381
|
}
|
1893
2382
|
const dependentPromises = [];
|
1894
|
-
|
1895
|
-
|
1896
|
-
dependentPromises.push(this.
|
2383
|
+
const dependents = this.dependentStateMap.get(instanceId) ?? [];
|
2384
|
+
for (const dependent of dependents) {
|
2385
|
+
dependentPromises.push(this.getInstancePromiseForOperation(dependent.id));
|
1897
2386
|
}
|
1898
2387
|
await Promise.all(dependentPromises);
|
1899
2388
|
this.abortController.signal.throwIfAborted();
|
2389
|
+
await this.processBeforeDestroyTriggers(state, component, logger);
|
1900
2390
|
logger.info("destroying unit...");
|
1901
2391
|
const [type, name] = parseInstanceId(instanceId);
|
1902
2392
|
await this.runnerBackend.destroy({
|
@@ -1926,6 +2416,12 @@ class RuntimeOperation {
|
|
1926
2416
|
currentResourceCount: 0,
|
1927
2417
|
totalResourceCount: 0
|
1928
2418
|
});
|
2419
|
+
const state = this.stateMap.get(instance.id);
|
2420
|
+
if (!state) {
|
2421
|
+
logger.warn("state not found for unit, but recreate was requested");
|
2422
|
+
return;
|
2423
|
+
}
|
2424
|
+
await this.processBeforeDestroyTriggers(state, component, logger);
|
1929
2425
|
logger.info("destroying unit...");
|
1930
2426
|
await this.runnerBackend.destroy({
|
1931
2427
|
projectId: this.operation.projectId,
|
@@ -2012,26 +2508,28 @@ class RuntimeOperation {
|
|
2012
2508
|
throw new Error(`The operation on unit "${statePatch.id}" failed: ${statePatch.error}`);
|
2013
2509
|
}
|
2014
2510
|
}
|
2015
|
-
async prepareUnitConfig(instance) {
|
2511
|
+
async prepareUnitConfig(instance, invokedTriggers = []) {
|
2016
2512
|
const config = {};
|
2017
|
-
for (const [key, value] of Object.entries(instance.args)) {
|
2513
|
+
for (const [key, value] of Object.entries(instance.args ?? {})) {
|
2018
2514
|
config[key] = valueToString(value);
|
2019
2515
|
}
|
2020
|
-
|
2021
|
-
|
2516
|
+
const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
|
2517
|
+
for (const [key, value] of Object.entries(instanceInputs)) {
|
2518
|
+
const resolvedInput = await this.resolveInstanceInput(value.map((input) => input.input));
|
2519
|
+
config[`input.${key}`] = JSON.stringify(resolvedInput);
|
2022
2520
|
}
|
2521
|
+
config["$invokedTriggers"] = JSON.stringify(invokedTriggers);
|
2023
2522
|
return config;
|
2024
2523
|
}
|
2025
2524
|
async resolveInstanceInput(input) {
|
2026
|
-
|
2027
|
-
|
2028
|
-
|
2029
|
-
return await this.resolveSingleInstanceInput(input);
|
2525
|
+
const promises = input.map((input2) => this.resolveSingleInstanceInput(input2));
|
2526
|
+
const results = await Promise.all(promises);
|
2527
|
+
return results.flat();
|
2030
2528
|
}
|
2031
2529
|
async resolveSingleInstanceInput(input) {
|
2032
2530
|
const loadedInstance = this.instanceMap.get(input.instanceId);
|
2033
2531
|
if (loadedInstance && !loadedInstance.parentId) {
|
2034
|
-
return input;
|
2532
|
+
return [input];
|
2035
2533
|
}
|
2036
2534
|
const parentInstance = await this.loadCompositeInstance(input.instanceId);
|
2037
2535
|
const parentOutput = parentInstance.instance.outputs?.[input.output];
|
@@ -2061,10 +2559,10 @@ class RuntimeOperation {
|
|
2061
2559
|
return compositeInstance;
|
2062
2560
|
});
|
2063
2561
|
}
|
2064
|
-
async evaluateCompositeInstances(
|
2562
|
+
async evaluateCompositeInstances() {
|
2065
2563
|
await this.projectLock.lockInstances(this.operation.instanceIds, async () => {
|
2066
2564
|
const compositeInstances = await this.libraryBackend.evaluateCompositeInstances(
|
2067
|
-
|
2565
|
+
Array.from(this.instanceMap.values()),
|
2068
2566
|
this.operation.instanceIds
|
2069
2567
|
);
|
2070
2568
|
this.abortController.signal.throwIfAborted();
|
@@ -2079,7 +2577,7 @@ class RuntimeOperation {
|
|
2079
2577
|
await Promise.all(
|
2080
2578
|
compositeInstances.map(async (instance) => ({
|
2081
2579
|
...instance,
|
2082
|
-
inputHash: await this.
|
2580
|
+
inputHash: await this.resolveInputHash(instance.instance.id)
|
2083
2581
|
}))
|
2084
2582
|
)
|
2085
2583
|
);
|
@@ -2090,12 +2588,15 @@ class RuntimeOperation {
|
|
2090
2588
|
await this.stateBackend.putOperation(this.operation);
|
2091
2589
|
}
|
2092
2590
|
updateInstanceState(patch) {
|
2093
|
-
if (patch.
|
2094
|
-
|
2095
|
-
|
2591
|
+
if (!patch.id) {
|
2592
|
+
throw new Error("The ID of the instance state is required.");
|
2593
|
+
}
|
2594
|
+
if (patch.logLine) {
|
2595
|
+
this.persistLogs.call([patch.id, patch.logLine]);
|
2596
|
+
this.instanceLogsEE.emit(`${this.operation.id}/${patch.id}`, patch.logLine);
|
2096
2597
|
return;
|
2097
2598
|
}
|
2098
|
-
const state =
|
2599
|
+
const state = applyPartialInstanceState(this.stateMap, patch);
|
2099
2600
|
this.persistStates.call(state);
|
2100
2601
|
if (patch.secrets) {
|
2101
2602
|
this.persistSecrets.call([patch.id, patch.secrets]);
|
@@ -2110,6 +2611,20 @@ class RuntimeOperation {
|
|
2110
2611
|
children.push(state);
|
2111
2612
|
this.childrenStateMap.set(state.parentId, children);
|
2112
2613
|
}
|
2614
|
+
addDependentState(state) {
|
2615
|
+
const instance = this.instanceMap.get(state.id);
|
2616
|
+
if (!instance) {
|
2617
|
+
return;
|
2618
|
+
}
|
2619
|
+
const instanceInputs = this.resolvedInstanceInputs.get(state.id) ?? {};
|
2620
|
+
for (const inputs of Object.values(instanceInputs)) {
|
2621
|
+
for (const input of inputs) {
|
2622
|
+
const dependents = this.dependentStateMap.get(input.input.instanceId) ?? [];
|
2623
|
+
dependents.push(state);
|
2624
|
+
this.dependentStateMap.set(input.input.instanceId, dependents);
|
2625
|
+
}
|
2626
|
+
}
|
2627
|
+
}
|
2113
2628
|
async extendWithNotCreatedDependencies() {
|
2114
2629
|
const instanceIdsSet = /* @__PURE__ */ new Set();
|
2115
2630
|
const traverse = async (instanceId) => {
|
@@ -2120,17 +2635,14 @@ class RuntimeOperation {
|
|
2120
2635
|
if (!instance) {
|
2121
2636
|
return;
|
2122
2637
|
}
|
2123
|
-
|
2124
|
-
|
2125
|
-
|
2126
|
-
|
2127
|
-
}
|
2128
|
-
} else {
|
2129
|
-
await traverse(input.instanceId);
|
2638
|
+
const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
|
2639
|
+
for (const inputs of Object.values(instanceInputs)) {
|
2640
|
+
for (const input of inputs) {
|
2641
|
+
await traverse(input.input.instanceId);
|
2130
2642
|
}
|
2131
2643
|
}
|
2132
2644
|
const state = this.stateMap.get(instance.id);
|
2133
|
-
const expectedInputHash = await this.
|
2645
|
+
const expectedInputHash = await this.resolveInputHash(instance.id);
|
2134
2646
|
if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
|
2135
2647
|
instanceIdsSet.add(instanceId);
|
2136
2648
|
}
|
@@ -2151,8 +2663,9 @@ class RuntimeOperation {
|
|
2151
2663
|
if (!state || state.status === "not_created") {
|
2152
2664
|
return;
|
2153
2665
|
}
|
2154
|
-
|
2155
|
-
|
2666
|
+
const dependents = this.dependentStateMap.get(instanceKey) ?? [];
|
2667
|
+
for (const dependent of dependents) {
|
2668
|
+
traverse(dependent.id);
|
2156
2669
|
instanceIdsSet.add(instanceKey);
|
2157
2670
|
}
|
2158
2671
|
};
|
@@ -2192,16 +2705,10 @@ class RuntimeOperation {
|
|
2192
2705
|
}
|
2193
2706
|
getInstanceDependencies(instance) {
|
2194
2707
|
const dependencies = [];
|
2195
|
-
|
2196
|
-
|
2197
|
-
|
2198
|
-
|
2199
|
-
if (dependency) {
|
2200
|
-
dependencies.push(dependency);
|
2201
|
-
}
|
2202
|
-
}
|
2203
|
-
} else {
|
2204
|
-
const dependency = this.instanceMap.get(input.instanceId);
|
2708
|
+
const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
|
2709
|
+
for (const inputs of Object.values(instanceInputs)) {
|
2710
|
+
for (const input of inputs) {
|
2711
|
+
const dependency = this.instanceMap.get(input.input.instanceId);
|
2205
2712
|
if (dependency) {
|
2206
2713
|
dependencies.push(dependency);
|
2207
2714
|
}
|
@@ -2210,22 +2717,14 @@ class RuntimeOperation {
|
|
2210
2717
|
return dependencies;
|
2211
2718
|
}
|
2212
2719
|
resolveUnitSource(component) {
|
2213
|
-
if (!component.source) {
|
2720
|
+
if (!component.source || component.source.type === "local") {
|
2214
2721
|
return {
|
2215
2722
|
type: "local",
|
2216
|
-
path
|
2723
|
+
// auto-generate path for local units
|
2724
|
+
path: component.source?.path ?? component.type.split(".").join("/")
|
2217
2725
|
};
|
2218
2726
|
}
|
2219
|
-
return
|
2220
|
-
...component.source,
|
2221
|
-
path: component.source.path ?? component.type.replaceAll(".", "/")
|
2222
|
-
};
|
2223
|
-
}
|
2224
|
-
static accumulator(values, value) {
|
2225
|
-
if (!values) {
|
2226
|
-
return [value];
|
2227
|
-
}
|
2228
|
-
return [...values, value];
|
2727
|
+
return component.source;
|
2229
2728
|
}
|
2230
2729
|
persistStates = funnel(
|
2231
2730
|
(states) => {
|
@@ -2240,18 +2739,18 @@ class RuntimeOperation {
|
|
2240
2739
|
minQuietPeriodMs: 100,
|
2241
2740
|
maxBurstDurationMs: 1e3,
|
2242
2741
|
triggerAt: "end",
|
2243
|
-
reducer:
|
2742
|
+
reducer: arrayAccumulator
|
2244
2743
|
}
|
2245
2744
|
);
|
2246
2745
|
persistLogs = funnel(
|
2247
2746
|
(entries) => {
|
2248
|
-
this.logger.
|
2747
|
+
this.logger.trace({ msg: "persisting logs", count: entries.length });
|
2249
2748
|
void this.stateBackend.appendInstanceLogs(this.operation.id, entries);
|
2250
2749
|
},
|
2251
2750
|
{
|
2252
2751
|
minQuietPeriodMs: 100,
|
2253
2752
|
maxBurstDurationMs: 200,
|
2254
|
-
reducer:
|
2753
|
+
reducer: arrayAccumulator
|
2255
2754
|
}
|
2256
2755
|
);
|
2257
2756
|
persistSecrets = funnel(
|
@@ -2267,9 +2766,22 @@ class RuntimeOperation {
|
|
2267
2766
|
{
|
2268
2767
|
minQuietPeriodMs: 100,
|
2269
2768
|
maxBurstDurationMs: 200,
|
2270
|
-
reducer:
|
2769
|
+
reducer: arrayAccumulator
|
2271
2770
|
}
|
2272
2771
|
);
|
2772
|
+
static abortMessagePatterns = [
|
2773
|
+
"Operation aborted",
|
2774
|
+
"Command was killed with SIGINT"
|
2775
|
+
];
|
2776
|
+
static isAbortError(err) {
|
2777
|
+
if (isAbortError(err)) {
|
2778
|
+
return true;
|
2779
|
+
}
|
2780
|
+
if (err instanceof Error) {
|
2781
|
+
return RuntimeOperation.abortMessagePatterns.some((pattern) => err.message.includes(pattern));
|
2782
|
+
}
|
2783
|
+
return false;
|
2784
|
+
}
|
2273
2785
|
}
|
2274
2786
|
|
2275
2787
|
class OperationManager {
|
@@ -2395,7 +2907,8 @@ class OperationManager {
|
|
2395
2907
|
const activeOperations = await stateBackend.getActiveOperations();
|
2396
2908
|
for (const operation of activeOperations) {
|
2397
2909
|
logger.info({ msg: "relaunching operation", operationId: operation.id });
|
2398
|
-
|
2910
|
+
operation.status = "cancelled";
|
2911
|
+
await stateBackend.putOperation(operation);
|
2399
2912
|
}
|
2400
2913
|
return operator;
|
2401
2914
|
}
|
@@ -2420,7 +2933,8 @@ async function createServices({
|
|
2420
2933
|
config ??= await loadConfig();
|
2421
2934
|
logger ??= pino({
|
2422
2935
|
name: config.HIGHSTATE_BACKEND_LOGGER_NAME,
|
2423
|
-
level: config.HIGHSTATE_BACKEND_LOGGER_LEVEL
|
2936
|
+
level: config.HIGHSTATE_BACKEND_LOGGER_LEVEL,
|
2937
|
+
errorKey: "error"
|
2424
2938
|
});
|
2425
2939
|
const localPulumiHost = LocalPulumiHost.create(logger);
|
2426
2940
|
const projectLockManager = new ProjectLockManager();
|
@@ -2430,7 +2944,7 @@ async function createServices({
|
|
2430
2944
|
runnerBackend ??= await createRunnerBackend(config, localPulumiHost);
|
2431
2945
|
projectBackend ??= await createProjectBackend(config);
|
2432
2946
|
terminalBackend ??= createTerminalBackend(config, logger);
|
2433
|
-
terminalManager ??= TerminalManager.create(terminalBackend, logger);
|
2947
|
+
terminalManager ??= TerminalManager.create(terminalBackend, stateBackend, runnerBackend, logger);
|
2434
2948
|
operationManager ??= await OperationManager.create(
|
2435
2949
|
runnerBackend,
|
2436
2950
|
stateBackend,
|