@elench/testkit 0.1.145 → 0.1.147
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/README.md +12 -0
- package/lib/cli/operations/db/schema/refresh/operation.mjs +11 -1
- package/lib/cli/operations/db/schema/verify/operation.mjs +11 -1
- package/lib/config-api/auth-fixtures.mjs +41 -10
- package/lib/database/admin.mjs +227 -0
- package/lib/database/cleanup.mjs +201 -0
- package/lib/database/constants.mjs +10 -0
- package/lib/database/index.mjs +46 -720
- package/lib/database/local-postgres.mjs +158 -0
- package/lib/database/locks.mjs +31 -0
- package/lib/database/resource-postgres.mjs +72 -0
- package/lib/database/state-files.mjs +53 -0
- package/lib/ownership/docker.mjs +9 -0
- package/lib/runner/default-runtime-runner.mjs +2 -0
- package/lib/runner/lifecycle.mjs +1 -1
- package/lib/runner/maintenance.mjs +3 -0
- package/lib/runner/playwright-runner.mjs +10 -2
- package/lib/runner/scheduler/index.mjs +3 -0
- package/lib/runtime/index.d.ts +8 -0
- package/lib/runtime-src/k6/http.js +9 -2
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/node_modules/es-toolkit/CHANGELOG.md +801 -0
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/package.json +6 -6
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +1 -1
package/lib/database/index.mjs
CHANGED
|
@@ -1,47 +1,27 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { execa } from "execa";
|
|
4
3
|
import {
|
|
5
4
|
computeTemplateFingerprint as computeTemplateFingerprintModel,
|
|
6
5
|
} from "./fingerprint.mjs";
|
|
7
6
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
escapeIdentifier as escapeIdentifierModel,
|
|
13
|
-
escapeSqlLiteral as escapeSqlLiteralModel,
|
|
14
|
-
hashString as hashStringModel,
|
|
15
|
-
limitIdentifier as limitIdentifierModel,
|
|
16
|
-
slugSegment as slugSegmentModel,
|
|
7
|
+
buildDatabaseUrl,
|
|
8
|
+
buildRuntimeDatabaseName,
|
|
9
|
+
buildTemplateDatabaseName,
|
|
10
|
+
hashString,
|
|
17
11
|
} from "./naming.mjs";
|
|
18
12
|
import {
|
|
19
|
-
getLocalInfraDir
|
|
20
|
-
getLocalLocksDir
|
|
21
|
-
getLocalServiceCacheDir
|
|
22
|
-
getResourceLocksDir
|
|
23
|
-
hasRemainingLocalArtifacts
|
|
24
|
-
readStateValue
|
|
25
|
-
visitDirs as visitDirsModel,
|
|
13
|
+
getLocalInfraDir,
|
|
14
|
+
getLocalLocksDir,
|
|
15
|
+
getLocalServiceCacheDir,
|
|
16
|
+
getResourceLocksDir,
|
|
17
|
+
hasRemainingLocalArtifacts,
|
|
18
|
+
readStateValue,
|
|
26
19
|
} from "./state.mjs";
|
|
27
20
|
import {
|
|
28
21
|
runTemplateStage,
|
|
29
22
|
runTemplateStep,
|
|
30
23
|
} from "./template-steps.mjs";
|
|
31
24
|
import { materializeDatabaseConfig } from "../config/database-materialization.mjs";
|
|
32
|
-
import {
|
|
33
|
-
TESTKIT_PRODUCT_DIR_LABEL,
|
|
34
|
-
TESTKIT_PRODUCT_ID_LABEL,
|
|
35
|
-
TESTKIT_RESOURCE_KIND_LABEL,
|
|
36
|
-
TESTKIT_SCOPE_LABEL,
|
|
37
|
-
buildDockerResourceLabels,
|
|
38
|
-
buildProductIdentity,
|
|
39
|
-
dockerContainerSummary,
|
|
40
|
-
dockerLabelArgs,
|
|
41
|
-
listLegacyTestkitPostgresContainers,
|
|
42
|
-
listManagedDockerContainers,
|
|
43
|
-
removeDockerContainer,
|
|
44
|
-
} from "../ownership/docker.mjs";
|
|
45
25
|
import {
|
|
46
26
|
applySourceSchemaCache,
|
|
47
27
|
createSourceSchemaMismatchError,
|
|
@@ -50,17 +30,36 @@ import {
|
|
|
50
30
|
verifyLocalSchemaMatchesSource,
|
|
51
31
|
} from "./schema-source.mjs";
|
|
52
32
|
import { collectStateDirLines } from "../runner/state-io.mjs";
|
|
33
|
+
import {
|
|
34
|
+
cloneDatabaseFromTemplate,
|
|
35
|
+
createEmptyDatabase,
|
|
36
|
+
databaseExists,
|
|
37
|
+
dropDatabaseIfExists,
|
|
38
|
+
waitForResourcePostgresReady,
|
|
39
|
+
} from "./admin.mjs";
|
|
40
|
+
import {
|
|
41
|
+
ensureLocalContainer,
|
|
42
|
+
loadExistingLocalContainer,
|
|
43
|
+
} from "./local-postgres.mjs";
|
|
44
|
+
import {
|
|
45
|
+
resolveResourcePostgresInfra,
|
|
46
|
+
resolveResourcePostgresInfraFromState,
|
|
47
|
+
} from "./resource-postgres.mjs";
|
|
48
|
+
import {
|
|
49
|
+
writeCacheState,
|
|
50
|
+
writeResourceConnectionState,
|
|
51
|
+
} from "./state-files.mjs";
|
|
52
|
+
import { withLock } from "./locks.mjs";
|
|
53
|
+
import {
|
|
54
|
+
cleanupLocalPostgresDockerResources,
|
|
55
|
+
} from "./cleanup.mjs";
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const LOCAL_ADMIN_QUERY_RETRY_MS = 15_000;
|
|
61
|
-
const LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS = 250;
|
|
62
|
-
const LOCAL_DROP_DATABASE_TIMEOUT_MS = 15_000;
|
|
63
|
-
const LOCAL_DROP_DATABASE_POLL_INTERVAL_MS = 250;
|
|
57
|
+
export {
|
|
58
|
+
dropDatabaseWithForceOrDrain,
|
|
59
|
+
isTransientAdminQueryConnectionError,
|
|
60
|
+
waitForDatabaseConnectionsToDrain,
|
|
61
|
+
} from "./admin.mjs";
|
|
62
|
+
export { formatDatabaseResourceCleanupLine } from "./cleanup.mjs";
|
|
64
63
|
|
|
65
64
|
export async function prepareDatabaseRuntime(config, options = {}) {
|
|
66
65
|
const db = materializeDatabaseConfig(
|
|
@@ -112,7 +111,7 @@ export async function destroyServiceDatabaseCache(productDir, serviceName) {
|
|
|
112
111
|
const resourceLockDir = getResourceLocksDir(productDir, resourceName || "unknown");
|
|
113
112
|
fs.mkdirSync(resourceLockDir, { recursive: true });
|
|
114
113
|
await withLock(path.join(resourceLockDir, `template-${serviceName}.lock`), async () => {
|
|
115
|
-
const infra = resolveResourcePostgresInfraFromState(cacheDir);
|
|
114
|
+
const infra = resolveResourcePostgresInfraFromState(cacheDir, readStateValue);
|
|
116
115
|
const templateDbName = readStateValue(path.join(cacheDir, "template_database_name"));
|
|
117
116
|
if (infra && templateDbName) {
|
|
118
117
|
await dropDatabaseIfExists(infra, templateDbName);
|
|
@@ -144,17 +143,11 @@ export async function destroyServiceDatabaseCache(productDir, serviceName) {
|
|
|
144
143
|
export async function cleanupOrphanedLocalInfrastructure(productDir) {
|
|
145
144
|
const infraDir = getLocalInfraDir(productDir);
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (fs.existsSync(infraDir)) {
|
|
150
|
-
const containerName = readStateValue(path.join(infraDir, "container_name"));
|
|
151
|
-
if (containerName) {
|
|
152
|
-
await stopAndRemoveContainer(containerName);
|
|
153
|
-
}
|
|
146
|
+
const resourceCleanup = await cleanupOwnedLocalDatabaseResources(productDir, { stopIdle: true });
|
|
147
|
+
if (!hasRemainingLocalArtifacts(productDir, readStateValue)) {
|
|
154
148
|
fs.rmSync(infraDir, { recursive: true, force: true });
|
|
155
149
|
}
|
|
156
|
-
|
|
157
|
-
await cleanupOwnedLocalDatabaseResources(productDir);
|
|
150
|
+
return resourceCleanup;
|
|
158
151
|
}
|
|
159
152
|
|
|
160
153
|
export async function destroyOwnedLocalDatabaseResources(productDir, options = {}) {
|
|
@@ -171,6 +164,7 @@ export async function cleanupOwnedLocalDatabaseResources(productDir, options = {
|
|
|
171
164
|
force: Boolean(options.force),
|
|
172
165
|
global: false,
|
|
173
166
|
includeLegacy: Boolean(options.includeLegacy),
|
|
167
|
+
stopIdle: options.stopIdle !== false,
|
|
174
168
|
});
|
|
175
169
|
}
|
|
176
170
|
|
|
@@ -181,6 +175,7 @@ export async function cleanupGlobalLocalDatabaseResources(options = {}) {
|
|
|
181
175
|
force: false,
|
|
182
176
|
global: true,
|
|
183
177
|
includeLegacy: Boolean(options.includeLegacy),
|
|
178
|
+
stopIdle: options.stopIdle !== false,
|
|
184
179
|
});
|
|
185
180
|
}
|
|
186
181
|
|
|
@@ -492,680 +487,11 @@ async function destroyResourceRuntimeDatabase(stateDir) {
|
|
|
492
487
|
const dbName = readStateValue(path.join(stateDir, "resource_database_name"));
|
|
493
488
|
if (!dbName) return;
|
|
494
489
|
|
|
495
|
-
const infra = resolveResourcePostgresInfraFromState(stateDir);
|
|
490
|
+
const infra = resolveResourcePostgresInfraFromState(stateDir, readStateValue);
|
|
496
491
|
if (!infra) return;
|
|
497
492
|
await dropDatabaseIfExists(infra, dbName);
|
|
498
493
|
}
|
|
499
494
|
|
|
500
|
-
async function ensureLocalContainer(productDir, database = {}) {
|
|
501
|
-
const infraDir = getLocalInfraDir(productDir);
|
|
502
|
-
fs.mkdirSync(infraDir, { recursive: true });
|
|
503
|
-
|
|
504
|
-
const containerName =
|
|
505
|
-
readStateValue(path.join(infraDir, "container_name")) || buildContainerName(productDir);
|
|
506
|
-
const image = database.image || readStateValue(path.join(infraDir, "image")) || LOCAL_IMAGE;
|
|
507
|
-
const user = database.user || readStateValue(path.join(infraDir, "user")) || LOCAL_USER;
|
|
508
|
-
const password =
|
|
509
|
-
database.password || readStateValue(path.join(infraDir, "password")) || LOCAL_PASSWORD;
|
|
510
|
-
|
|
511
|
-
let inspect = await inspectContainer(containerName);
|
|
512
|
-
if (inspect && inspect.Config?.Image !== image) {
|
|
513
|
-
await stopAndRemoveContainer(containerName);
|
|
514
|
-
inspect = null;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (!inspect) {
|
|
518
|
-
const labels = buildLocalPostgresContainerLabels(productDir, containerName);
|
|
519
|
-
await execa("docker", [
|
|
520
|
-
"run",
|
|
521
|
-
"-d",
|
|
522
|
-
"--name",
|
|
523
|
-
containerName,
|
|
524
|
-
...dockerLabelArgs(labels),
|
|
525
|
-
"-e",
|
|
526
|
-
`POSTGRES_USER=${user}`,
|
|
527
|
-
"-e",
|
|
528
|
-
`POSTGRES_PASSWORD=${password}`,
|
|
529
|
-
"-e",
|
|
530
|
-
`POSTGRES_DB=${LOCAL_ADMIN_DB}`,
|
|
531
|
-
"-p",
|
|
532
|
-
"127.0.0.1::5432",
|
|
533
|
-
image,
|
|
534
|
-
]);
|
|
535
|
-
inspect = await inspectContainer(containerName);
|
|
536
|
-
} else if (!inspect.State?.Running) {
|
|
537
|
-
await execa("docker", ["start", containerName]);
|
|
538
|
-
inspect = await inspectContainer(containerName);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const hostPort = inspect?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort;
|
|
542
|
-
if (!hostPort) {
|
|
543
|
-
throw new Error(`Could not determine published port for local database container ${containerName}`);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const infra = {
|
|
547
|
-
containerName,
|
|
548
|
-
containerId: inspect.Id,
|
|
549
|
-
image,
|
|
550
|
-
user,
|
|
551
|
-
password,
|
|
552
|
-
host: "127.0.0.1",
|
|
553
|
-
port: Number(hostPort),
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
await waitForLocalContainerReady(infra);
|
|
557
|
-
writeLocalInfraState(infraDir, infra);
|
|
558
|
-
return infra;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
async function loadExistingLocalContainer(productDir) {
|
|
562
|
-
const infraDir = getLocalInfraDir(productDir);
|
|
563
|
-
const containerName = readStateValue(path.join(infraDir, "container_name"));
|
|
564
|
-
if (!containerName) return null;
|
|
565
|
-
|
|
566
|
-
const inspect = await inspectContainer(containerName);
|
|
567
|
-
if (!inspect) return null;
|
|
568
|
-
|
|
569
|
-
if (!inspect.State?.Running) {
|
|
570
|
-
await execa("docker", ["start", containerName]);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const nextInspect = await inspectContainer(containerName);
|
|
574
|
-
const hostPort = nextInspect?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort;
|
|
575
|
-
if (!hostPort) return null;
|
|
576
|
-
|
|
577
|
-
const infra = {
|
|
578
|
-
containerName,
|
|
579
|
-
containerId: nextInspect.Id,
|
|
580
|
-
image: nextInspect.Config?.Image || readStateValue(path.join(infraDir, "image")) || LOCAL_IMAGE,
|
|
581
|
-
user: readStateValue(path.join(infraDir, "user")) || LOCAL_USER,
|
|
582
|
-
password: readStateValue(path.join(infraDir, "password")) || LOCAL_PASSWORD,
|
|
583
|
-
host: "127.0.0.1",
|
|
584
|
-
port: Number(hostPort),
|
|
585
|
-
};
|
|
586
|
-
await waitForLocalContainerReady(infra);
|
|
587
|
-
return infra;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async function waitForLocalContainerReady(infra) {
|
|
591
|
-
const startedAt = Date.now();
|
|
592
|
-
while (Date.now() - startedAt < LOCAL_READY_TIMEOUT_MS) {
|
|
593
|
-
try {
|
|
594
|
-
await execa("docker", [
|
|
595
|
-
"exec",
|
|
596
|
-
infra.containerName,
|
|
597
|
-
"pg_isready",
|
|
598
|
-
"-U",
|
|
599
|
-
infra.user,
|
|
600
|
-
"-d",
|
|
601
|
-
LOCAL_ADMIN_DB,
|
|
602
|
-
]);
|
|
603
|
-
return;
|
|
604
|
-
} catch {
|
|
605
|
-
await sleep(LOCAL_POLL_INTERVAL_MS);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
throw new Error(`Timed out waiting for local database container ${infra.containerName}`);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
async function waitForResourcePostgresReady(infra) {
|
|
613
|
-
const startedAt = Date.now();
|
|
614
|
-
while (Date.now() - startedAt < LOCAL_READY_TIMEOUT_MS) {
|
|
615
|
-
try {
|
|
616
|
-
await runAdminQuery(infra, ["-tAc", "SELECT 1"]);
|
|
617
|
-
return;
|
|
618
|
-
} catch (error) {
|
|
619
|
-
if (!isTransientAdminQueryConnectionError(error)) throw error;
|
|
620
|
-
await sleep(LOCAL_POLL_INTERVAL_MS);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
throw new Error(`Timed out waiting for Postgres resource "${infra.resourceName}" at ${infra.host}:${infra.port}`);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
async function inspectContainer(containerName) {
|
|
628
|
-
try {
|
|
629
|
-
const { stdout } = await execa("docker", ["inspect", containerName]);
|
|
630
|
-
const parsed = JSON.parse(stdout);
|
|
631
|
-
return parsed[0] || null;
|
|
632
|
-
} catch {
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
async function stopAndRemoveContainer(containerName) {
|
|
638
|
-
try {
|
|
639
|
-
await execa("docker", ["rm", "-f", containerName]);
|
|
640
|
-
} catch {
|
|
641
|
-
// Already gone.
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async function databaseExists(infra, dbName) {
|
|
646
|
-
const result = await runAdminQuery(infra, [
|
|
647
|
-
"-tAc",
|
|
648
|
-
`SELECT 1 FROM pg_database WHERE datname = '${escapeSqlLiteral(dbName)}'`,
|
|
649
|
-
]);
|
|
650
|
-
return result.trim() === "1";
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
async function createEmptyDatabase(infra, dbName) {
|
|
654
|
-
await runAdminQuery(infra, ["-c", `CREATE DATABASE "${escapeIdentifier(dbName)}"`]);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
async function cloneDatabaseFromTemplate(infra, dbName, templateDbName) {
|
|
658
|
-
await runAdminQuery(infra, [
|
|
659
|
-
"-c",
|
|
660
|
-
`CREATE DATABASE "${escapeIdentifier(dbName)}" TEMPLATE "${escapeIdentifier(templateDbName)}"`,
|
|
661
|
-
]);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
async function dropDatabaseIfExists(infra, dbName) {
|
|
665
|
-
await dropDatabaseWithForceOrDrain(infra, dbName);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
export async function dropDatabaseWithForceOrDrain(infra, dbName, hooks = {}) {
|
|
669
|
-
const runAdminQueryFn = hooks.runAdminQuery || runAdminQuery;
|
|
670
|
-
const databaseExistsFn = hooks.databaseExists || databaseExists;
|
|
671
|
-
const sleepFn = hooks.sleep || sleep;
|
|
672
|
-
|
|
673
|
-
try {
|
|
674
|
-
await runAdminQueryFn(infra, [
|
|
675
|
-
"-c",
|
|
676
|
-
`DROP DATABASE IF EXISTS "${escapeIdentifier(dbName)}" WITH (FORCE)`,
|
|
677
|
-
]);
|
|
678
|
-
return;
|
|
679
|
-
} catch (error) {
|
|
680
|
-
if (!isUnsupportedForceDropError(error)) {
|
|
681
|
-
throw error;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
if (!(await databaseExistsFn(infra, dbName))) {
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
let restoreConnections = false;
|
|
690
|
-
try {
|
|
691
|
-
await runAdminQueryFn(infra, [
|
|
692
|
-
"-c",
|
|
693
|
-
`ALTER DATABASE "${escapeIdentifier(dbName)}" WITH ALLOW_CONNECTIONS false`,
|
|
694
|
-
]);
|
|
695
|
-
restoreConnections = true;
|
|
696
|
-
await waitForDatabaseConnectionsToDrain(infra, dbName, {
|
|
697
|
-
runAdminQuery: runAdminQueryFn,
|
|
698
|
-
sleep: sleepFn,
|
|
699
|
-
});
|
|
700
|
-
await runAdminQueryFn(infra, [
|
|
701
|
-
"-c",
|
|
702
|
-
`DROP DATABASE IF EXISTS "${escapeIdentifier(dbName)}"`,
|
|
703
|
-
]);
|
|
704
|
-
restoreConnections = false;
|
|
705
|
-
} finally {
|
|
706
|
-
if (restoreConnections && (await databaseExistsFn(infra, dbName))) {
|
|
707
|
-
await runAdminQueryFn(infra, [
|
|
708
|
-
"-c",
|
|
709
|
-
`ALTER DATABASE "${escapeIdentifier(dbName)}" WITH ALLOW_CONNECTIONS true`,
|
|
710
|
-
]).catch(() => {
|
|
711
|
-
// Best-effort restoration for failed fallback drops.
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
export async function waitForDatabaseConnectionsToDrain(infra, dbName, hooks = {}) {
|
|
718
|
-
const runAdminQueryFn = hooks.runAdminQuery || runAdminQuery;
|
|
719
|
-
const sleepFn = hooks.sleep || sleep;
|
|
720
|
-
const now = hooks.now || Date.now;
|
|
721
|
-
const timeoutMs = hooks.timeoutMs ?? LOCAL_DROP_DATABASE_TIMEOUT_MS;
|
|
722
|
-
const deadline = now() + timeoutMs;
|
|
723
|
-
|
|
724
|
-
while (true) {
|
|
725
|
-
await terminateDatabaseConnections(infra, dbName, runAdminQueryFn);
|
|
726
|
-
const remainingConnections = await countDatabaseConnections(infra, dbName, runAdminQueryFn);
|
|
727
|
-
if (remainingConnections === 0) {
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
if (now() >= deadline) {
|
|
731
|
-
throw new Error(
|
|
732
|
-
`Timed out waiting for database "${dbName}" connections to close (${remainingConnections} remaining)`
|
|
733
|
-
);
|
|
734
|
-
}
|
|
735
|
-
await sleepFn(LOCAL_DROP_DATABASE_POLL_INTERVAL_MS);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
async function runAdminQuery(infra, args) {
|
|
740
|
-
const command = infra.containerName ? "docker" : "psql";
|
|
741
|
-
const commandArgs = infra.containerName
|
|
742
|
-
? [
|
|
743
|
-
"exec",
|
|
744
|
-
"-e",
|
|
745
|
-
`PGPASSWORD=${infra.password}`,
|
|
746
|
-
infra.containerName,
|
|
747
|
-
"psql",
|
|
748
|
-
"-v",
|
|
749
|
-
"ON_ERROR_STOP=1",
|
|
750
|
-
"-U",
|
|
751
|
-
infra.user,
|
|
752
|
-
"-d",
|
|
753
|
-
infra.adminDatabase || LOCAL_ADMIN_DB,
|
|
754
|
-
...args,
|
|
755
|
-
]
|
|
756
|
-
: [
|
|
757
|
-
"-v",
|
|
758
|
-
"ON_ERROR_STOP=1",
|
|
759
|
-
"-h",
|
|
760
|
-
infra.host,
|
|
761
|
-
"-p",
|
|
762
|
-
String(infra.port),
|
|
763
|
-
"-U",
|
|
764
|
-
infra.user,
|
|
765
|
-
"-d",
|
|
766
|
-
infra.adminDatabase || LOCAL_ADMIN_DB,
|
|
767
|
-
...args,
|
|
768
|
-
];
|
|
769
|
-
const commandOptions = infra.containerName
|
|
770
|
-
? {}
|
|
771
|
-
: {
|
|
772
|
-
env: {
|
|
773
|
-
...process.env,
|
|
774
|
-
PGPASSWORD: infra.password,
|
|
775
|
-
...(infra.sslMode ? { PGSSLMODE: infra.sslMode } : {}),
|
|
776
|
-
},
|
|
777
|
-
};
|
|
778
|
-
const startedAt = Date.now();
|
|
779
|
-
while (true) {
|
|
780
|
-
try {
|
|
781
|
-
const { stdout } = await execa(command, commandArgs, commandOptions);
|
|
782
|
-
return stdout;
|
|
783
|
-
} catch (error) {
|
|
784
|
-
if (
|
|
785
|
-
Date.now() - startedAt >= LOCAL_ADMIN_QUERY_RETRY_MS ||
|
|
786
|
-
!isTransientAdminQueryConnectionError(error)
|
|
787
|
-
) {
|
|
788
|
-
throw error;
|
|
789
|
-
}
|
|
790
|
-
await sleep(LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
export function isTransientAdminQueryConnectionError(error) {
|
|
796
|
-
const text = `${error?.shortMessage || ""}\n${error?.stderr || ""}\n${error?.stdout || ""}\n${error?.message || ""}`;
|
|
797
|
-
return (
|
|
798
|
-
text.includes('connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed') ||
|
|
799
|
-
text.includes("No such file or directory") ||
|
|
800
|
-
text.includes("the database system is starting up") ||
|
|
801
|
-
text.includes("could not connect to server")
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
async function terminateDatabaseConnections(infra, dbName, runAdminQueryFn) {
|
|
806
|
-
await runAdminQueryFn(infra, [
|
|
807
|
-
"-c",
|
|
808
|
-
[
|
|
809
|
-
"SELECT pg_terminate_backend(pid)",
|
|
810
|
-
"FROM pg_stat_activity",
|
|
811
|
-
`WHERE datname = '${escapeSqlLiteral(dbName)}' AND pid <> pg_backend_pid();`,
|
|
812
|
-
].join(" "),
|
|
813
|
-
]);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
async function countDatabaseConnections(infra, dbName, runAdminQueryFn) {
|
|
817
|
-
const result = await runAdminQueryFn(infra, [
|
|
818
|
-
"-tAc",
|
|
819
|
-
[
|
|
820
|
-
"SELECT COUNT(*)",
|
|
821
|
-
"FROM pg_stat_activity",
|
|
822
|
-
`WHERE datname = '${escapeSqlLiteral(dbName)}' AND pid <> pg_backend_pid();`,
|
|
823
|
-
].join(" "),
|
|
824
|
-
]);
|
|
825
|
-
return Number.parseInt(result.trim(), 10) || 0;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function isUnsupportedForceDropError(error) {
|
|
829
|
-
const text = `${error?.shortMessage || ""}\n${error?.stderr || ""}\n${error?.stdout || ""}\n${error?.message || ""}`;
|
|
830
|
-
return (
|
|
831
|
-
text.includes('syntax error at or near "WITH"') ||
|
|
832
|
-
text.includes('option "force"')
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
495
|
async function computeTemplateFingerprint(config, options = {}) {
|
|
837
496
|
return computeTemplateFingerprintModel(config, options);
|
|
838
497
|
}
|
|
839
|
-
|
|
840
|
-
function buildDatabaseUrl(infra, dbName) {
|
|
841
|
-
return buildDatabaseUrlModel(infra, dbName);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function resolveResourcePostgresInfra(config, processEnv = process.env) {
|
|
845
|
-
const resourceName = String(config.testkit?.database?.resource || "").trim();
|
|
846
|
-
if (!resourceName) {
|
|
847
|
-
throw new Error("Resource-backed Postgres database requires database.resource");
|
|
848
|
-
}
|
|
849
|
-
const connections = parseResourceConnections(processEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON);
|
|
850
|
-
const connection = connections[resourceName];
|
|
851
|
-
if (!connection) {
|
|
852
|
-
const available = Object.keys(connections).sort().join(", ") || "none";
|
|
853
|
-
throw new Error(`Postgres resource "${resourceName}" is not available. Available resources: ${available}`);
|
|
854
|
-
}
|
|
855
|
-
return normalizeResourcePostgresConnection(resourceName, connection);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
function resolveResourcePostgresInfraFromState(stateDir, processEnv = process.env) {
|
|
859
|
-
const resourceName = readStateValue(path.join(stateDir, "resource_name"));
|
|
860
|
-
if (!resourceName) return null;
|
|
861
|
-
const connections = parseResourceConnections(processEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON);
|
|
862
|
-
const connection = connections[resourceName] || readResourceConnectionState(stateDir);
|
|
863
|
-
return connection ? normalizeResourcePostgresConnection(resourceName, connection) : null;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function parseResourceConnections(raw) {
|
|
867
|
-
if (!raw) return {};
|
|
868
|
-
try {
|
|
869
|
-
const parsed = JSON.parse(raw);
|
|
870
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
871
|
-
} catch (error) {
|
|
872
|
-
throw new Error(`Invalid TESTKIT_RESOURCE_CONNECTIONS_JSON: ${error.message}`);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function normalizeResourcePostgresConnection(resourceName, connection) {
|
|
877
|
-
const fromUrl = connection.url ? parsePostgresConnectionUrl(connection.url) : {};
|
|
878
|
-
const host = connection.host || fromUrl.host;
|
|
879
|
-
const port = Number(connection.port || fromUrl.port || 5432);
|
|
880
|
-
const user = connection.user || fromUrl.user;
|
|
881
|
-
const password = connection.password || fromUrl.password || "";
|
|
882
|
-
const adminDatabase = connection.adminDatabase || connection.admin_database || fromUrl.database || LOCAL_ADMIN_DB;
|
|
883
|
-
const sslMode = connection.sslMode || connection.sslmode || fromUrl.sslMode || "disable";
|
|
884
|
-
if (!host) throw new Error(`Postgres resource "${resourceName}" connection is missing host`);
|
|
885
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
886
|
-
throw new Error(`Postgres resource "${resourceName}" connection has invalid port`);
|
|
887
|
-
}
|
|
888
|
-
if (!user) throw new Error(`Postgres resource "${resourceName}" connection is missing user`);
|
|
889
|
-
return {
|
|
890
|
-
backend: "resource",
|
|
891
|
-
resourceName,
|
|
892
|
-
host,
|
|
893
|
-
port,
|
|
894
|
-
user,
|
|
895
|
-
password,
|
|
896
|
-
adminDatabase,
|
|
897
|
-
sslMode,
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
function parsePostgresConnectionUrl(rawUrl) {
|
|
902
|
-
const parsed = new URL(rawUrl);
|
|
903
|
-
return {
|
|
904
|
-
host: parsed.hostname,
|
|
905
|
-
port: parsed.port ? Number(parsed.port) : 5432,
|
|
906
|
-
database: decodeURIComponent(parsed.pathname.replace(/^\//, "")) || LOCAL_ADMIN_DB,
|
|
907
|
-
user: decodeURIComponent(parsed.username || ""),
|
|
908
|
-
password: decodeURIComponent(parsed.password || ""),
|
|
909
|
-
sslMode: parsed.searchParams.get("sslmode") || undefined,
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
async function cleanupLocalPostgresDockerResources(options = {}) {
|
|
914
|
-
const product = buildProductIdentity(options.productDir || process.cwd());
|
|
915
|
-
const managed = await listManagedDockerContainers();
|
|
916
|
-
const targets = [];
|
|
917
|
-
const kept = [];
|
|
918
|
-
|
|
919
|
-
for (const container of managed) {
|
|
920
|
-
if (!isManagedLocalPostgresContainer(container)) continue;
|
|
921
|
-
const classification = classifyManagedLocalPostgresContainer(container, product, options);
|
|
922
|
-
if (classification.remove) {
|
|
923
|
-
targets.push({ ...container, reason: classification.reason, legacy: false });
|
|
924
|
-
} else if (classification.reason) {
|
|
925
|
-
kept.push({ ...container, reason: classification.reason, legacy: false });
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
if (options.includeLegacy) {
|
|
930
|
-
const currentLegacyName = buildContainerName(product.dir);
|
|
931
|
-
for (const container of await listLegacyTestkitPostgresContainers()) {
|
|
932
|
-
if (!options.global && container.name !== currentLegacyName) continue;
|
|
933
|
-
targets.push({ ...container, reason: "legacy-unlabelled", legacy: true });
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
if (!options.dryRun) {
|
|
938
|
-
for (const container of targets) {
|
|
939
|
-
await removeDockerContainer(container.name || container.id);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
return {
|
|
944
|
-
removed: options.dryRun ? [] : targets,
|
|
945
|
-
targets,
|
|
946
|
-
kept,
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
function classifyManagedLocalPostgresContainer(container, product, options) {
|
|
951
|
-
const labels = container.labels || {};
|
|
952
|
-
const containerProductId = labels[TESTKIT_PRODUCT_ID_LABEL] || "";
|
|
953
|
-
const containerProductDir = labels[TESTKIT_PRODUCT_DIR_LABEL] || "";
|
|
954
|
-
const currentProduct = containerProductId === product.id;
|
|
955
|
-
|
|
956
|
-
if (options.force && currentProduct) {
|
|
957
|
-
return { remove: true, reason: "destroy-current-product" };
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
if (!options.global && !currentProduct) {
|
|
961
|
-
return { remove: false, reason: "different-product" };
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
if (currentProduct) {
|
|
965
|
-
if (!hasRemainingLocalArtifacts(product.dir)) {
|
|
966
|
-
return { remove: true, reason: "current-product-no-local-artifacts" };
|
|
967
|
-
}
|
|
968
|
-
if (!localArtifactsReferenceContainer(product.dir, container.name)) {
|
|
969
|
-
return { remove: true, reason: "current-product-unreferenced" };
|
|
970
|
-
}
|
|
971
|
-
return { remove: false, reason: "current-product-referenced" };
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
if (options.global && containerProductDir && !fs.existsSync(containerProductDir)) {
|
|
975
|
-
return { remove: true, reason: "product-dir-missing" };
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
if (options.global && containerProductDir && !hasRemainingLocalArtifacts(containerProductDir)) {
|
|
979
|
-
return { remove: true, reason: "product-has-no-local-artifacts" };
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
return { remove: false, reason: options.global ? "other-product-retained" : "different-product" };
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function isManagedLocalPostgresContainer(container) {
|
|
986
|
-
const labels = container.labels || {};
|
|
987
|
-
return (
|
|
988
|
-
labels[TESTKIT_RESOURCE_KIND_LABEL] === "postgres-container" &&
|
|
989
|
-
labels[TESTKIT_SCOPE_LABEL] === "local-postgres"
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function buildLocalPostgresContainerLabels(productDir, containerName) {
|
|
994
|
-
return buildDockerResourceLabels(productDir, {
|
|
995
|
-
kind: "postgres-container",
|
|
996
|
-
name: containerName,
|
|
997
|
-
scope: "local-postgres",
|
|
998
|
-
cachePolicy: "product-cache",
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
function localArtifactsReferenceContainer(productDir, containerName) {
|
|
1003
|
-
if (!containerName) return false;
|
|
1004
|
-
const root = path.join(productDir, ".testkit");
|
|
1005
|
-
let referenced = false;
|
|
1006
|
-
visitDirs(root, (dir) => {
|
|
1007
|
-
if (referenced) return;
|
|
1008
|
-
for (const fileName of ["container_name", "local_container_name"]) {
|
|
1009
|
-
if (readStateValue(path.join(dir, fileName)) === containerName) {
|
|
1010
|
-
referenced = true;
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
});
|
|
1015
|
-
return referenced;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
export function formatDatabaseResourceCleanupLine(entry, dryRun = false) {
|
|
1019
|
-
const action = dryRun ? "Would remove" : "Removed";
|
|
1020
|
-
const legacy = entry.legacy ? " legacy" : "";
|
|
1021
|
-
return `${action}${legacy} database resource ${dockerContainerSummary(entry)} reason=${entry.reason}`;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function buildContainerName(productDir) {
|
|
1025
|
-
return buildContainerNameModel(productDir);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function buildTemplateDatabaseName(serviceName, fingerprint) {
|
|
1029
|
-
return buildTemplateDatabaseNameModel(serviceName, fingerprint);
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
function buildRuntimeDatabaseName(serviceName, bindingKey, fingerprint) {
|
|
1033
|
-
return buildRuntimeDatabaseNameModel(serviceName, bindingKey, fingerprint);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function writeLocalInfraState(infraDir, infra) {
|
|
1037
|
-
const product = buildProductIdentity(path.resolve(infraDir, "..", "..", ".."));
|
|
1038
|
-
fs.writeFileSync(path.join(infraDir, "database_backend"), "local");
|
|
1039
|
-
fs.writeFileSync(path.join(infraDir, "container_name"), infra.containerName);
|
|
1040
|
-
fs.writeFileSync(path.join(infraDir, "container_id"), infra.containerId);
|
|
1041
|
-
fs.writeFileSync(path.join(infraDir, "ownership_product_id"), product.id);
|
|
1042
|
-
fs.writeFileSync(path.join(infraDir, "image"), infra.image);
|
|
1043
|
-
fs.writeFileSync(path.join(infraDir, "user"), infra.user);
|
|
1044
|
-
fs.writeFileSync(path.join(infraDir, "password"), infra.password);
|
|
1045
|
-
fs.writeFileSync(path.join(infraDir, "host"), infra.host);
|
|
1046
|
-
fs.writeFileSync(path.join(infraDir, "port"), String(infra.port));
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
function writeCacheState(cacheDir, config, infra, templateDbName, fingerprint) {
|
|
1050
|
-
const backend = config.testkit.database.provider;
|
|
1051
|
-
fs.writeFileSync(path.join(cacheDir, "database_backend"), backend);
|
|
1052
|
-
fs.writeFileSync(path.join(cacheDir, "template_database_name"), templateDbName);
|
|
1053
|
-
fs.writeFileSync(path.join(cacheDir, "template_fingerprint"), fingerprint);
|
|
1054
|
-
if (backend === "local") {
|
|
1055
|
-
fs.writeFileSync(path.join(cacheDir, "container_name"), infra.containerName);
|
|
1056
|
-
} else {
|
|
1057
|
-
fs.writeFileSync(path.join(cacheDir, "resource_name"), infra.resourceName);
|
|
1058
|
-
writeResourceConnectionState(cacheDir, infra);
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function writeResourceConnectionState(stateDir, infra) {
|
|
1063
|
-
fs.writeFileSync(path.join(stateDir, "resource_host"), infra.host);
|
|
1064
|
-
fs.writeFileSync(path.join(stateDir, "resource_port"), String(infra.port));
|
|
1065
|
-
fs.writeFileSync(path.join(stateDir, "resource_user"), infra.user);
|
|
1066
|
-
fs.writeFileSync(path.join(stateDir, "resource_password"), infra.password);
|
|
1067
|
-
fs.writeFileSync(path.join(stateDir, "resource_admin_database"), infra.adminDatabase || LOCAL_ADMIN_DB);
|
|
1068
|
-
fs.writeFileSync(path.join(stateDir, "resource_sslmode"), infra.sslMode || "disable");
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
function readResourceConnectionState(stateDir) {
|
|
1072
|
-
const host = readStateValue(path.join(stateDir, "resource_host"));
|
|
1073
|
-
const user = readStateValue(path.join(stateDir, "resource_user"));
|
|
1074
|
-
if (!host || !user) return null;
|
|
1075
|
-
return {
|
|
1076
|
-
host,
|
|
1077
|
-
port: Number(readStateValue(path.join(stateDir, "resource_port")) || 5432),
|
|
1078
|
-
user,
|
|
1079
|
-
password: readStateValue(path.join(stateDir, "resource_password")) || "",
|
|
1080
|
-
adminDatabase: readStateValue(path.join(stateDir, "resource_admin_database")) || LOCAL_ADMIN_DB,
|
|
1081
|
-
sslMode: readStateValue(path.join(stateDir, "resource_sslmode")) || "disable",
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function getLocalInfraDir(productDir) {
|
|
1086
|
-
return getLocalInfraDirModel(productDir);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function getLocalLocksDir(productDir) {
|
|
1090
|
-
return getLocalLocksDirModel(productDir);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
function getLocalServiceCacheDir(productDir, serviceName) {
|
|
1094
|
-
return getLocalServiceCacheDirModel(productDir, serviceName);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
function getResourceLocksDir(productDir, resourceName) {
|
|
1098
|
-
return getResourceLocksDirModel(productDir, resourceName);
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
function hasRemainingLocalArtifacts(productDir) {
|
|
1102
|
-
return hasRemainingLocalArtifactsModel(productDir, readStateValue);
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
function visitDirs(root, visitor) {
|
|
1106
|
-
return visitDirsModel(root, visitor);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
async function withLock(lockPath, fn) {
|
|
1110
|
-
const lockDir = `${lockPath}.dir`;
|
|
1111
|
-
const timeoutMs = 60_000;
|
|
1112
|
-
const startedAt = Date.now();
|
|
1113
|
-
|
|
1114
|
-
while (true) {
|
|
1115
|
-
try {
|
|
1116
|
-
fs.mkdirSync(lockDir, { recursive: false });
|
|
1117
|
-
break;
|
|
1118
|
-
} catch (error) {
|
|
1119
|
-
if (error.code !== "EEXIST") throw error;
|
|
1120
|
-
if (Date.now() - startedAt > timeoutMs) {
|
|
1121
|
-
throw new Error(`Timed out waiting for lock ${path.basename(lockPath)}`);
|
|
1122
|
-
}
|
|
1123
|
-
await sleep(200);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
try {
|
|
1128
|
-
return await fn();
|
|
1129
|
-
} finally {
|
|
1130
|
-
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function hashString(value, length = 12) {
|
|
1135
|
-
return hashStringModel(value, length);
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
function slugSegment(value) {
|
|
1139
|
-
return slugSegmentModel(value);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
function limitIdentifier(value, maxLength) {
|
|
1143
|
-
return limitIdentifierModel(value, maxLength);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
function escapeIdentifier(value) {
|
|
1147
|
-
return escapeIdentifierModel(value);
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
function escapeSqlLiteral(value) {
|
|
1151
|
-
return escapeSqlLiteralModel(value);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
function readStateValue(filePath) {
|
|
1155
|
-
return readStateValueModel(filePath);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
function sleep(ms) {
|
|
1159
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function removeLocalRuntimeState(stateDir) {
|
|
1163
|
-
for (const file of [
|
|
1164
|
-
"local_database_name",
|
|
1165
|
-
"local_template_fingerprint",
|
|
1166
|
-
"local_template_database_name",
|
|
1167
|
-
"local_container_name",
|
|
1168
|
-
]) {
|
|
1169
|
-
fs.rmSync(path.join(stateDir, file), { force: true });
|
|
1170
|
-
}
|
|
1171
|
-
}
|