@highstate/backend 0.7.0 → 0.7.1
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 +42 -6
- package/dist/index.mjs +357 -160
- package/dist/runner/source-resolution-worker.mjs +22 -0
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.mjs +2 -2
- package/dist/{terminal-NNJYvGqi.d.ts → terminal-Cm2WqcyB.d.ts} +192 -28
- package/dist/{terminal-C4MfopTF.mjs → terminal-CqIsctlZ.mjs} +92 -11
- package/package.json +6 -3
package/dist/index.mjs
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { z } from 'zod';
|
2
|
-
import { pickBy, mapKeys, mapValues,
|
2
|
+
import { pickBy, mapKeys, mapValues, omit, pick } from 'remeda';
|
3
3
|
import { BetterLock } from 'better-lock';
|
4
4
|
import { basename, relative, dirname, resolve as resolve$1 } from 'node:path';
|
5
5
|
import { findWorkspaceDir, readPackageJSON } from 'pkg-types';
|
@@ -10,7 +10,7 @@ import Watcher from 'watcher';
|
|
10
10
|
import { resolve } from 'import-meta-resolve';
|
11
11
|
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
|
12
12
|
import { getInstanceId, isUnitModel, parseInstanceId } from '@highstate/contract';
|
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
|
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 createInstanceStateFrontendPatch, o as applyPartialInstanceState } from './terminal-CqIsctlZ.mjs';
|
14
14
|
import { sha256 } from 'crypto-hash';
|
15
15
|
import 'ajv';
|
16
16
|
import { Readable, PassThrough } from 'node:stream';
|
@@ -57,11 +57,60 @@ function errorToString(error) {
|
|
57
57
|
}
|
58
58
|
return JSON.stringify(error);
|
59
59
|
}
|
60
|
-
function
|
61
|
-
|
62
|
-
|
60
|
+
function createAsyncBatcher(fn, { waitMs = 100, maxWaitTimeMs = 1e3 } = {}) {
|
61
|
+
let batch = [];
|
62
|
+
let activeTimeout = null;
|
63
|
+
let maxWaitTimeout = null;
|
64
|
+
let firstCallTimestamp = null;
|
65
|
+
async function processBatch() {
|
66
|
+
if (batch.length === 0) return;
|
67
|
+
const currentBatch = batch;
|
68
|
+
batch = [];
|
69
|
+
await fn(currentBatch);
|
70
|
+
if (maxWaitTimeout) {
|
71
|
+
clearTimeout(maxWaitTimeout);
|
72
|
+
maxWaitTimeout = null;
|
73
|
+
}
|
74
|
+
firstCallTimestamp = null;
|
75
|
+
}
|
76
|
+
function schedule() {
|
77
|
+
if (activeTimeout) clearTimeout(activeTimeout);
|
78
|
+
activeTimeout = setTimeout(() => {
|
79
|
+
activeTimeout = null;
|
80
|
+
void processBatch();
|
81
|
+
}, waitMs);
|
82
|
+
if (!firstCallTimestamp) {
|
83
|
+
firstCallTimestamp = Date.now();
|
84
|
+
maxWaitTimeout = setTimeout(() => {
|
85
|
+
if (activeTimeout) clearTimeout(activeTimeout);
|
86
|
+
activeTimeout = null;
|
87
|
+
void processBatch();
|
88
|
+
}, maxWaitTimeMs);
|
89
|
+
}
|
63
90
|
}
|
64
|
-
return
|
91
|
+
return {
|
92
|
+
/**
|
93
|
+
* Add an item to the batch.
|
94
|
+
*/
|
95
|
+
call(item) {
|
96
|
+
batch.push(item);
|
97
|
+
schedule();
|
98
|
+
},
|
99
|
+
/**
|
100
|
+
* Immediately flush the pending batch (if any).
|
101
|
+
*/
|
102
|
+
async flush() {
|
103
|
+
if (activeTimeout) {
|
104
|
+
clearTimeout(activeTimeout);
|
105
|
+
activeTimeout = null;
|
106
|
+
}
|
107
|
+
if (maxWaitTimeout) {
|
108
|
+
clearTimeout(maxWaitTimeout);
|
109
|
+
maxWaitTimeout = null;
|
110
|
+
}
|
111
|
+
await processBatch();
|
112
|
+
}
|
113
|
+
};
|
65
114
|
}
|
66
115
|
|
67
116
|
class LocalPulumiHost {
|
@@ -79,14 +128,15 @@ class LocalPulumiHost {
|
|
79
128
|
return null;
|
80
129
|
}
|
81
130
|
}
|
82
|
-
async
|
131
|
+
async runEmpty(options, fn) {
|
132
|
+
const { projectId, pulumiProjectName, pulumiStackName, envVars } = options;
|
83
133
|
return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
|
84
134
|
const { LocalWorkspace } = await import('@pulumi/pulumi/automation/index.js');
|
85
135
|
const stack = await LocalWorkspace.createOrSelectStack(
|
86
136
|
{
|
87
137
|
projectName: pulumiProjectName,
|
88
138
|
stackName: pulumiStackName,
|
89
|
-
program
|
139
|
+
program: () => Promise.resolve()
|
90
140
|
},
|
91
141
|
{
|
92
142
|
projectSettings: {
|
@@ -113,25 +163,14 @@ class LocalPulumiHost {
|
|
113
163
|
}
|
114
164
|
});
|
115
165
|
}
|
116
|
-
async
|
117
|
-
|
118
|
-
projectId,
|
119
|
-
pulumiProjectName,
|
120
|
-
pulumiStackName,
|
121
|
-
async () => {
|
122
|
-
},
|
123
|
-
fn,
|
124
|
-
envVars
|
125
|
-
);
|
126
|
-
}
|
127
|
-
// TODO: extract args to options object
|
128
|
-
async runLocal(projectId, pulumiProjectName, pulumiStackName, programPathResolver, fn, stackConfig, envVars) {
|
166
|
+
async runLocal(options, fn) {
|
167
|
+
const { projectId, pulumiProjectName, pulumiStackName, projectPath, stackConfig, envVars } = options;
|
129
168
|
return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
|
130
169
|
const { LocalWorkspace } = await import('@pulumi/pulumi/automation/index.js');
|
131
170
|
const stack = await LocalWorkspace.createOrSelectStack(
|
132
171
|
{
|
133
172
|
stackName: pulumiStackName,
|
134
|
-
workDir:
|
173
|
+
workDir: projectPath
|
135
174
|
},
|
136
175
|
{
|
137
176
|
projectSettings: {
|
@@ -260,10 +299,12 @@ class LocalSecretBackend {
|
|
260
299
|
this.pulumiProjectHost.setPassword(projectId, password);
|
261
300
|
try {
|
262
301
|
await this.pulumiProjectHost.runLocal(
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
302
|
+
{
|
303
|
+
projectId,
|
304
|
+
pulumiProjectName: this.projectName,
|
305
|
+
pulumiStackName: projectId,
|
306
|
+
projectPath: this.projectPath
|
307
|
+
},
|
267
308
|
async (stack) => {
|
268
309
|
this.logger.debug({ projectId }, "checking password");
|
269
310
|
await stack.info(true);
|
@@ -283,10 +324,12 @@ class LocalSecretBackend {
|
|
283
324
|
}
|
284
325
|
get(projectId, instanceId) {
|
285
326
|
return this.pulumiProjectHost.runLocal(
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
327
|
+
{
|
328
|
+
projectId,
|
329
|
+
pulumiProjectName: this.projectName,
|
330
|
+
pulumiStackName: projectId,
|
331
|
+
projectPath: this.projectPath
|
332
|
+
},
|
290
333
|
async (stack) => {
|
291
334
|
this.logger.debug({ projectId, instanceId }, "getting secrets");
|
292
335
|
const config = await stack.getAllConfig();
|
@@ -299,10 +342,12 @@ class LocalSecretBackend {
|
|
299
342
|
}
|
300
343
|
set(projectId, instanceId, values) {
|
301
344
|
return this.pulumiProjectHost.runLocal(
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
345
|
+
{
|
346
|
+
projectId,
|
347
|
+
pulumiProjectName: this.projectName,
|
348
|
+
pulumiStackName: projectId,
|
349
|
+
projectPath: this.projectPath
|
350
|
+
},
|
306
351
|
async (stack) => {
|
307
352
|
this.logger.debug({ projectId, instanceId }, "setting secrets");
|
308
353
|
const componentSecrets = mapValues(
|
@@ -777,6 +822,9 @@ class ProjectLock {
|
|
777
822
|
this.lock = lock;
|
778
823
|
this.projectId = projectId;
|
779
824
|
}
|
825
|
+
canImmediatelyAcquireLock(instanceId) {
|
826
|
+
return this.lock.canAcquire(`${this.projectId}/${instanceId}`);
|
827
|
+
}
|
780
828
|
lockInstance(instanceId, fn) {
|
781
829
|
return this.lock.acquire(`${this.projectId}/${instanceId}`, fn);
|
782
830
|
}
|
@@ -1189,17 +1237,10 @@ class TerminalManager {
|
|
1189
1237
|
logger.child({ service: "TerminalManager" })
|
1190
1238
|
);
|
1191
1239
|
}
|
1192
|
-
persistHistory =
|
1193
|
-
(entries)
|
1194
|
-
|
1195
|
-
|
1196
|
-
},
|
1197
|
-
{
|
1198
|
-
minQuietPeriodMs: 100,
|
1199
|
-
maxBurstDurationMs: 200,
|
1200
|
-
reducer: arrayAccumulator
|
1201
|
-
}
|
1202
|
-
);
|
1240
|
+
persistHistory = createAsyncBatcher(async (entries) => {
|
1241
|
+
this.logger.trace({ msg: "persisting history lines", count: entries.length });
|
1242
|
+
await this.stateBackend.appendTerminalSessionHistory(entries);
|
1243
|
+
});
|
1203
1244
|
}
|
1204
1245
|
|
1205
1246
|
class InvalidInstanceStatusError extends Error {
|
@@ -1244,9 +1285,11 @@ class LocalRunnerBackend {
|
|
1244
1285
|
}
|
1245
1286
|
getState(options) {
|
1246
1287
|
return this.pulumiProjectHost.runEmpty(
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1288
|
+
{
|
1289
|
+
projectId: options.projectId,
|
1290
|
+
pulumiProjectName: options.instanceType,
|
1291
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options)
|
1292
|
+
},
|
1250
1293
|
async (stack) => {
|
1251
1294
|
const info = await stack.info();
|
1252
1295
|
const instanceId = getInstanceId(options.instanceType, options.instanceName);
|
@@ -1273,9 +1316,11 @@ class LocalRunnerBackend {
|
|
1273
1316
|
}
|
1274
1317
|
getTerminalFactory(options, terminalName) {
|
1275
1318
|
return this.pulumiProjectHost.runEmpty(
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1319
|
+
{
|
1320
|
+
projectId: options.projectId,
|
1321
|
+
pulumiProjectName: options.instanceType,
|
1322
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options)
|
1323
|
+
},
|
1279
1324
|
async (stack) => {
|
1280
1325
|
const outputs = await stack.outputs();
|
1281
1326
|
if (!outputs["$terminals"]) {
|
@@ -1292,9 +1337,11 @@ class LocalRunnerBackend {
|
|
1292
1337
|
}
|
1293
1338
|
getPageContent(options, pageName) {
|
1294
1339
|
return this.pulumiProjectHost.runEmpty(
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1340
|
+
{
|
1341
|
+
projectId: options.projectId,
|
1342
|
+
pulumiProjectName: options.instanceType,
|
1343
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options)
|
1344
|
+
},
|
1298
1345
|
async (stack) => {
|
1299
1346
|
const outputs = await stack.outputs();
|
1300
1347
|
if (!outputs["$pages"]) {
|
@@ -1311,9 +1358,11 @@ class LocalRunnerBackend {
|
|
1311
1358
|
}
|
1312
1359
|
getFileContent(options, fileName) {
|
1313
1360
|
return this.pulumiProjectHost.runEmpty(
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1361
|
+
{
|
1362
|
+
projectId: options.projectId,
|
1363
|
+
pulumiProjectName: options.instanceType,
|
1364
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options)
|
1365
|
+
},
|
1317
1366
|
async (stack) => {
|
1318
1367
|
const outputs = await stack.outputs();
|
1319
1368
|
if (!outputs["$files"]) {
|
@@ -1342,21 +1391,47 @@ class LocalRunnerBackend {
|
|
1342
1391
|
...mapValues(options.config, (value) => ({ value })),
|
1343
1392
|
...mapValues(options.secrets, (value) => ({ value, secret: true }))
|
1344
1393
|
};
|
1345
|
-
void this.updateWorker(options, configMap);
|
1394
|
+
void this.updateWorker(options, configMap, false);
|
1395
|
+
}
|
1396
|
+
async preview(options) {
|
1397
|
+
const currentStatus = await this.validateStatus(options, [
|
1398
|
+
"not_created",
|
1399
|
+
"previewing",
|
1400
|
+
"created",
|
1401
|
+
"error"
|
1402
|
+
]);
|
1403
|
+
if (currentStatus === "previewing") {
|
1404
|
+
return;
|
1405
|
+
}
|
1406
|
+
const configMap = {
|
1407
|
+
...mapValues(options.config, (value) => ({ value })),
|
1408
|
+
...mapValues(options.secrets, (value) => ({ value, secret: true }))
|
1409
|
+
};
|
1410
|
+
void this.updateWorker(options, configMap, true);
|
1346
1411
|
}
|
1347
|
-
async updateWorker(options, configMap) {
|
1412
|
+
async updateWorker(options, configMap, preview) {
|
1348
1413
|
const instanceId = LocalRunnerBackend.getInstanceId(options);
|
1349
1414
|
try {
|
1415
|
+
const [projectPath, allowedDependencies] = await this.resolveProjectPath(
|
1416
|
+
options.instanceName,
|
1417
|
+
options.source
|
1418
|
+
);
|
1350
1419
|
await this.pulumiProjectHost.runLocal(
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1420
|
+
{
|
1421
|
+
projectId: options.projectId,
|
1422
|
+
pulumiProjectName: options.instanceType,
|
1423
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
1424
|
+
projectPath,
|
1425
|
+
stackConfig: configMap,
|
1426
|
+
envVars: {
|
1427
|
+
HIGHSTATE_CACHE_DIR: this.cacheDir
|
1428
|
+
}
|
1429
|
+
},
|
1355
1430
|
async (stack) => {
|
1356
|
-
await
|
1431
|
+
await this.setStackConfig(stack, configMap);
|
1357
1432
|
this.updateState({
|
1358
1433
|
id: instanceId,
|
1359
|
-
status: "updating",
|
1434
|
+
status: preview ? "previewing" : "updating",
|
1360
1435
|
currentResourceCount: 0,
|
1361
1436
|
totalResourceCount: 0
|
1362
1437
|
});
|
@@ -1364,8 +1439,11 @@ class LocalRunnerBackend {
|
|
1364
1439
|
let totalResourceCount = 0;
|
1365
1440
|
await runWithRetryOnError(
|
1366
1441
|
async () => {
|
1367
|
-
await stack
|
1442
|
+
await stack[preview ? "preview" : "up"]({
|
1368
1443
|
color: "always",
|
1444
|
+
refresh: options.refresh,
|
1445
|
+
signal: options.signal,
|
1446
|
+
diff: preview,
|
1369
1447
|
onEvent: (event) => {
|
1370
1448
|
if (event.resourcePreEvent) {
|
1371
1449
|
totalResourceCount = updateResourceCount(
|
@@ -1389,8 +1467,7 @@ class LocalRunnerBackend {
|
|
1389
1467
|
if (this.printOutput) {
|
1390
1468
|
console.log(message);
|
1391
1469
|
}
|
1392
|
-
}
|
1393
|
-
signal: options.signal
|
1470
|
+
}
|
1394
1471
|
});
|
1395
1472
|
const extraOutputs = await this.getExtraOutputsStatePatch(stack);
|
1396
1473
|
this.updateState({
|
@@ -1403,15 +1480,14 @@ class LocalRunnerBackend {
|
|
1403
1480
|
async (error) => {
|
1404
1481
|
const isUnlocked = await this.pulumiProjectHost.tryUnlockStack(stack, error);
|
1405
1482
|
if (isUnlocked) return true;
|
1406
|
-
const isResolved = await this.tryInstallMissingDependencies(
|
1483
|
+
const isResolved = await this.tryInstallMissingDependencies(
|
1484
|
+
error,
|
1485
|
+
allowedDependencies
|
1486
|
+
);
|
1407
1487
|
if (isResolved) return true;
|
1408
1488
|
return false;
|
1409
1489
|
}
|
1410
1490
|
);
|
1411
|
-
},
|
1412
|
-
configMap,
|
1413
|
-
{
|
1414
|
-
HIGHSTATE_CACHE_DIR: this.cacheDir
|
1415
1491
|
}
|
1416
1492
|
);
|
1417
1493
|
} catch (error) {
|
@@ -1422,6 +1498,12 @@ class LocalRunnerBackend {
|
|
1422
1498
|
});
|
1423
1499
|
}
|
1424
1500
|
}
|
1501
|
+
async setStackConfig(stack, configMap) {
|
1502
|
+
const currentConfig = await stack.getAllConfig();
|
1503
|
+
const currentConfigKeys = Object.keys(currentConfig);
|
1504
|
+
await stack.removeAllConfig(currentConfigKeys);
|
1505
|
+
await stack.setAllConfig(configMap);
|
1506
|
+
}
|
1425
1507
|
async destroy(options) {
|
1426
1508
|
const currentStatus = await this.validateStatus(options, [
|
1427
1509
|
"not_created",
|
@@ -1437,10 +1519,18 @@ class LocalRunnerBackend {
|
|
1437
1519
|
async destroyWorker(options) {
|
1438
1520
|
const instanceId = LocalRunnerBackend.getInstanceId(options);
|
1439
1521
|
try {
|
1440
|
-
await this.
|
1441
|
-
|
1442
|
-
|
1443
|
-
|
1522
|
+
const [projectPath] = await this.resolveProjectPath(options.instanceName, options.source);
|
1523
|
+
await this.pulumiProjectHost.runLocal(
|
1524
|
+
{
|
1525
|
+
projectId: options.projectId,
|
1526
|
+
pulumiProjectName: options.instanceType,
|
1527
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options),
|
1528
|
+
projectPath,
|
1529
|
+
envVars: {
|
1530
|
+
HIGHSTATE_CACHE_DIR: this.cacheDir,
|
1531
|
+
PULUMI_K8S_DELETE_UNREACHABLE: options.deleteUnreachable ? "true" : ""
|
1532
|
+
}
|
1533
|
+
},
|
1444
1534
|
async (stack) => {
|
1445
1535
|
const summary = await stack.workspace.stack();
|
1446
1536
|
let currentResourceCount = summary?.resourceCount ?? 0;
|
@@ -1454,6 +1544,9 @@ class LocalRunnerBackend {
|
|
1454
1544
|
async () => {
|
1455
1545
|
await stack.destroy({
|
1456
1546
|
color: "always",
|
1547
|
+
refresh: options.refresh,
|
1548
|
+
remove: true,
|
1549
|
+
signal: options.signal,
|
1457
1550
|
onEvent: (event) => {
|
1458
1551
|
if (event.resOutputsEvent) {
|
1459
1552
|
currentResourceCount = updateResourceCount(
|
@@ -1469,8 +1562,7 @@ class LocalRunnerBackend {
|
|
1469
1562
|
if (this.printOutput) {
|
1470
1563
|
console.log(message);
|
1471
1564
|
}
|
1472
|
-
}
|
1473
|
-
signal: options.signal
|
1565
|
+
}
|
1474
1566
|
});
|
1475
1567
|
const extraOutputs = await this.getExtraOutputsStatePatch(stack);
|
1476
1568
|
this.updateState({
|
@@ -1482,12 +1574,19 @@ class LocalRunnerBackend {
|
|
1482
1574
|
},
|
1483
1575
|
(error) => this.pulumiProjectHost.tryUnlockStack(stack, error)
|
1484
1576
|
);
|
1485
|
-
},
|
1486
|
-
{
|
1487
|
-
PULUMI_K8S_DELETE_UNREACHABLE: "true"
|
1488
1577
|
}
|
1489
1578
|
);
|
1490
1579
|
} catch (error) {
|
1580
|
+
const { StackNotFoundError } = await import('@pulumi/pulumi/automation');
|
1581
|
+
if (error instanceof StackNotFoundError) {
|
1582
|
+
this.updateState({
|
1583
|
+
id: instanceId,
|
1584
|
+
status: "not_created",
|
1585
|
+
totalResourceCount: 0,
|
1586
|
+
currentResourceCount: 0
|
1587
|
+
});
|
1588
|
+
return;
|
1589
|
+
}
|
1491
1590
|
this.updateState({
|
1492
1591
|
id: instanceId,
|
1493
1592
|
status: "error",
|
@@ -1511,9 +1610,11 @@ class LocalRunnerBackend {
|
|
1511
1610
|
const instanceId = LocalRunnerBackend.getInstanceId(options);
|
1512
1611
|
try {
|
1513
1612
|
await this.pulumiProjectHost.runEmpty(
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1613
|
+
{
|
1614
|
+
projectId: options.projectId,
|
1615
|
+
pulumiProjectName: options.instanceType,
|
1616
|
+
pulumiStackName: LocalRunnerBackend.getStackName(options)
|
1617
|
+
},
|
1517
1618
|
async (stack) => {
|
1518
1619
|
const summary = await stack.workspace.stack();
|
1519
1620
|
let currentResourceCount = 0;
|
@@ -1635,24 +1736,33 @@ class LocalRunnerBackend {
|
|
1635
1736
|
static getInstanceId(options) {
|
1636
1737
|
return getInstanceId(options.instanceType, options.instanceName);
|
1637
1738
|
}
|
1638
|
-
async resolveProjectPath(source) {
|
1739
|
+
async resolveProjectPath(instanceType, source) {
|
1639
1740
|
if (source.type === "local") {
|
1640
|
-
|
1641
|
-
|
1642
|
-
|
1643
|
-
return resolve$1(this.sourceBasePath, source.path);
|
1741
|
+
const path = source.path ?? instanceType.replace(/\./g, "/");
|
1742
|
+
const projectPath = resolve$1(this.sourceBasePath, path);
|
1743
|
+
return [projectPath, []];
|
1644
1744
|
}
|
1645
1745
|
if (!this.skipSourceCheck) {
|
1646
1746
|
const packageName = source.version ? `${source.package}@${source.version}` : source.package;
|
1647
1747
|
await ensureDependencyInstalled(packageName);
|
1648
1748
|
}
|
1649
|
-
const
|
1650
|
-
|
1651
|
-
|
1652
|
-
|
1653
|
-
|
1749
|
+
const workerPathUrl = resolve(
|
1750
|
+
`@highstate/backend/source-resolution-worker`,
|
1751
|
+
import.meta.url
|
1752
|
+
);
|
1753
|
+
const workerPath = fileURLToPath(workerPathUrl);
|
1754
|
+
const worker = new Worker(workerPath, {
|
1755
|
+
workerData: { source, skipSourceCheck: this.skipSourceCheck }
|
1756
|
+
});
|
1757
|
+
for await (const [event] of on(worker, "message")) {
|
1758
|
+
const eventData = event;
|
1759
|
+
if (eventData.type === "result") {
|
1760
|
+
return [eventData.projectPath, eventData.allowedDependencies];
|
1761
|
+
}
|
1762
|
+
}
|
1763
|
+
throw new Error("Worker ended without sending the result");
|
1654
1764
|
}
|
1655
|
-
async tryInstallMissingDependencies(error) {
|
1765
|
+
async tryInstallMissingDependencies(error, allowedDependencies) {
|
1656
1766
|
if (!(error instanceof Error)) {
|
1657
1767
|
return false;
|
1658
1768
|
}
|
@@ -1662,6 +1772,11 @@ class LocalRunnerBackend {
|
|
1662
1772
|
return false;
|
1663
1773
|
}
|
1664
1774
|
const packageName = match[1];
|
1775
|
+
if (!allowedDependencies.includes(packageName)) {
|
1776
|
+
throw new Error(
|
1777
|
+
`Dependency '${packageName}' was requested to be auto-installed, but it is not allowed. Please add it to the 'peerDependencies' in the package.json of the unit.`
|
1778
|
+
);
|
1779
|
+
}
|
1665
1780
|
await ensureDependencyInstalled(packageName);
|
1666
1781
|
return true;
|
1667
1782
|
}
|
@@ -2001,7 +2116,7 @@ class LocalStateBackend {
|
|
2001
2116
|
const currentUser = await localPulumiHost.getCurrentUser();
|
2002
2117
|
if (!currentUser) {
|
2003
2118
|
throw new Error(
|
2004
|
-
"The pulumi is not
|
2119
|
+
"The pulumi state is not specified, please run `pulumi login` or specify the state location manually before restarting the service"
|
2005
2120
|
);
|
2006
2121
|
}
|
2007
2122
|
if (!currentUser.url) {
|
@@ -2154,6 +2269,14 @@ class RuntimeOperation {
|
|
2154
2269
|
this.persistSecrets.flush()
|
2155
2270
|
]);
|
2156
2271
|
this.logger.debug("operation finished, all entries persisted");
|
2272
|
+
if (this.operation.type === "preview") {
|
2273
|
+
const states = await this.stateBackend.getInstanceStates(this.operation.projectId);
|
2274
|
+
for (const state of states) {
|
2275
|
+
if (this.operation.instanceIds.includes(state.id)) {
|
2276
|
+
this.stateEE.emit(this.operation.projectId, createInstanceStateFrontendPatch(state));
|
2277
|
+
}
|
2278
|
+
}
|
2279
|
+
}
|
2157
2280
|
}
|
2158
2281
|
}
|
2159
2282
|
resetPendingStateStatuses() {
|
@@ -2231,9 +2354,9 @@ class RuntimeOperation {
|
|
2231
2354
|
{ promiseCache: this.inputHashPromiseCache }
|
2232
2355
|
);
|
2233
2356
|
if (this.operation.type === "update") {
|
2234
|
-
await this.
|
2357
|
+
await this.extendWithDependencies();
|
2235
2358
|
await this.updateOperation();
|
2236
|
-
} else if (this.operation.type === "destroy") {
|
2359
|
+
} else if (this.operation.type === "destroy" && this.operation.options.destroyDependentInstances !== false) {
|
2237
2360
|
this.extendWithCreatedDependents();
|
2238
2361
|
await this.updateOperation();
|
2239
2362
|
}
|
@@ -2280,7 +2403,8 @@ class RuntimeOperation {
|
|
2280
2403
|
}
|
2281
2404
|
async getUnitPromise(instance, component) {
|
2282
2405
|
switch (this.operation.type) {
|
2283
|
-
case "update":
|
2406
|
+
case "update":
|
2407
|
+
case "preview": {
|
2284
2408
|
return this.updateUnit(instance, component);
|
2285
2409
|
}
|
2286
2410
|
case "recreate": {
|
@@ -2368,11 +2492,12 @@ class RuntimeOperation {
|
|
2368
2492
|
const secrets = await this.secretBackend.get(this.operation.projectId, instance.id);
|
2369
2493
|
this.abortController.signal.throwIfAborted();
|
2370
2494
|
logger.debug("secrets loaded", { count: Object.keys(secrets).length });
|
2371
|
-
await this.runnerBackend.update({
|
2495
|
+
await this.runnerBackend[this.operation.type === "preview" ? "preview" : "update"]({
|
2372
2496
|
projectId: this.operation.projectId,
|
2373
2497
|
instanceType: instance.type,
|
2374
2498
|
instanceName: instance.name,
|
2375
2499
|
config: await this.prepareUnitConfig(instance),
|
2500
|
+
refresh: this.operation.options.refresh,
|
2376
2501
|
secrets: mapValues(secrets, (value) => valueToString(value)),
|
2377
2502
|
source: this.resolveUnitSource(component),
|
2378
2503
|
signal: this.abortController.signal
|
@@ -2385,23 +2510,34 @@ class RuntimeOperation {
|
|
2385
2510
|
finalStatuses: ["created", "error"]
|
2386
2511
|
});
|
2387
2512
|
await this.watchStateStream(stream);
|
2388
|
-
const inputHash = await this.
|
2389
|
-
|
2390
|
-
|
2391
|
-
|
2392
|
-
|
2393
|
-
sourceHash: void 0
|
2394
|
-
// TODO: implement source hash
|
2395
|
-
});
|
2396
|
-
this.inputHashPromiseCache.clear();
|
2397
|
-
return await this.resolveInputHash(instance.id);
|
2513
|
+
const inputHash = await this.getUpToDateInputHash(instance);
|
2514
|
+
this.updateInstanceState({
|
2515
|
+
id: instance.id,
|
2516
|
+
inputHash,
|
2517
|
+
dependencyIds: dependencies.map((dependency) => dependency.id)
|
2398
2518
|
});
|
2399
|
-
this.updateInstanceState({ id: instance.id, inputHash });
|
2400
2519
|
logger.debug("input hash after update", { inputHash });
|
2401
2520
|
logger.info("unit updated");
|
2402
2521
|
});
|
2403
2522
|
}
|
2523
|
+
async getUpToDateInputHash(instance) {
|
2524
|
+
return await this.inputHashLock.acquire(async () => {
|
2525
|
+
this.inputHashNodes.set(instance.id, {
|
2526
|
+
instance,
|
2527
|
+
resolvedInputs: this.resolvedInstanceInputs.get(instance.id),
|
2528
|
+
state: this.stateMap.get(instance.id),
|
2529
|
+
sourceHash: void 0
|
2530
|
+
// TODO: implement source hash
|
2531
|
+
});
|
2532
|
+
this.inputHashPromiseCache.clear();
|
2533
|
+
return await this.resolveInputHash(instance.id);
|
2534
|
+
});
|
2535
|
+
}
|
2404
2536
|
async processBeforeDestroyTriggers(state, component, logger) {
|
2537
|
+
if (!this.operation.options.invokeDestroyTriggers) {
|
2538
|
+
logger.debug("destroy triggers are disabled for the operation");
|
2539
|
+
return;
|
2540
|
+
}
|
2405
2541
|
const instance = this.instanceMap.get(state.id);
|
2406
2542
|
if (!instance) {
|
2407
2543
|
throw new Error(`Instance not found: ${state.id}`);
|
@@ -2414,12 +2550,13 @@ class RuntimeOperation {
|
|
2414
2550
|
logger.info("updating unit to process before-destroy triggers...");
|
2415
2551
|
const secrets = await this.secretBackend.get(this.operation.projectId, instance.id);
|
2416
2552
|
this.abortController.signal.throwIfAborted();
|
2417
|
-
logger.debug(
|
2553
|
+
logger.debug({ count: Object.keys(secrets).length }, "secrets loaded");
|
2418
2554
|
await this.runnerBackend.update({
|
2419
2555
|
projectId: this.operation.projectId,
|
2420
2556
|
instanceType: instance.type,
|
2421
2557
|
instanceName: instance.name,
|
2422
2558
|
config: await this.prepareUnitConfig(instance, invokedTriggers),
|
2559
|
+
refresh: this.operation.options.refresh,
|
2423
2560
|
secrets: mapValues(secrets, (value) => valueToString(value)),
|
2424
2561
|
source: this.resolveUnitSource(component),
|
2425
2562
|
signal: this.abortController.signal
|
@@ -2463,7 +2600,10 @@ class RuntimeOperation {
|
|
2463
2600
|
projectId: this.operation.projectId,
|
2464
2601
|
instanceType: type,
|
2465
2602
|
instanceName: name,
|
2466
|
-
|
2603
|
+
refresh: this.operation.options.refresh,
|
2604
|
+
signal: this.abortController.signal,
|
2605
|
+
source: this.resolveUnitSource(component),
|
2606
|
+
deleteUnreachable: this.operation.options.deleteUnreachableResources
|
2467
2607
|
});
|
2468
2608
|
this.logger.debug("destroy request sent");
|
2469
2609
|
const stream = this.runnerBackend.watch({
|
@@ -2497,7 +2637,10 @@ class RuntimeOperation {
|
|
2497
2637
|
projectId: this.operation.projectId,
|
2498
2638
|
instanceType: instance.type,
|
2499
2639
|
instanceName: instance.name,
|
2500
|
-
|
2640
|
+
refresh: this.operation.options.refresh,
|
2641
|
+
signal: this.abortController.signal,
|
2642
|
+
source: this.resolveUnitSource(component),
|
2643
|
+
deleteUnreachable: this.operation.options.deleteUnreachableResources
|
2501
2644
|
});
|
2502
2645
|
logger.debug("destroy request sent");
|
2503
2646
|
const destroyStream = this.runnerBackend.watch({
|
@@ -2518,6 +2661,7 @@ class RuntimeOperation {
|
|
2518
2661
|
instanceType: instance.type,
|
2519
2662
|
instanceName: instance.name,
|
2520
2663
|
config: await this.prepareUnitConfig(instance),
|
2664
|
+
refresh: this.operation.options.refresh,
|
2521
2665
|
secrets: mapValues(secrets, (value) => valueToString(value)),
|
2522
2666
|
source: this.resolveUnitSource(component),
|
2523
2667
|
signal: this.abortController.signal
|
@@ -2529,7 +2673,13 @@ class RuntimeOperation {
|
|
2529
2673
|
instanceName: instance.name,
|
2530
2674
|
finalStatuses: ["created", "error"]
|
2531
2675
|
});
|
2532
|
-
this.
|
2676
|
+
const inputHash = await this.getUpToDateInputHash(instance);
|
2677
|
+
const dependencies = this.getInstanceDependencies(instance);
|
2678
|
+
this.updateInstanceState({
|
2679
|
+
id: instance.id,
|
2680
|
+
inputHash,
|
2681
|
+
dependencyIds: dependencies.map((dependency) => dependency.id)
|
2682
|
+
});
|
2533
2683
|
await this.watchStateStream(updateStream);
|
2534
2684
|
logger.info("unit recreated");
|
2535
2685
|
});
|
@@ -2667,9 +2817,11 @@ class RuntimeOperation {
|
|
2667
2817
|
return;
|
2668
2818
|
}
|
2669
2819
|
const state = applyPartialInstanceState(this.stateMap, patch);
|
2670
|
-
this.
|
2671
|
-
|
2672
|
-
|
2820
|
+
if (this.operation.type !== "preview") {
|
2821
|
+
this.persistStates.call(state);
|
2822
|
+
if (patch.secrets) {
|
2823
|
+
this.persistSecrets.call([patch.id, patch.secrets]);
|
2824
|
+
}
|
2673
2825
|
}
|
2674
2826
|
this.stateEE.emit(this.operation.projectId, createInstanceStateFrontendPatch(patch));
|
2675
2827
|
}
|
@@ -2681,7 +2833,29 @@ class RuntimeOperation {
|
|
2681
2833
|
children.push(state);
|
2682
2834
|
this.childrenStateMap.set(state.parentId, children);
|
2683
2835
|
}
|
2836
|
+
removeStateFromParent(state) {
|
2837
|
+
if (!state.parentId) {
|
2838
|
+
return;
|
2839
|
+
}
|
2840
|
+
const children = this.childrenStateMap.get(state.parentId) ?? [];
|
2841
|
+
const index = children.findIndex((child) => child.id === state.id);
|
2842
|
+
if (index !== -1) {
|
2843
|
+
children.splice(index, 1);
|
2844
|
+
this.childrenStateMap.set(state.parentId, children);
|
2845
|
+
}
|
2846
|
+
}
|
2684
2847
|
addDependentState(state) {
|
2848
|
+
const instance = this.instanceMap.get(state.id);
|
2849
|
+
if (!instance) {
|
2850
|
+
return;
|
2851
|
+
}
|
2852
|
+
for (const dependency of state.dependencyIds) {
|
2853
|
+
const dependents = this.dependentStateMap.get(dependency) ?? [];
|
2854
|
+
dependents.push(state);
|
2855
|
+
this.dependentStateMap.set(dependency, dependents);
|
2856
|
+
}
|
2857
|
+
}
|
2858
|
+
removeDependentState(state) {
|
2685
2859
|
const instance = this.instanceMap.get(state.id);
|
2686
2860
|
if (!instance) {
|
2687
2861
|
return;
|
@@ -2690,12 +2864,15 @@ class RuntimeOperation {
|
|
2690
2864
|
for (const inputs of Object.values(instanceInputs)) {
|
2691
2865
|
for (const input of inputs) {
|
2692
2866
|
const dependents = this.dependentStateMap.get(input.input.instanceId) ?? [];
|
2693
|
-
dependents.
|
2694
|
-
|
2867
|
+
const index = dependents.findIndex((dependent) => dependent.id === state.id);
|
2868
|
+
if (index !== -1) {
|
2869
|
+
dependents.splice(index, 1);
|
2870
|
+
this.dependentStateMap.set(input.input.instanceId, dependents);
|
2871
|
+
}
|
2695
2872
|
}
|
2696
2873
|
}
|
2697
2874
|
}
|
2698
|
-
async
|
2875
|
+
async extendWithDependencies() {
|
2699
2876
|
const instanceIdsSet = /* @__PURE__ */ new Set();
|
2700
2877
|
const traverse = async (instanceId) => {
|
2701
2878
|
if (instanceIdsSet.has(instanceId)) {
|
@@ -2713,6 +2890,10 @@ class RuntimeOperation {
|
|
2713
2890
|
}
|
2714
2891
|
const state = this.stateMap.get(instance.id);
|
2715
2892
|
const expectedInputHash = await this.resolveInputHash(instance.id);
|
2893
|
+
if (this.operation.options.forceUpdateDependencies) {
|
2894
|
+
instanceIdsSet.add(instanceId);
|
2895
|
+
return;
|
2896
|
+
}
|
2716
2897
|
if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
|
2717
2898
|
instanceIdsSet.add(instanceId);
|
2718
2899
|
}
|
@@ -2754,7 +2935,15 @@ class RuntimeOperation {
|
|
2754
2935
|
if (instancePromise) {
|
2755
2936
|
return instancePromise;
|
2756
2937
|
}
|
2757
|
-
instancePromise =
|
2938
|
+
instancePromise = (async () => {
|
2939
|
+
const wasImmediatelyAcquired = this.projectLock.canImmediatelyAcquireLock(instanceId);
|
2940
|
+
await this.projectLock.lockInstance(instanceId, async () => {
|
2941
|
+
if (!wasImmediatelyAcquired) {
|
2942
|
+
await this.refetchState(instanceId);
|
2943
|
+
}
|
2944
|
+
return await fn();
|
2945
|
+
});
|
2946
|
+
})();
|
2758
2947
|
instancePromise = instancePromise.finally(() => this.instancePromiseMap.delete(instanceId));
|
2759
2948
|
this.instancePromiseMap.set(instanceId, instancePromise);
|
2760
2949
|
return instancePromise;
|
@@ -2763,6 +2952,8 @@ class RuntimeOperation {
|
|
2763
2952
|
switch (this.operation.type) {
|
2764
2953
|
case "update":
|
2765
2954
|
return "updating";
|
2955
|
+
case "preview":
|
2956
|
+
return "previewing";
|
2766
2957
|
case "recreate":
|
2767
2958
|
return "updating";
|
2768
2959
|
case "destroy":
|
@@ -2796,47 +2987,44 @@ class RuntimeOperation {
|
|
2796
2987
|
}
|
2797
2988
|
return component.source;
|
2798
2989
|
}
|
2799
|
-
|
2800
|
-
(
|
2801
|
-
|
2802
|
-
|
2803
|
-
|
2804
|
-
|
2805
|
-
|
2806
|
-
|
2807
|
-
|
2808
|
-
|
2809
|
-
|
2810
|
-
|
2811
|
-
|
2812
|
-
|
2813
|
-
|
2814
|
-
|
2815
|
-
persistLogs = funnel(
|
2816
|
-
(entries) => {
|
2817
|
-
this.logger.trace({ msg: "persisting logs", count: entries.length });
|
2818
|
-
void this.stateBackend.appendInstanceLogs(this.operation.id, entries);
|
2819
|
-
},
|
2820
|
-
{
|
2821
|
-
minQuietPeriodMs: 100,
|
2822
|
-
maxBurstDurationMs: 200,
|
2823
|
-
reducer: arrayAccumulator
|
2990
|
+
async refetchState(instanceId) {
|
2991
|
+
this.logger.info({ instanceId }, "refetching state since the lock was not immediately acquired");
|
2992
|
+
const state = await this.stateBackend.getInstanceState(this.operation.projectId, instanceId);
|
2993
|
+
const oldState = this.stateMap.get(instanceId);
|
2994
|
+
if (oldState) {
|
2995
|
+
this.removeStateFromParent(oldState);
|
2996
|
+
this.removeDependentState(oldState);
|
2997
|
+
}
|
2998
|
+
if (state) {
|
2999
|
+
this.stateMap.set(instanceId, state);
|
3000
|
+
this.initialStatusMap.set(instanceId, state.status);
|
3001
|
+
this.tryAddStateToParent(state);
|
3002
|
+
this.addDependentState(state);
|
3003
|
+
} else {
|
3004
|
+
this.stateMap.delete(instanceId);
|
3005
|
+
this.initialStatusMap.delete(instanceId);
|
2824
3006
|
}
|
2825
|
-
|
2826
|
-
|
2827
|
-
(
|
3007
|
+
}
|
3008
|
+
persistStates = createAsyncBatcher(async (states) => {
|
3009
|
+
this.logger.debug({ msg: "persisting states", count: states.length });
|
3010
|
+
await this.stateBackend.putAffectedInstanceStates(
|
3011
|
+
this.operation.projectId,
|
3012
|
+
this.operation.id,
|
3013
|
+
states
|
3014
|
+
);
|
3015
|
+
});
|
3016
|
+
persistLogs = createAsyncBatcher(async (entries) => {
|
3017
|
+
this.logger.trace({ msg: "persisting logs", count: entries.length });
|
3018
|
+
await this.stateBackend.appendInstanceLogs(this.operation.id, entries);
|
3019
|
+
});
|
3020
|
+
persistSecrets = createAsyncBatcher(
|
3021
|
+
async (entries) => {
|
2828
3022
|
this.logger.debug({ msg: "persisting secrets", count: entries.length });
|
2829
3023
|
for (const [instanceId, secrets] of entries) {
|
2830
|
-
|
2831
|
-
|
2832
|
-
|
2833
|
-
});
|
3024
|
+
const existingSecrets = await this.secretBackend.get(this.operation.projectId, instanceId);
|
3025
|
+
Object.assign(existingSecrets, secrets);
|
3026
|
+
await this.secretBackend.set(this.operation.projectId, instanceId, existingSecrets);
|
2834
3027
|
}
|
2835
|
-
},
|
2836
|
-
{
|
2837
|
-
minQuietPeriodMs: 100,
|
2838
|
-
maxBurstDurationMs: 200,
|
2839
|
-
reducer: arrayAccumulator
|
2840
3028
|
}
|
2841
3029
|
);
|
2842
3030
|
static abortMessagePatterns = [
|
@@ -2929,12 +3117,21 @@ class OperationManager {
|
|
2929
3117
|
type: request.type,
|
2930
3118
|
instanceIds: request.instanceIds,
|
2931
3119
|
status: "pending",
|
3120
|
+
options: {
|
3121
|
+
forceUpdateDependencies: request.options?.forceUpdateDependencies ?? false,
|
3122
|
+
destroyDependentInstances: request.options?.destroyDependentInstances ?? true,
|
3123
|
+
invokeDestroyTriggers: request.options?.invokeDestroyTriggers ?? false,
|
3124
|
+
deleteUnreachableResources: request.options?.deleteUnreachableResources ?? false,
|
3125
|
+
refresh: request.options?.refresh ?? false
|
3126
|
+
},
|
2932
3127
|
error: null,
|
2933
3128
|
startedAt: Date.now(),
|
2934
3129
|
completedAt: null
|
2935
3130
|
};
|
3131
|
+
this.logger.info({ operation }, "launching operation");
|
2936
3132
|
await this.stateBackend.putOperation(operation);
|
2937
3133
|
this.startOperation(operation);
|
3134
|
+
return operation;
|
2938
3135
|
}
|
2939
3136
|
/**
|
2940
3137
|
* Cancels the current operation.
|