@jeffusion/bungee-core 6.4.1 → 6.5.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/config-storage-worker.js +1206 -989
- package/dist/main.js +6258 -6204
- package/dist/plugins/chatgpt-oauth/index.js +7 -18
- package/dist/plugins/chatgpt-oauth/index.js.map +4 -4
- package/dist/plugins/chatgpt-oauth/ui/AccountsPage.svelte +13 -9
- package/dist/plugins/codex-router/index.js +2 -3
- package/dist/plugins/codex-router/index.js.map +3 -3
- package/dist/plugins/llm-protocol-adapter/control.js +45 -30
- package/dist/plugins/llm-protocol-adapter/control.js.map +4 -4
- package/dist/plugins/llm-protocol-adapter/index.js +53 -31
- package/dist/plugins/llm-protocol-adapter/index.js.map +6 -6
- package/package.json +2 -2
|
@@ -213,7 +213,7 @@ function copyJsonValue(value, path, context) {
|
|
|
213
213
|
context.add("non_json_value", path, "Expected a JSON-compatible value");
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
|
-
function normalizePositions(values, path, context) {
|
|
216
|
+
function normalizePositions(values, path, context, itemPath = (index) => `${path}[${index}]`) {
|
|
217
217
|
const reserved = new Set;
|
|
218
218
|
const seen = new Set;
|
|
219
219
|
for (const value of values) {
|
|
@@ -224,7 +224,7 @@ function normalizePositions(values, path, context) {
|
|
|
224
224
|
return values.map((value, index) => {
|
|
225
225
|
if (isObject(value) && typeof value.position === "number" && Number.isSafeInteger(value.position) && value.position >= 0) {
|
|
226
226
|
if (seen.has(value.position)) {
|
|
227
|
-
context.add("duplicate_position", `${
|
|
227
|
+
context.add("duplicate_position", `${itemPath(index)}.position`, "Position must be unique within its owner");
|
|
228
228
|
}
|
|
229
229
|
seen.add(value.position);
|
|
230
230
|
return value.position;
|
|
@@ -566,8 +566,6 @@ function defaultMenuGroup(id) {
|
|
|
566
566
|
}
|
|
567
567
|
// src/config-storage/gateway-target-validation.ts
|
|
568
568
|
function validateGatewayTargets(config, schemas, context) {
|
|
569
|
-
if (!schemas)
|
|
570
|
-
return;
|
|
571
569
|
const routes = new Map(config.routes.map((route) => [route.id, route]));
|
|
572
570
|
const services = new Map(config.services.map((service) => [service.id, service]));
|
|
573
571
|
const edges = new Map;
|
|
@@ -593,39 +591,52 @@ function validateGatewayTargets(config, schemas, context) {
|
|
|
593
591
|
plugins.forEach((plugin, index) => {
|
|
594
592
|
if (!plugin.enabled)
|
|
595
593
|
return;
|
|
596
|
-
for (const field of schemas
|
|
594
|
+
for (const field of schemas?.get(plugin.name) ?? [])
|
|
597
595
|
visit(plugin.options?.[field.name], field, owner, `${path}[${index}].options.${field.name}`);
|
|
598
596
|
});
|
|
599
597
|
};
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
598
|
+
const references = (values, owner, path) => {
|
|
599
|
+
for (const [index, reference] of (values ?? []).entries()) {
|
|
600
|
+
const target = `service:${reference.service_id}`;
|
|
601
|
+
if (!services.has(reference.service_id))
|
|
602
|
+
context.add("unknown_service", `${path}[${index}].service_id`, "Service does not exist");
|
|
603
|
+
if (target === owner)
|
|
604
|
+
context.add("invalid_value", `${path}[${index}].service_id`, "Service cannot reference itself");
|
|
604
605
|
const refs = edges.get(owner) ?? new Set;
|
|
605
|
-
refs.add(
|
|
606
|
+
refs.add(target);
|
|
606
607
|
edges.set(owner, refs);
|
|
607
608
|
}
|
|
608
|
-
});
|
|
609
|
-
config.services.forEach((service, index) => bindings(service.plugins, `service:${service.id}`, `services[${index}].plugins`));
|
|
610
|
-
const active = new Set, done = new Set;
|
|
611
|
-
const walk = (owner) => {
|
|
612
|
-
if (active.has(owner))
|
|
613
|
-
return false;
|
|
614
|
-
if (done.has(owner))
|
|
615
|
-
return true;
|
|
616
|
-
active.add(owner);
|
|
617
|
-
for (const target of edges.get(owner) ?? [])
|
|
618
|
-
if (!walk(target))
|
|
619
|
-
return false;
|
|
620
|
-
active.delete(owner);
|
|
621
|
-
done.add(owner);
|
|
622
|
-
return true;
|
|
623
609
|
};
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
610
|
+
config.routes.forEach((route, index) => {
|
|
611
|
+
const owner = `route:${route.id}`;
|
|
612
|
+
bindings(route.plugins, owner, `routes[${index}].plugins`);
|
|
613
|
+
references(route.service_references, owner, `routes[${index}].service_references`);
|
|
614
|
+
route.endpoints?.forEach((upstream, i) => bindings(upstream.plugins, owner, `routes[${index}].endpoints[${i}].plugins`));
|
|
615
|
+
});
|
|
616
|
+
config.services.forEach((service, index) => {
|
|
617
|
+
const owner = `service:${service.id}`;
|
|
618
|
+
bindings(service.plugins, owner, `services[${index}].plugins`);
|
|
619
|
+
references(service.service_references, owner, `services[${index}].service_references`);
|
|
620
|
+
service.endpoints.forEach((upstream, i) => bindings(upstream.plugins, owner, `services[${index}].endpoints[${i}].plugins`));
|
|
621
|
+
});
|
|
622
|
+
const incoming = new Map;
|
|
623
|
+
for (const [owner, targets] of edges) {
|
|
624
|
+
if (!incoming.has(owner))
|
|
625
|
+
incoming.set(owner, 0);
|
|
626
|
+
for (const target of targets)
|
|
627
|
+
incoming.set(target, (incoming.get(target) ?? 0) + 1);
|
|
628
|
+
}
|
|
629
|
+
const ready = [...incoming].filter(([, count]) => count === 0).map(([owner]) => owner);
|
|
630
|
+
for (let index = 0;index < ready.length; index++) {
|
|
631
|
+
for (const target of edges.get(ready[index]) ?? []) {
|
|
632
|
+
const count = incoming.get(target) - 1;
|
|
633
|
+
incoming.set(target, count);
|
|
634
|
+
if (count === 0)
|
|
635
|
+
ready.push(target);
|
|
628
636
|
}
|
|
637
|
+
}
|
|
638
|
+
if (ready.length !== incoming.size)
|
|
639
|
+
context.add("invalid_value", "routes", "Internal dispatch cycle");
|
|
629
640
|
}
|
|
630
641
|
|
|
631
642
|
// ../../node_modules/acorn/dist/acorn.mjs
|
|
@@ -6458,6 +6469,131 @@ function validateDirectionalModificationRules(object, path, context) {
|
|
|
6458
6469
|
}
|
|
6459
6470
|
}
|
|
6460
6471
|
|
|
6472
|
+
// src/config-storage/service-policy-validation.ts
|
|
6473
|
+
var HEALTH_FIELDS = [
|
|
6474
|
+
"enabled",
|
|
6475
|
+
"interval_ms",
|
|
6476
|
+
"timeout_ms",
|
|
6477
|
+
"path",
|
|
6478
|
+
"method",
|
|
6479
|
+
"expected_status",
|
|
6480
|
+
"unhealthy_threshold",
|
|
6481
|
+
"healthy_threshold",
|
|
6482
|
+
"body",
|
|
6483
|
+
"content_type",
|
|
6484
|
+
"headers",
|
|
6485
|
+
"query",
|
|
6486
|
+
"auto_enable_on_active_health_check"
|
|
6487
|
+
];
|
|
6488
|
+
var FAILOVER_FIELDS = ["enabled", "retry_on", "retry_on_response", "passive_health", "recovery", "slow_start"];
|
|
6489
|
+
function validateHealth(value, path, context) {
|
|
6490
|
+
const object = objectField(value, path, HEALTH_FIELDS, context);
|
|
6491
|
+
if (!object)
|
|
6492
|
+
return;
|
|
6493
|
+
booleanField(object, "enabled", path, context);
|
|
6494
|
+
for (const field of ["interval_ms", "timeout_ms", "unhealthy_threshold", "healthy_threshold"]) {
|
|
6495
|
+
numberField(object, field, path, context, { positive: true });
|
|
6496
|
+
}
|
|
6497
|
+
for (const field of ["path", "method", "body", "content_type"])
|
|
6498
|
+
optionalString(object, field, path, context);
|
|
6499
|
+
if (object.expected_status !== undefined) {
|
|
6500
|
+
if (!Array.isArray(object.expected_status))
|
|
6501
|
+
context.add("invalid_type", `${path}.expected_status`, "Expected an array");
|
|
6502
|
+
else
|
|
6503
|
+
object.expected_status.forEach((item, index) => {
|
|
6504
|
+
validateStatus(item, `${path}.expected_status[${index}]`, context);
|
|
6505
|
+
});
|
|
6506
|
+
}
|
|
6507
|
+
if (object.headers !== undefined)
|
|
6508
|
+
stringRecord(object.headers, `${path}.headers`, context);
|
|
6509
|
+
if (object.query !== undefined)
|
|
6510
|
+
stringRecord(object.query, `${path}.query`, context);
|
|
6511
|
+
optionalBoolean(object, "auto_enable_on_active_health_check", path, context);
|
|
6512
|
+
}
|
|
6513
|
+
function validateStatus(value, path, context) {
|
|
6514
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 100 || value > 599) {
|
|
6515
|
+
context.add("invalid_value", path, "Expected an HTTP status");
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
function validateRetryOn(value, path, context) {
|
|
6519
|
+
const valid = (item) => typeof item === "string" || typeof item === "number" && Number.isFinite(item);
|
|
6520
|
+
if (Array.isArray(value)) {
|
|
6521
|
+
value.forEach((item, index) => {
|
|
6522
|
+
if (!valid(item))
|
|
6523
|
+
context.add("invalid_type", `${path}[${index}]`, "Expected a string or number");
|
|
6524
|
+
});
|
|
6525
|
+
} else if (!valid(value))
|
|
6526
|
+
context.add("invalid_type", path, "Expected a string, number, or array");
|
|
6527
|
+
}
|
|
6528
|
+
function validatePositiveObject(value, path, fields, context) {
|
|
6529
|
+
const object = objectField(value, path, fields, context);
|
|
6530
|
+
if (object)
|
|
6531
|
+
fields.forEach((field) => {
|
|
6532
|
+
numberField(object, field, path, context, { positive: true });
|
|
6533
|
+
});
|
|
6534
|
+
return object;
|
|
6535
|
+
}
|
|
6536
|
+
function validateFailover(value, path, context) {
|
|
6537
|
+
const object = objectField(value, path, FAILOVER_FIELDS, context);
|
|
6538
|
+
if (!object)
|
|
6539
|
+
return;
|
|
6540
|
+
booleanField(object, "enabled", path, context);
|
|
6541
|
+
if (object.retry_on !== undefined)
|
|
6542
|
+
validateRetryOn(object.retry_on, `${path}.retry_on`, context);
|
|
6543
|
+
if (object.retry_on_response !== undefined)
|
|
6544
|
+
stringArray(object.retry_on_response, `${path}.retry_on_response`, context);
|
|
6545
|
+
validatePositiveObject(object.passive_health, `${path}.passive_health`, ["consecutive_failures", "healthy_successes", "auto_disable_threshold"], context);
|
|
6546
|
+
validatePositiveObject(object.recovery, `${path}.recovery`, ["backoff_base_ms", "probe_timeout_ms"], context);
|
|
6547
|
+
const slow = objectField(object.slow_start, `${path}.slow_start`, ["enabled", "duration_ms", "initial_weight_factor"], context);
|
|
6548
|
+
if (!slow)
|
|
6549
|
+
return;
|
|
6550
|
+
booleanField(slow, "enabled", `${path}.slow_start`, context);
|
|
6551
|
+
numberField(slow, "duration_ms", `${path}.slow_start`, context, { positive: true });
|
|
6552
|
+
const factor = slow.initial_weight_factor;
|
|
6553
|
+
if (factor !== undefined && (typeof factor !== "number" || !Number.isFinite(factor) || factor <= 0 || factor > 1)) {
|
|
6554
|
+
context.add("invalid_value", `${path}.slow_start.initial_weight_factor`, "Expected a finite number in (0, 1]");
|
|
6555
|
+
}
|
|
6556
|
+
}
|
|
6557
|
+
function validateLoadBalancing(value, path, context) {
|
|
6558
|
+
const object = objectField(value, path, ["policy", "hash_policy"], context);
|
|
6559
|
+
if (!object)
|
|
6560
|
+
return;
|
|
6561
|
+
const policies = new Set(["weighted_random", "round_robin", "least_requests", "consistent_hash"]);
|
|
6562
|
+
if (typeof object.policy !== "string" || !policies.has(object.policy)) {
|
|
6563
|
+
context.add("invalid_load_balancing", `${path}.policy`, "Unsupported load balancing policy");
|
|
6564
|
+
}
|
|
6565
|
+
const hash = objectField(object.hash_policy, `${path}.hash_policy`, ["header", "expression"], context);
|
|
6566
|
+
if (hash) {
|
|
6567
|
+
optionalString(hash, "header", `${path}.hash_policy`, context);
|
|
6568
|
+
validateExpressionSyntax(hash.expression, `${path}.hash_policy.expression`, context);
|
|
6569
|
+
}
|
|
6570
|
+
if (object.policy === "consistent_hash") {
|
|
6571
|
+
const header = typeof hash?.header === "string" ? hash.header.trim() : "";
|
|
6572
|
+
const expression = typeof hash?.expression === "string" ? hash.expression.trim() : "";
|
|
6573
|
+
const headerShapeValid = hash?.header === undefined || typeof hash.header === "string";
|
|
6574
|
+
const expressionShapeValid = hash?.expression === undefined || typeof hash.expression === "string";
|
|
6575
|
+
if (!header && !expression && headerShapeValid && expressionShapeValid) {
|
|
6576
|
+
context.add("invalid_load_balancing", `${path}.hash_policy`, "Hash input is required");
|
|
6577
|
+
}
|
|
6578
|
+
}
|
|
6579
|
+
}
|
|
6580
|
+
function validateServicePolicies(object, path, context) {
|
|
6581
|
+
validateHealth(object.health_check, `${path}.health_check`, context);
|
|
6582
|
+
validateFailover(object.failover, `${path}.failover`, context);
|
|
6583
|
+
validateLoadBalancing(object.load_balancing, `${path}.load_balancing`, context);
|
|
6584
|
+
}
|
|
6585
|
+
function validateServiceSelectionPolicies(object, path, context) {
|
|
6586
|
+
validateLoadBalancing(object.load_balancing, `${path}.load_balancing`, context);
|
|
6587
|
+
const failover = objectField(object.failover, `${path}.failover`, ["enabled", "retry_on", "retry_on_response"], context);
|
|
6588
|
+
if (!failover)
|
|
6589
|
+
return;
|
|
6590
|
+
booleanField(failover, "enabled", `${path}.failover`, context);
|
|
6591
|
+
if (failover.retry_on !== undefined)
|
|
6592
|
+
validateRetryOn(failover.retry_on, `${path}.failover.retry_on`, context);
|
|
6593
|
+
if (failover.retry_on_response !== undefined)
|
|
6594
|
+
stringArray(failover.retry_on_response, `${path}.failover.retry_on_response`, context);
|
|
6595
|
+
}
|
|
6596
|
+
|
|
6461
6597
|
// src/config-storage/route-policy-validation.ts
|
|
6462
6598
|
function validatePathRewrite(value, path, context) {
|
|
6463
6599
|
if (value === undefined)
|
|
@@ -6611,6 +6747,7 @@ function validateRetry(value, path, context) {
|
|
|
6611
6747
|
numberField(object, "per_retry_timeout_ms", path, context, { positive: true });
|
|
6612
6748
|
}
|
|
6613
6749
|
function validateRoutePolicies(object, path, context) {
|
|
6750
|
+
validateServiceSelectionPolicies(object, path, context);
|
|
6614
6751
|
validatePathRewrite(object.path_rewrite, `${path}.path_rewrite`, context);
|
|
6615
6752
|
validateAuth(object.auth, `${path}.auth`, context);
|
|
6616
6753
|
const websocket = objectField(object.websocket, `${path}.websocket`, ["enabled"], context);
|
|
@@ -6644,120 +6781,6 @@ function validateRoutePolicies(object, path, context) {
|
|
|
6644
6781
|
validateRetry(object.retry, `${path}.retry`, context);
|
|
6645
6782
|
}
|
|
6646
6783
|
|
|
6647
|
-
// src/config-storage/service-policy-validation.ts
|
|
6648
|
-
var HEALTH_FIELDS = [
|
|
6649
|
-
"enabled",
|
|
6650
|
-
"interval_ms",
|
|
6651
|
-
"timeout_ms",
|
|
6652
|
-
"path",
|
|
6653
|
-
"method",
|
|
6654
|
-
"expected_status",
|
|
6655
|
-
"unhealthy_threshold",
|
|
6656
|
-
"healthy_threshold",
|
|
6657
|
-
"body",
|
|
6658
|
-
"content_type",
|
|
6659
|
-
"headers",
|
|
6660
|
-
"query",
|
|
6661
|
-
"auto_enable_on_active_health_check"
|
|
6662
|
-
];
|
|
6663
|
-
var FAILOVER_FIELDS = ["enabled", "retry_on", "retry_on_response", "passive_health", "recovery", "slow_start"];
|
|
6664
|
-
function validateHealth(value, path, context) {
|
|
6665
|
-
const object = objectField(value, path, HEALTH_FIELDS, context);
|
|
6666
|
-
if (!object)
|
|
6667
|
-
return;
|
|
6668
|
-
booleanField(object, "enabled", path, context);
|
|
6669
|
-
for (const field of ["interval_ms", "timeout_ms", "unhealthy_threshold", "healthy_threshold"]) {
|
|
6670
|
-
numberField(object, field, path, context, { positive: true });
|
|
6671
|
-
}
|
|
6672
|
-
for (const field of ["path", "method", "body", "content_type"])
|
|
6673
|
-
optionalString(object, field, path, context);
|
|
6674
|
-
if (object.expected_status !== undefined) {
|
|
6675
|
-
if (!Array.isArray(object.expected_status))
|
|
6676
|
-
context.add("invalid_type", `${path}.expected_status`, "Expected an array");
|
|
6677
|
-
else
|
|
6678
|
-
object.expected_status.forEach((item, index) => {
|
|
6679
|
-
validateStatus(item, `${path}.expected_status[${index}]`, context);
|
|
6680
|
-
});
|
|
6681
|
-
}
|
|
6682
|
-
if (object.headers !== undefined)
|
|
6683
|
-
stringRecord(object.headers, `${path}.headers`, context);
|
|
6684
|
-
if (object.query !== undefined)
|
|
6685
|
-
stringRecord(object.query, `${path}.query`, context);
|
|
6686
|
-
optionalBoolean(object, "auto_enable_on_active_health_check", path, context);
|
|
6687
|
-
}
|
|
6688
|
-
function validateStatus(value, path, context) {
|
|
6689
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 100 || value > 599) {
|
|
6690
|
-
context.add("invalid_value", path, "Expected an HTTP status");
|
|
6691
|
-
}
|
|
6692
|
-
}
|
|
6693
|
-
function validateRetryOn(value, path, context) {
|
|
6694
|
-
const valid = (item) => typeof item === "string" || typeof item === "number" && Number.isFinite(item);
|
|
6695
|
-
if (Array.isArray(value)) {
|
|
6696
|
-
value.forEach((item, index) => {
|
|
6697
|
-
if (!valid(item))
|
|
6698
|
-
context.add("invalid_type", `${path}[${index}]`, "Expected a string or number");
|
|
6699
|
-
});
|
|
6700
|
-
} else if (!valid(value))
|
|
6701
|
-
context.add("invalid_type", path, "Expected a string, number, or array");
|
|
6702
|
-
}
|
|
6703
|
-
function validatePositiveObject(value, path, fields, context) {
|
|
6704
|
-
const object = objectField(value, path, fields, context);
|
|
6705
|
-
if (object)
|
|
6706
|
-
fields.forEach((field) => {
|
|
6707
|
-
numberField(object, field, path, context, { positive: true });
|
|
6708
|
-
});
|
|
6709
|
-
return object;
|
|
6710
|
-
}
|
|
6711
|
-
function validateFailover(value, path, context) {
|
|
6712
|
-
const object = objectField(value, path, FAILOVER_FIELDS, context);
|
|
6713
|
-
if (!object)
|
|
6714
|
-
return;
|
|
6715
|
-
booleanField(object, "enabled", path, context);
|
|
6716
|
-
if (object.retry_on !== undefined)
|
|
6717
|
-
validateRetryOn(object.retry_on, `${path}.retry_on`, context);
|
|
6718
|
-
if (object.retry_on_response !== undefined)
|
|
6719
|
-
stringArray(object.retry_on_response, `${path}.retry_on_response`, context);
|
|
6720
|
-
validatePositiveObject(object.passive_health, `${path}.passive_health`, ["consecutive_failures", "healthy_successes", "auto_disable_threshold"], context);
|
|
6721
|
-
validatePositiveObject(object.recovery, `${path}.recovery`, ["backoff_base_ms", "probe_timeout_ms"], context);
|
|
6722
|
-
const slow = objectField(object.slow_start, `${path}.slow_start`, ["enabled", "duration_ms", "initial_weight_factor"], context);
|
|
6723
|
-
if (!slow)
|
|
6724
|
-
return;
|
|
6725
|
-
booleanField(slow, "enabled", `${path}.slow_start`, context);
|
|
6726
|
-
numberField(slow, "duration_ms", `${path}.slow_start`, context, { positive: true });
|
|
6727
|
-
const factor = slow.initial_weight_factor;
|
|
6728
|
-
if (factor !== undefined && (typeof factor !== "number" || !Number.isFinite(factor) || factor <= 0 || factor > 1)) {
|
|
6729
|
-
context.add("invalid_value", `${path}.slow_start.initial_weight_factor`, "Expected a finite number in (0, 1]");
|
|
6730
|
-
}
|
|
6731
|
-
}
|
|
6732
|
-
function validateLoadBalancing(value, path, context) {
|
|
6733
|
-
const object = objectField(value, path, ["policy", "hash_policy"], context);
|
|
6734
|
-
if (!object)
|
|
6735
|
-
return;
|
|
6736
|
-
const policies = new Set(["weighted_random", "round_robin", "least_requests", "consistent_hash"]);
|
|
6737
|
-
if (typeof object.policy !== "string" || !policies.has(object.policy)) {
|
|
6738
|
-
context.add("invalid_load_balancing", `${path}.policy`, "Unsupported load balancing policy");
|
|
6739
|
-
}
|
|
6740
|
-
const hash = objectField(object.hash_policy, `${path}.hash_policy`, ["header", "expression"], context);
|
|
6741
|
-
if (hash) {
|
|
6742
|
-
optionalString(hash, "header", `${path}.hash_policy`, context);
|
|
6743
|
-
validateExpressionSyntax(hash.expression, `${path}.hash_policy.expression`, context);
|
|
6744
|
-
}
|
|
6745
|
-
if (object.policy === "consistent_hash") {
|
|
6746
|
-
const header = typeof hash?.header === "string" ? hash.header.trim() : "";
|
|
6747
|
-
const expression = typeof hash?.expression === "string" ? hash.expression.trim() : "";
|
|
6748
|
-
const headerShapeValid = hash?.header === undefined || typeof hash.header === "string";
|
|
6749
|
-
const expressionShapeValid = hash?.expression === undefined || typeof hash.expression === "string";
|
|
6750
|
-
if (!header && !expression && headerShapeValid && expressionShapeValid) {
|
|
6751
|
-
context.add("invalid_load_balancing", `${path}.hash_policy`, "Hash input is required");
|
|
6752
|
-
}
|
|
6753
|
-
}
|
|
6754
|
-
}
|
|
6755
|
-
function validateServicePolicies(object, path, context) {
|
|
6756
|
-
validateHealth(object.health_check, `${path}.health_check`, context);
|
|
6757
|
-
validateFailover(object.failover, `${path}.failover`, context);
|
|
6758
|
-
validateLoadBalancing(object.load_balancing, `${path}.load_balancing`, context);
|
|
6759
|
-
}
|
|
6760
|
-
|
|
6761
6784
|
// src/config-storage/domain-validation.ts
|
|
6762
6785
|
var AUTH_FIELDS = ["enabled", "tokens"];
|
|
6763
6786
|
function validateAuth(value, path, context) {
|
|
@@ -6811,7 +6834,7 @@ function validateUpstreamDomain(object, path, context) {
|
|
|
6811
6834
|
}
|
|
6812
6835
|
function validateServiceDomain(object, path, endpointCount, context) {
|
|
6813
6836
|
if (endpointCount === 0)
|
|
6814
|
-
context.add("empty_service", `${path}.endpoints`, "Service requires at least one upstream");
|
|
6837
|
+
context.add("empty_service", `${path}.endpoints`, "Service requires at least one upstream or service reference");
|
|
6815
6838
|
validateServicePolicies(object, path, context);
|
|
6816
6839
|
}
|
|
6817
6840
|
function validateRouteDomain(object, path, hasService, endpointCount, context) {
|
|
@@ -7234,7 +7257,9 @@ var ROUTE_KEYS = [
|
|
|
7234
7257
|
"direct_response",
|
|
7235
7258
|
"redirect",
|
|
7236
7259
|
"retry",
|
|
7237
|
-
"websocket"
|
|
7260
|
+
"websocket",
|
|
7261
|
+
"load_balancing",
|
|
7262
|
+
"failover"
|
|
7238
7263
|
];
|
|
7239
7264
|
var UPSTREAM_KEYS = ["request", "response", "description", "condition"];
|
|
7240
7265
|
var ENTITY_KEYS = ["id", "position", "plugins"];
|
|
@@ -7243,8 +7268,8 @@ function allowed(...groups) {
|
|
|
7243
7268
|
}
|
|
7244
7269
|
var ROOT_FIELDS = allowed(GLOBAL_KEYS, ["services", "routes", "plugins"]);
|
|
7245
7270
|
var PLUGIN_FIELDS = allowed(ENTITY_KEYS, ["name", "options", "enabled", "path"]);
|
|
7246
|
-
var SERVICE_FIELDS = allowed(ENTITY_KEYS, SERVICE_KEYS, ["name", "endpoints"]);
|
|
7247
|
-
var ROUTE_FIELDS = allowed(ENTITY_KEYS, ROUTE_KEYS, ["path", "
|
|
7271
|
+
var SERVICE_FIELDS = allowed(ENTITY_KEYS, SERVICE_KEYS, ["name", "endpoints", "service_references"]);
|
|
7272
|
+
var ROUTE_FIELDS = allowed(ENTITY_KEYS, ROUTE_KEYS, ["path", "service_references", "endpoints"]);
|
|
7248
7273
|
var UPSTREAM_FIELDS = allowed(ENTITY_KEYS, UPSTREAM_KEYS, [
|
|
7249
7274
|
"target",
|
|
7250
7275
|
"weight",
|
|
@@ -7339,10 +7364,10 @@ function booleanDefault(object, key, fallback, path, context) {
|
|
|
7339
7364
|
context.add("invalid_type", `${path}.${key}`, "Expected a boolean");
|
|
7340
7365
|
return fallback;
|
|
7341
7366
|
}
|
|
7342
|
-
function parseUpstreams(value, path, catalog, context, present = false) {
|
|
7367
|
+
function parseUpstreams(value, path, catalog, context, present = false, sharedPositions) {
|
|
7343
7368
|
const upstreams = [];
|
|
7344
7369
|
const candidates = context.array(value, path, present);
|
|
7345
|
-
const positions = normalizePositions(candidates, path, context);
|
|
7370
|
+
const positions = sharedPositions ?? normalizePositions(candidates, path, context);
|
|
7346
7371
|
candidates.forEach((candidate, index) => {
|
|
7347
7372
|
const itemPath = `${path}[${index}]`;
|
|
7348
7373
|
const object = context.object(candidate, itemPath);
|
|
@@ -7370,13 +7395,44 @@ function parseUpstreams(value, path, catalog, context, present = false) {
|
|
|
7370
7395
|
});
|
|
7371
7396
|
return upstreams.sort((left, right) => left.position - right.position);
|
|
7372
7397
|
}
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
const
|
|
7376
|
-
const
|
|
7377
|
-
const
|
|
7378
|
-
candidates.
|
|
7379
|
-
const
|
|
7398
|
+
var REFERENCE_FIELDS = new Set(["id", "position", "service_id", "weight", "priority", "is_disabled"]);
|
|
7399
|
+
function parseReferences(value, path, context, positions, present = false) {
|
|
7400
|
+
const candidates = context.array(value, path, present);
|
|
7401
|
+
const normalized = positions ?? normalizePositions(candidates, path, context);
|
|
7402
|
+
const targets = new Set;
|
|
7403
|
+
return candidates.flatMap((candidate, index) => {
|
|
7404
|
+
const itemPath = `${path}[${index}]`;
|
|
7405
|
+
const object = context.object(candidate, itemPath);
|
|
7406
|
+
if (!object)
|
|
7407
|
+
return [];
|
|
7408
|
+
rejectUnknownFields(object, REFERENCE_FIELDS, itemPath, context);
|
|
7409
|
+
const serviceId = context.string(object, "service_id", `${itemPath}.service_id`);
|
|
7410
|
+
if (!isLowercaseUuid(serviceId))
|
|
7411
|
+
context.add("invalid_uuid", `${itemPath}.service_id`, "Expected a lowercase UUID");
|
|
7412
|
+
if (targets.has(serviceId))
|
|
7413
|
+
context.add("invalid_value", `${itemPath}.service_id`, "Service reference must be unique within its owner");
|
|
7414
|
+
targets.add(serviceId);
|
|
7415
|
+
for (const key of ["weight", "priority"])
|
|
7416
|
+
if (typeof object[key] === "number" && object[key] <= 0) {
|
|
7417
|
+
context.add("invalid_positive_number", `${itemPath}.${key}`, "Expected a positive number");
|
|
7418
|
+
}
|
|
7419
|
+
return [{
|
|
7420
|
+
id: context.id(object, `${itemPath}.id`),
|
|
7421
|
+
position: context.position(object, normalized[index], `${itemPath}.position`),
|
|
7422
|
+
service_id: serviceId,
|
|
7423
|
+
weight: numberDefault(object, "weight", 100, itemPath, context),
|
|
7424
|
+
priority: numberDefault(object, "priority", 1, itemPath, context),
|
|
7425
|
+
is_disabled: booleanDefault(object, "is_disabled", false, itemPath, context)
|
|
7426
|
+
}];
|
|
7427
|
+
}).sort((left, right) => left.position - right.position);
|
|
7428
|
+
}
|
|
7429
|
+
function parseServices(value, catalog, context) {
|
|
7430
|
+
const services = [];
|
|
7431
|
+
const names = new Set;
|
|
7432
|
+
const candidates = context.array(value, "services", value !== undefined);
|
|
7433
|
+
const positions = normalizePositions(candidates, "services", context);
|
|
7434
|
+
candidates.forEach((candidate, index) => {
|
|
7435
|
+
const path = `services[${index}]`;
|
|
7380
7436
|
const object = context.object(candidate, path);
|
|
7381
7437
|
if (!object)
|
|
7382
7438
|
return;
|
|
@@ -7387,7 +7443,11 @@ function parseServices(value, catalog, context) {
|
|
|
7387
7443
|
context.add("duplicate_name", `${path}.name`, "Service name must be unique");
|
|
7388
7444
|
else
|
|
7389
7445
|
names.add(name);
|
|
7390
|
-
const
|
|
7446
|
+
const endpointCandidates = Array.isArray(object.endpoints) ? object.endpoints : [];
|
|
7447
|
+
const referenceCandidates = Array.isArray(object.service_references) ? object.service_references : [];
|
|
7448
|
+
const sharedPositions = normalizePositions([...endpointCandidates, ...referenceCandidates], path, context, (index) => index < endpointCandidates.length ? `${path}.endpoints[${index}]` : `${path}.service_references[${index - endpointCandidates.length}]`);
|
|
7449
|
+
const references = parseReferences(object.service_references, `${path}.service_references`, context, sharedPositions.slice(endpointCandidates.length), "service_references" in object);
|
|
7450
|
+
const endpointCount = endpointCandidates.length + references.length;
|
|
7391
7451
|
const policies = copyKnown(object, SERVICE_KEYS, path, context);
|
|
7392
7452
|
validateServiceDomain(object, path, endpointCount, context);
|
|
7393
7453
|
services.push({
|
|
@@ -7395,13 +7455,14 @@ function parseServices(value, catalog, context) {
|
|
|
7395
7455
|
id,
|
|
7396
7456
|
position: context.position(object, positions[index], `${path}.position`),
|
|
7397
7457
|
name,
|
|
7398
|
-
endpoints: parseUpstreams(object.endpoints, `${path}.endpoints`, catalog, context, "endpoints" in object),
|
|
7458
|
+
endpoints: parseUpstreams(object.endpoints, `${path}.endpoints`, catalog, context, "endpoints" in object, sharedPositions),
|
|
7459
|
+
...references.length ? { service_references: references } : {},
|
|
7399
7460
|
plugins: parsePlugins(object.plugins, `${path}.plugins`, catalog, context, "plugins" in object)
|
|
7400
7461
|
});
|
|
7401
7462
|
});
|
|
7402
7463
|
return services.sort((left, right) => left.position - right.position);
|
|
7403
7464
|
}
|
|
7404
|
-
function parseRoutes(value,
|
|
7465
|
+
function parseRoutes(value, catalog, context) {
|
|
7405
7466
|
const routes = [];
|
|
7406
7467
|
const paths = new Set;
|
|
7407
7468
|
const candidates = context.array(value, "routes", value !== undefined);
|
|
@@ -7418,14 +7479,13 @@ function parseRoutes(value, serviceIds, catalog, context) {
|
|
|
7418
7479
|
context.add("duplicate_path", `${itemPath}.path`, "Route path must be unique");
|
|
7419
7480
|
else
|
|
7420
7481
|
paths.add(path);
|
|
7421
|
-
const
|
|
7422
|
-
const hasService =
|
|
7482
|
+
const references = parseReferences(object.service_references, `${itemPath}.service_references`, context, undefined, "service_references" in object);
|
|
7483
|
+
const hasService = references.length > 0;
|
|
7484
|
+
if ("service_references" in object && !hasService)
|
|
7485
|
+
context.add("invalid_value", `${itemPath}.service_references`, "Service route requires at least one reference");
|
|
7423
7486
|
const endpointCount = Array.isArray(object.endpoints) ? object.endpoints.length : 0;
|
|
7424
7487
|
const policies = copyKnown(object, ROUTE_KEYS, itemPath, context);
|
|
7425
7488
|
validateRouteDomain(object, itemPath, hasService, endpointCount, context);
|
|
7426
|
-
validateServiceReference(serviceId, itemPath, serviceIds, context);
|
|
7427
|
-
if ("service" in object)
|
|
7428
|
-
context.add("invalid_value", `${itemPath}.service`, "Use service_id");
|
|
7429
7489
|
if (hasService && object.endpoints !== undefined) {
|
|
7430
7490
|
context.add("conflicting_owner", `${itemPath}.endpoints`, "A service route cannot own upstreams");
|
|
7431
7491
|
}
|
|
@@ -7437,22 +7497,12 @@ function parseRoutes(value, serviceIds, catalog, context) {
|
|
|
7437
7497
|
plugins: parsePlugins(object.plugins, `${itemPath}.plugins`, catalog, context, "plugins" in object)
|
|
7438
7498
|
};
|
|
7439
7499
|
if (hasService)
|
|
7440
|
-
routes.push({ ...base,
|
|
7500
|
+
routes.push({ ...base, service_references: references });
|
|
7441
7501
|
else
|
|
7442
7502
|
routes.push({ ...base, endpoints: parseUpstreams(object.endpoints, `${itemPath}.endpoints`, catalog, context, "endpoints" in object) });
|
|
7443
7503
|
});
|
|
7444
7504
|
return routes.sort((left, right) => left.position - right.position);
|
|
7445
7505
|
}
|
|
7446
|
-
function validateServiceReference(value, path, serviceIds, context) {
|
|
7447
|
-
if (value === undefined)
|
|
7448
|
-
return;
|
|
7449
|
-
if (typeof value !== "string")
|
|
7450
|
-
context.add("invalid_type", `${path}.service_id`, "Expected a UUID string");
|
|
7451
|
-
else if (!isLowercaseUuid(value))
|
|
7452
|
-
context.add("invalid_uuid", `${path}.service_id`, "Expected a lowercase UUID");
|
|
7453
|
-
else if (!serviceIds.has(value))
|
|
7454
|
-
context.add("unknown_service", `${path}.service_id`, "Service does not exist");
|
|
7455
|
-
}
|
|
7456
7506
|
function parseNormalizeCompile(input, options) {
|
|
7457
7507
|
const context = new ValidationContext;
|
|
7458
7508
|
const safeInput = preflightJsonGraph(input, context);
|
|
@@ -7474,11 +7524,10 @@ function parseNormalizeCompile(input, options) {
|
|
|
7474
7524
|
const catalog = options?.availablePlugins === undefined ? options?.pluginSchemas : new Map([...options.pluginSchemas].filter(([name]) => options.availablePlugins?.has(name)));
|
|
7475
7525
|
const plugins = parsePlugins(root.plugins, "plugins", catalog, context, "plugins" in root);
|
|
7476
7526
|
const services = parseServices(root.services, catalog, context);
|
|
7477
|
-
const serviceIds = new Set(services.map(({ id }) => id));
|
|
7478
7527
|
const value = {
|
|
7479
7528
|
...policies,
|
|
7480
7529
|
services,
|
|
7481
|
-
routes: parseRoutes(root.routes,
|
|
7530
|
+
routes: parseRoutes(root.routes, catalog, context),
|
|
7482
7531
|
plugins
|
|
7483
7532
|
};
|
|
7484
7533
|
validateGatewayTargets(value, catalog, context);
|
|
@@ -8676,16 +8725,12 @@ function writeUpstreams(db, upstreams, owner) {
|
|
|
8676
8725
|
}
|
|
8677
8726
|
}
|
|
8678
8727
|
function servicePolicy(service) {
|
|
8679
|
-
const { id, position, name, endpoints, plugins, ...policy } = service;
|
|
8728
|
+
const { id, position, name, endpoints, service_references, plugins, ...policy } = service;
|
|
8680
8729
|
return policy;
|
|
8681
8730
|
}
|
|
8682
8731
|
function routePolicy(route) {
|
|
8683
8732
|
const { id, position, path, plugins, ...remainder } = route;
|
|
8684
|
-
|
|
8685
|
-
const { service_id, ...policy } = remainder;
|
|
8686
|
-
return policy;
|
|
8687
|
-
}
|
|
8688
|
-
const { endpoints, ...policy } = remainder;
|
|
8733
|
+
const { endpoints, service_references, ...policy } = remainder;
|
|
8689
8734
|
return policy;
|
|
8690
8735
|
}
|
|
8691
8736
|
function writeServices(db, services) {
|
|
@@ -8702,12 +8747,10 @@ function writeServices(db, services) {
|
|
|
8702
8747
|
}
|
|
8703
8748
|
function writeRoutes(db, routes) {
|
|
8704
8749
|
for (const route of routes) {
|
|
8705
|
-
|
|
8706
|
-
db.run("INSERT INTO routes (id,position,path,service_id,policy_json) VALUES (?,?,?,?,?)", [
|
|
8750
|
+
db.run("INSERT INTO routes (id,position,path,policy_json) VALUES (?,?,?,?)", [
|
|
8707
8751
|
route.id,
|
|
8708
8752
|
route.position,
|
|
8709
8753
|
route.path,
|
|
8710
|
-
serviceId,
|
|
8711
8754
|
canonicalJson(routePolicy(route))
|
|
8712
8755
|
]);
|
|
8713
8756
|
if (route.endpoints !== undefined)
|
|
@@ -8716,6 +8759,8 @@ function writeRoutes(db, routes) {
|
|
|
8716
8759
|
}
|
|
8717
8760
|
}
|
|
8718
8761
|
function replaceActiveMaterialization(db, aggregate) {
|
|
8762
|
+
if (db.query("SELECT 1 FROM sqlite_schema WHERE name='service_references'").get())
|
|
8763
|
+
db.run("DELETE FROM service_references");
|
|
8719
8764
|
db.run("DELETE FROM plugin_bindings");
|
|
8720
8765
|
db.run("DELETE FROM plugin_activations");
|
|
8721
8766
|
db.run("DELETE FROM upstreams");
|
|
@@ -8736,11 +8781,30 @@ function replaceActiveMaterialization(db, aggregate) {
|
|
|
8736
8781
|
]);
|
|
8737
8782
|
writeServices(db, logical.services);
|
|
8738
8783
|
writeRoutes(db, logical.routes);
|
|
8784
|
+
for (const service of logical.services)
|
|
8785
|
+
writeReferences(db, service.service_references ?? [], "service", service.id);
|
|
8786
|
+
for (const route of logical.routes)
|
|
8787
|
+
writeReferences(db, route.service_references ?? [], "route", route.id);
|
|
8739
8788
|
writeBindings(db, logical.plugins, { kind: "global", id: "" });
|
|
8740
8789
|
for (const activation of aggregate.plugin_activations) {
|
|
8741
8790
|
db.run("INSERT INTO plugin_activations (plugin_name) VALUES (?)", [activation.plugin_name]);
|
|
8742
8791
|
}
|
|
8743
8792
|
}
|
|
8793
|
+
function writeReferences(db, references, ownerKind, ownerId) {
|
|
8794
|
+
for (const ref of references)
|
|
8795
|
+
db.run(`INSERT INTO service_references
|
|
8796
|
+
(id,owner_kind,owner_service_id,route_id,position,service_id,weight,priority,is_disabled) VALUES(?,?,?,?,?,?,?,?,?)`, [
|
|
8797
|
+
ref.id,
|
|
8798
|
+
ownerKind,
|
|
8799
|
+
ownerKind === "service" ? ownerId : null,
|
|
8800
|
+
ownerKind === "route" ? ownerId : null,
|
|
8801
|
+
ref.position,
|
|
8802
|
+
ref.service_id,
|
|
8803
|
+
ref.weight,
|
|
8804
|
+
ref.priority,
|
|
8805
|
+
ref.is_disabled ? 1 : 0
|
|
8806
|
+
]);
|
|
8807
|
+
}
|
|
8744
8808
|
|
|
8745
8809
|
// src/config-storage/recovery-store.ts
|
|
8746
8810
|
var RECOVERY_MAX_ATTEMPTS = 6;
|
|
@@ -9279,520 +9343,261 @@ function commitTransaction(db, prepared, options, snapshot, offlineManagement =
|
|
|
9279
9343
|
} };
|
|
9280
9344
|
}
|
|
9281
9345
|
|
|
9282
|
-
// src/config-storage/
|
|
9283
|
-
import {
|
|
9346
|
+
// src/config-storage/service-reference-upgrade.ts
|
|
9347
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
9284
9348
|
|
|
9285
|
-
// src/
|
|
9286
|
-
function
|
|
9287
|
-
|
|
9349
|
+
// src/config-storage/persisted-json.ts
|
|
9350
|
+
function parseCanonicalObject(text, field, reserved = new Set) {
|
|
9351
|
+
try {
|
|
9352
|
+
const value = JSON.parse(text);
|
|
9353
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
9354
|
+
throw new TypeError("expected a JSON object");
|
|
9355
|
+
}
|
|
9356
|
+
if (text !== canonicalJson(value))
|
|
9357
|
+
throw new TypeError("expected canonical JSON text");
|
|
9358
|
+
const entries = Object.entries(value);
|
|
9359
|
+
const key = entries.find(([name]) => reserved.has(name))?.[0];
|
|
9360
|
+
if (key !== undefined)
|
|
9361
|
+
throw new TypeError(`reserved key ${key}`);
|
|
9362
|
+
return Object.fromEntries(entries);
|
|
9363
|
+
} catch (error) {
|
|
9364
|
+
throw new ConfigRepositoryError("schema_corrupt", `${field} is invalid`, error);
|
|
9365
|
+
}
|
|
9288
9366
|
}
|
|
9289
|
-
|
|
9290
|
-
|
|
9367
|
+
|
|
9368
|
+
// src/config-storage/relation-materialization.ts
|
|
9369
|
+
var UPSTREAM_RESERVED = new Set(["id", "position", "target", "weight", "priority", "is_disabled", "plugins"]);
|
|
9370
|
+
function bindingValue(row) {
|
|
9371
|
+
requireUuid(row.id, "plugin_bindings.id");
|
|
9372
|
+
const scopeKind = requireOneOf(row.scope_kind, ["global", "service", "route", "upstream"], "plugin_bindings.scope_kind");
|
|
9373
|
+
const compatibleOwner = scopeKind === "global" ? row.scope_owner === "" && row.service_id === null && row.route_id === null && row.upstream_id === null : scopeKind === "service" ? row.scope_owner === row.service_id && row.service_id !== null && row.route_id === null && row.upstream_id === null : scopeKind === "route" ? row.scope_owner === row.route_id && row.route_id !== null && row.service_id === null && row.upstream_id === null : row.scope_owner === row.upstream_id && row.upstream_id !== null && row.service_id === null && row.route_id === null;
|
|
9374
|
+
if (!compatibleOwner)
|
|
9375
|
+
throw new ConfigRepositoryError("schema_corrupt", "plugin binding owner is invalid");
|
|
9376
|
+
if (scopeKind !== "global")
|
|
9377
|
+
requireUuid(row.scope_owner, "plugin_bindings.scope_owner");
|
|
9378
|
+
return {
|
|
9379
|
+
id: row.id,
|
|
9380
|
+
position: requireSafeInteger(row.position, "plugin_bindings.position"),
|
|
9381
|
+
name: requirePluginName(row.plugin_name, "plugin_bindings.plugin_name"),
|
|
9382
|
+
...row.options_json === null ? {} : { options: parseCanonicalObject(row.options_json, "options_json") },
|
|
9383
|
+
enabled: requireBooleanInteger(row.enabled, "plugin_bindings.enabled")
|
|
9384
|
+
};
|
|
9291
9385
|
}
|
|
9292
|
-
function
|
|
9293
|
-
const
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
}
|
|
9386
|
+
function groupBindings(rows) {
|
|
9387
|
+
const groups = new Map;
|
|
9388
|
+
for (const row of rows) {
|
|
9389
|
+
const key = `${row.scope_kind}:${row.scope_owner}`;
|
|
9390
|
+
const values = groups.get(key) ?? [];
|
|
9391
|
+
values.push(bindingValue(row));
|
|
9392
|
+
groups.set(key, values);
|
|
9393
|
+
}
|
|
9394
|
+
return groups;
|
|
9300
9395
|
}
|
|
9301
|
-
function
|
|
9302
|
-
const
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9396
|
+
function takeGroup(groups, key) {
|
|
9397
|
+
const values = groups.get(key) ?? [];
|
|
9398
|
+
groups.delete(key);
|
|
9399
|
+
return values;
|
|
9400
|
+
}
|
|
9401
|
+
function groupUpstreams(rows, bindings) {
|
|
9402
|
+
const groups = new Map;
|
|
9403
|
+
for (const row of rows) {
|
|
9404
|
+
requireUuid(row.id, "upstreams.id");
|
|
9405
|
+
const ownerKind = requireOneOf(row.owner_kind, ["service", "route"], "upstreams.owner_kind");
|
|
9406
|
+
if (ownerKind === "service" && row.route_id !== null || ownerKind === "route" && row.service_id !== null) {
|
|
9407
|
+
throw new ConfigRepositoryError("schema_corrupt", "upstream has incompatible owners");
|
|
9408
|
+
}
|
|
9409
|
+
const ownerId = ownerKind === "service" ? row.service_id : row.route_id;
|
|
9410
|
+
if (ownerId === null)
|
|
9411
|
+
throw new ConfigRepositoryError("schema_corrupt", "upstream owner is missing");
|
|
9412
|
+
requireUuid(ownerId, "upstreams.owner_id");
|
|
9413
|
+
const key = `${ownerKind}:${ownerId}`;
|
|
9414
|
+
const values = groups.get(key) ?? [];
|
|
9415
|
+
values.push({
|
|
9416
|
+
...parseCanonicalObject(row.policy_json, "upstream policy_json", UPSTREAM_RESERVED),
|
|
9417
|
+
id: row.id,
|
|
9418
|
+
position: requireSafeInteger(row.position, "upstreams.position"),
|
|
9419
|
+
target: row.target,
|
|
9420
|
+
weight: requirePositiveFinite(row.weight, "upstreams.weight"),
|
|
9421
|
+
priority: requirePositiveFinite(row.priority, "upstreams.priority"),
|
|
9422
|
+
is_disabled: requireBooleanInteger(row.is_disabled, "upstreams.is_disabled"),
|
|
9423
|
+
plugins: takeGroup(bindings, `upstream:${row.id}`)
|
|
9424
|
+
});
|
|
9425
|
+
groups.set(key, values);
|
|
9426
|
+
}
|
|
9427
|
+
return groups;
|
|
9428
|
+
}
|
|
9429
|
+
|
|
9430
|
+
// src/config-storage/read-materialization.ts
|
|
9431
|
+
var SERVICE_RESERVED = new Set(["id", "position", "name", "endpoints", "service_references", "plugins"]);
|
|
9432
|
+
var ROUTE_RESERVED = new Set(["id", "position", "path", "service_id", "service", "service_references", "endpoints", "plugins"]);
|
|
9433
|
+
function readSettings(db) {
|
|
9434
|
+
const hasPublication = sqliteAll(db, "PRAGMA table_info(settings)").some(({ name }) => name === "publication_json");
|
|
9435
|
+
const rows = sqliteAll(db, `SELECT log_level,body_parser_limit,auth_json,logging_json${hasPublication ? ",publication_json" : ",NULL AS publication_json"} FROM settings`);
|
|
9436
|
+
const row = rows[0];
|
|
9437
|
+
if (rows.length !== 1 || row === undefined)
|
|
9438
|
+
throw new ConfigRepositoryError("schema_corrupt", "settings singleton is broken");
|
|
9439
|
+
return {
|
|
9440
|
+
...row.log_level === null ? {} : { log_level: requireOptionalGlobalScalar(row.log_level, "log_level") },
|
|
9441
|
+
...row.body_parser_limit === null ? {} : {
|
|
9442
|
+
body_parser_limit: requireOptionalGlobalScalar(row.body_parser_limit, "body_parser_limit")
|
|
9443
|
+
},
|
|
9444
|
+
...row.auth_json === null ? {} : { auth: parseCanonicalObject(row.auth_json, "auth_json") },
|
|
9445
|
+
...row.logging_json === null ? {} : { logging: parseCanonicalObject(row.logging_json, "logging_json") },
|
|
9446
|
+
...row.publication_json === null ? {} : { publication: parseCanonicalObject(row.publication_json, "publication_json") }
|
|
9447
|
+
};
|
|
9448
|
+
}
|
|
9449
|
+
function readRawActiveAggregate(db) {
|
|
9450
|
+
const references = new Map;
|
|
9451
|
+
for (const row of sqliteAll(db, "SELECT id,owner_kind,owner_service_id,route_id,position,service_id,weight,priority,is_disabled FROM service_references ORDER BY owner_kind,position")) {
|
|
9452
|
+
const ownerKind = requireOneOf(row.owner_kind, ["service", "route"], "service_references.owner_kind");
|
|
9453
|
+
const ownerId = ownerKind === "service" ? row.owner_service_id : row.route_id;
|
|
9454
|
+
if (ownerId === null || (ownerKind === "service" ? row.route_id !== null : row.owner_service_id !== null)) {
|
|
9455
|
+
throw new ConfigRepositoryError("schema_corrupt", "service reference owner is invalid");
|
|
9456
|
+
}
|
|
9457
|
+
const key = `${ownerKind}:${requireUuid(ownerId, "service_references.owner_id")}`;
|
|
9458
|
+
const values = references.get(key) ?? [];
|
|
9459
|
+
values.push({
|
|
9460
|
+
id: requireUuid(row.id, "service_references.id"),
|
|
9461
|
+
position: requireSafeInteger(row.position, "service_references.position"),
|
|
9462
|
+
service_id: requireUuid(row.service_id, "service_references.service_id"),
|
|
9463
|
+
weight: requirePositiveFinite(row.weight, "service_references.weight"),
|
|
9464
|
+
priority: requirePositiveFinite(row.priority, "service_references.priority"),
|
|
9465
|
+
is_disabled: requireBooleanInteger(row.is_disabled, "service_references.is_disabled")
|
|
9466
|
+
});
|
|
9467
|
+
references.set(key, values);
|
|
9468
|
+
}
|
|
9469
|
+
const takeReferences = (key) => {
|
|
9470
|
+
const values = references.get(key);
|
|
9471
|
+
references.delete(key);
|
|
9472
|
+
return values?.length ? { service_references: values } : {};
|
|
9473
|
+
};
|
|
9474
|
+
const bindings = groupBindings(sqliteAll(db, `SELECT id,scope_kind,scope_owner,service_id,route_id,
|
|
9475
|
+
upstream_id,position,plugin_name,options_json,enabled FROM plugin_bindings ORDER BY scope_kind,scope_owner,position`));
|
|
9476
|
+
const upstreams = groupUpstreams(sqliteAll(db, `SELECT id,owner_kind,service_id,route_id,
|
|
9477
|
+
position,target,weight,priority,is_disabled,policy_json FROM upstreams ORDER BY owner_kind,position`), bindings);
|
|
9478
|
+
const services = sqliteAll(db, "SELECT id,position,name,policy_json FROM services ORDER BY position").map((row) => ({
|
|
9479
|
+
...parseCanonicalObject(row.policy_json, "service policy_json", SERVICE_RESERVED),
|
|
9480
|
+
id: requireUuid(row.id, "services.id"),
|
|
9481
|
+
position: requireSafeInteger(row.position, "services.position"),
|
|
9482
|
+
...takeReferences(`service:${row.id}`),
|
|
9309
9483
|
name: row.name,
|
|
9310
|
-
|
|
9311
|
-
|
|
9484
|
+
endpoints: takeGroup(upstreams, `service:${row.id}`),
|
|
9485
|
+
plugins: takeGroup(bindings, `service:${row.id}`)
|
|
9312
9486
|
}));
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9487
|
+
const routes = sqliteAll(db, "SELECT id,position,path,policy_json FROM routes ORDER BY position").map((row) => ({
|
|
9488
|
+
...parseCanonicalObject(row.policy_json, "route policy_json", ROUTE_RESERVED),
|
|
9489
|
+
id: requireUuid(row.id, "routes.id"),
|
|
9490
|
+
position: requireSafeInteger(row.position, "routes.position"),
|
|
9491
|
+
path: row.path,
|
|
9492
|
+
...references.has(`route:${row.id}`) ? takeReferences(`route:${row.id}`) : { endpoints: takeGroup(upstreams, `route:${row.id}`) },
|
|
9493
|
+
plugins: takeGroup(bindings, `route:${row.id}`)
|
|
9494
|
+
}));
|
|
9495
|
+
const activations = sqliteAll(db, "SELECT plugin_name FROM plugin_activations ORDER BY plugin_name");
|
|
9496
|
+
const input = { logical_configuration: {
|
|
9497
|
+
...readSettings(db),
|
|
9498
|
+
services,
|
|
9499
|
+
routes,
|
|
9500
|
+
plugins: takeGroup(bindings, "global:")
|
|
9501
|
+
}, plugin_activations: activations.map(({ plugin_name }) => ({
|
|
9502
|
+
plugin_name: requirePluginName(plugin_name, "plugin_activations.plugin_name")
|
|
9503
|
+
})) };
|
|
9504
|
+
if (bindings.size !== 0 || upstreams.size !== 0 || references.size !== 0) {
|
|
9505
|
+
throw new ConfigRepositoryError("schema_corrupt", "normalized relation rows were not consumed");
|
|
9506
|
+
}
|
|
9507
|
+
return input;
|
|
9508
|
+
}
|
|
9509
|
+
function readActiveAggregate(db) {
|
|
9510
|
+
const result = parseNormalizeCompileAggregate(readRawActiveAggregate(db));
|
|
9511
|
+
if (!result.ok)
|
|
9512
|
+
throw new ConfigRepositoryError("schema_corrupt", "active configuration is invalid", result.errors);
|
|
9513
|
+
return result.value;
|
|
9329
9514
|
}
|
|
9330
9515
|
|
|
9331
|
-
// src/config-storage/
|
|
9332
|
-
var
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
up(db) {
|
|
9336
|
-
db.run(`CREATE TABLE management_protection (
|
|
9337
|
-
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
9338
|
-
required INTEGER NOT NULL CHECK(required IN (0,1)),
|
|
9339
|
-
selected_provider TEXT REFERENCES plugin_activations(plugin_name) DEFERRABLE INITIALLY DEFERRED,
|
|
9340
|
-
generation INTEGER NOT NULL CHECK(generation BETWEEN 0 AND 9007199254740991),
|
|
9341
|
-
CHECK((required=0 AND selected_provider IS NULL) OR (required=1 AND selected_provider IS NOT NULL))
|
|
9342
|
-
) STRICT`);
|
|
9343
|
-
db.run("INSERT INTO management_protection(id,required,selected_provider,generation) VALUES(1,0,NULL,0)");
|
|
9344
|
-
db.run("CREATE TRIGGER management_protection_no_delete BEFORE DELETE ON management_protection BEGIN SELECT RAISE(ABORT,'management protection cannot be deleted'); END");
|
|
9345
|
-
db.run("CREATE TRIGGER management_protection_no_replace BEFORE INSERT ON management_protection WHEN EXISTS(SELECT 1 FROM management_protection) BEGIN SELECT RAISE(ABORT,'management protection cannot be replaced'); END");
|
|
9346
|
-
db.run("CREATE TRIGGER management_protection_fixed_identity BEFORE UPDATE OF id ON management_protection BEGIN SELECT RAISE(ABORT,'management protection identity is immutable'); END");
|
|
9347
|
-
db.run("ALTER TABLE configuration_operations ADD COLUMN management_provider TEXT");
|
|
9348
|
-
db.run("ALTER TABLE configuration_operations ADD COLUMN offline_management INTEGER NOT NULL DEFAULT 0 CHECK(offline_management IN (0,1))");
|
|
9349
|
-
}
|
|
9516
|
+
// src/config-storage/cross-table-audit.ts
|
|
9517
|
+
var EMPTY_AGGREGATE = {
|
|
9518
|
+
logical_configuration: { services: [], routes: [], plugins: [] },
|
|
9519
|
+
plugin_activations: []
|
|
9350
9520
|
};
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
9359
|
-
revision INTEGER NOT NULL CHECK(revision >= 0 AND revision <= 9007199254740991),
|
|
9360
|
-
layout_json TEXT CHECK(layout_json IS NULL OR (json_valid(layout_json) AND json_type(layout_json)='array'
|
|
9361
|
-
AND length(CAST(layout_json AS BLOB)) <= 262144)),
|
|
9362
|
-
CHECK((revision=0 AND layout_json IS NULL) OR (revision>0 AND layout_json IS NOT NULL))
|
|
9363
|
-
) STRICT`);
|
|
9364
|
-
db.run("INSERT INTO ui_menu_layout(id,revision,layout_json) VALUES(1,0,NULL)");
|
|
9365
|
-
db.run("CREATE TRIGGER ui_menu_layout_no_delete BEFORE DELETE ON ui_menu_layout BEGIN SELECT RAISE(ABORT,'menu singleton cannot be deleted'); END");
|
|
9366
|
-
db.run("CREATE TRIGGER ui_menu_layout_no_replace BEFORE INSERT ON ui_menu_layout WHEN EXISTS(SELECT 1 FROM ui_menu_layout) BEGIN SELECT RAISE(ABORT,'menu singleton cannot be replaced'); END");
|
|
9367
|
-
db.run("CREATE TRIGGER ui_menu_layout_fixed_identity BEFORE UPDATE OF id ON ui_menu_layout BEGIN SELECT RAISE(ABORT,'menu singleton identity is immutable'); END");
|
|
9521
|
+
var INITIAL_REVISION_HASH = hashConfigurationContent(EMPTY_AGGREGATE);
|
|
9522
|
+
function auditConfigurationTables(db) {
|
|
9523
|
+
const states = sqliteAll(db, `SELECT id,active_revision,schema_version,created_at,updated_at
|
|
9524
|
+
FROM configuration_state ORDER BY id`);
|
|
9525
|
+
const state = states[0];
|
|
9526
|
+
if (states.length !== 1 || state === undefined || state.id !== 1 || state.schema_version !== 4) {
|
|
9527
|
+
throw new ConfigRepositoryError("schema_corrupt", "configuration state singleton is broken");
|
|
9368
9528
|
}
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
`CREATE TABLE configuration_operation_workers (
|
|
9377
|
-
mutation_id TEXT NOT NULL,
|
|
9378
|
-
worker_slot INTEGER NOT NULL CHECK(worker_slot >= 0 AND worker_slot <= 9007199254740991),
|
|
9379
|
-
target_revision INTEGER NOT NULL CHECK(target_revision > 0 AND target_revision <= 9007199254740991),
|
|
9380
|
-
drain_recovery_generation INTEGER NOT NULL DEFAULT 0
|
|
9381
|
-
CHECK(drain_recovery_generation >= 0 AND drain_recovery_generation <= 9007199254740991),
|
|
9382
|
-
attempt_no INTEGER NOT NULL DEFAULT 0 CHECK(attempt_no >= 0 AND attempt_no <= 9007199254740991),
|
|
9383
|
-
last_begin_previous_attempt_no INTEGER CHECK(last_begin_previous_attempt_no IS NULL OR
|
|
9384
|
-
(last_begin_previous_attempt_no >= 0 AND last_begin_previous_attempt_no < 9007199254740991)),
|
|
9385
|
-
last_begin_reason TEXT CHECK(last_begin_reason IS NULL OR last_begin_reason IN ('initial','retry','master_recovery')),
|
|
9386
|
-
state TEXT NOT NULL CHECK(state IN ('pending','converged','failed')),
|
|
9387
|
-
applied_revision INTEGER CHECK(applied_revision IS NULL OR (applied_revision > 0 AND applied_revision <= 9007199254740991)),
|
|
9388
|
-
last_error TEXT,
|
|
9389
|
-
updated_at INTEGER NOT NULL CHECK(updated_at >= 0 AND updated_at <= 9007199254740991),
|
|
9390
|
-
PRIMARY KEY(mutation_id,worker_slot),
|
|
9391
|
-
FOREIGN KEY(mutation_id,target_revision) REFERENCES configuration_operations(mutation_id,committed_revision) ON DELETE CASCADE,
|
|
9392
|
-
CHECK((attempt_no=0 AND last_begin_previous_attempt_no IS NULL AND last_begin_reason IS NULL) OR
|
|
9393
|
-
(attempt_no=1 AND last_begin_previous_attempt_no=0 AND last_begin_reason IN ('initial','master_recovery')) OR
|
|
9394
|
-
(attempt_no > 1 AND last_begin_previous_attempt_no=attempt_no-1 AND
|
|
9395
|
-
last_begin_reason IN ('retry','master_recovery'))),
|
|
9396
|
-
CHECK((state='pending' AND applied_revision IS NULL AND last_error IS NULL) OR
|
|
9397
|
-
(state='converged' AND attempt_no > 0 AND applied_revision=target_revision AND last_error IS NULL) OR
|
|
9398
|
-
(state='failed' AND attempt_no > 0 AND last_error IS NOT NULL AND
|
|
9399
|
-
length(last_error) <= 512 AND length(trim(last_error)) > 0))
|
|
9400
|
-
) STRICT, WITHOUT ROWID`,
|
|
9401
|
-
`CREATE TABLE configuration_operations (
|
|
9402
|
-
mutation_id TEXT PRIMARY KEY CHECK(length(mutation_id) BETWEEN 1 AND 128),
|
|
9403
|
-
request_hash TEXT NOT NULL CHECK(length(request_hash)=71 AND substr(request_hash,1,7)='sha256:' AND substr(request_hash,8) NOT GLOB '*[^0-9a-f]*'),
|
|
9404
|
-
expected_revision INTEGER NOT NULL CHECK(expected_revision > 0 AND expected_revision <= 9007199254740991),
|
|
9405
|
-
committed_revision INTEGER NOT NULL CHECK(committed_revision > 0 AND committed_revision <= 9007199254740991)
|
|
9406
|
-
REFERENCES configuration_revisions(revision),
|
|
9407
|
-
kind TEXT NOT NULL CHECK(kind IN ('config','admin_state')),
|
|
9408
|
-
target_worker_count INTEGER NOT NULL CHECK(target_worker_count >= 0 AND target_worker_count <= 9007199254740991),
|
|
9409
|
-
state TEXT NOT NULL CHECK(state IN ('committed','publishing','draining','converged','degraded')),
|
|
9410
|
-
result_status INTEGER,
|
|
9411
|
-
error_code TEXT,
|
|
9412
|
-
error_detail TEXT,
|
|
9413
|
-
drain_recovery_generation INTEGER NOT NULL DEFAULT 0
|
|
9414
|
-
CHECK(drain_recovery_generation >= 0 AND drain_recovery_generation <= 9007199254740991),
|
|
9415
|
-
last_drain_recovery_previous_generation INTEGER
|
|
9416
|
-
CHECK(last_drain_recovery_previous_generation IS NULL OR
|
|
9417
|
-
(last_drain_recovery_previous_generation >= 0 AND
|
|
9418
|
-
last_drain_recovery_previous_generation < 9007199254740991)),
|
|
9419
|
-
created_at INTEGER NOT NULL CHECK(created_at >= 0 AND created_at <= 9007199254740991),
|
|
9420
|
-
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at AND updated_at <= 9007199254740991),
|
|
9421
|
-
UNIQUE(mutation_id,committed_revision),
|
|
9422
|
-
CHECK((drain_recovery_generation=0 AND last_drain_recovery_previous_generation IS NULL) OR
|
|
9423
|
-
(drain_recovery_generation > 0 AND
|
|
9424
|
-
last_drain_recovery_previous_generation=drain_recovery_generation-1)),
|
|
9425
|
-
CHECK((state IN ('committed','publishing') AND drain_recovery_generation=0 AND
|
|
9426
|
-
result_status IS NULL AND error_code IS NULL AND error_detail IS NULL) OR
|
|
9427
|
-
(state='draining' AND result_status IS NULL AND error_code IS NULL AND error_detail IS NULL) OR
|
|
9428
|
-
(state='converged' AND drain_recovery_generation=0 AND
|
|
9429
|
-
result_status=200 AND error_code IS NULL AND error_detail IS NULL) OR
|
|
9430
|
-
(state='degraded' AND result_status=202 AND
|
|
9431
|
-
error_code IN ('replacement_convergence_failed','old_worker_drain_failed','control_readiness_failed') AND
|
|
9432
|
-
(error_code <> 'replacement_convergence_failed' OR drain_recovery_generation=0) AND
|
|
9433
|
-
error_detail IS NOT NULL AND length(error_detail) <= 512 AND length(trim(error_detail)) > 0))
|
|
9434
|
-
) STRICT`,
|
|
9435
|
-
`CREATE TABLE configuration_recoveries (
|
|
9436
|
-
recovery_sequence INTEGER PRIMARY KEY AUTOINCREMENT CHECK(recovery_sequence > 0),
|
|
9437
|
-
recovery_id TEXT NOT NULL UNIQUE CHECK(length(recovery_id)=36 AND substr(recovery_id,9,1)='-' AND substr(recovery_id,14,1)='-' AND substr(recovery_id,19,1)='-' AND substr(recovery_id,24,1)='-' AND replace(recovery_id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
9438
|
-
source_mutation_id TEXT NOT NULL,
|
|
9439
|
-
target_revision INTEGER NOT NULL CHECK(target_revision > 0 AND target_revision <= 9007199254740991),
|
|
9440
|
-
trigger TEXT NOT NULL CHECK(trigger IN ('automatic','manual')),
|
|
9441
|
-
state TEXT NOT NULL CHECK(state IN ('scheduled','running','succeeded','stopped')),
|
|
9442
|
-
attempt_count INTEGER NOT NULL CHECK(attempt_count >= 0 AND attempt_count <= 6),
|
|
9443
|
-
max_attempts INTEGER NOT NULL CHECK(max_attempts=6),
|
|
9444
|
-
next_retry_at INTEGER CHECK(next_retry_at IS NULL OR (next_retry_at >= 0 AND next_retry_at <= 9007199254740991)),
|
|
9445
|
-
final_reason_code TEXT CHECK(final_reason_code IS NULL OR final_reason_code IN
|
|
9446
|
-
('target_serving','already_serving','deterministic_worker_rejection','deterministic_protocol_failure',
|
|
9447
|
-
'deterministic_control_failure','retry_exhausted','revision_superseded','safety_outcome_unknown','fatal_source_failure')), final_reason_detail TEXT,
|
|
9448
|
-
created_at INTEGER NOT NULL CHECK(created_at >= 0 AND created_at <= 9007199254740991),
|
|
9449
|
-
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at AND updated_at <= 9007199254740991),
|
|
9450
|
-
FOREIGN KEY(source_mutation_id) REFERENCES configuration_operations(mutation_id),
|
|
9451
|
-
FOREIGN KEY(target_revision) REFERENCES configuration_revisions(revision),
|
|
9452
|
-
CHECK((state='scheduled' AND final_reason_code IS NULL AND final_reason_detail IS NULL) OR
|
|
9453
|
-
(state='running' AND next_retry_at IS NULL AND final_reason_code IS NULL AND final_reason_detail IS NULL) OR
|
|
9454
|
-
(state='succeeded' AND ((final_reason_code='already_serving' AND attempt_count=0) OR
|
|
9455
|
-
(final_reason_code='target_serving' AND attempt_count > 0)) AND next_retry_at IS NULL) OR
|
|
9456
|
-
(state='stopped' AND final_reason_code IN ('deterministic_worker_rejection','deterministic_protocol_failure',
|
|
9457
|
-
'deterministic_control_failure','revision_superseded') AND next_retry_at IS NULL) OR
|
|
9458
|
-
(state='stopped' AND final_reason_code='retry_exhausted' AND attempt_count=6 AND next_retry_at IS NULL) OR
|
|
9459
|
-
(state='stopped' AND final_reason_code='safety_outcome_unknown' AND attempt_count > 0 AND next_retry_at IS NULL) OR
|
|
9460
|
-
(state='stopped' AND final_reason_code='fatal_source_failure' AND attempt_count=0 AND next_retry_at IS NULL)),
|
|
9461
|
-
CHECK(state <> 'scheduled' OR next_retry_at IS NULL OR next_retry_at > updated_at),
|
|
9462
|
-
CHECK(state <> 'scheduled' OR attempt_count < 6), CHECK(state <> 'running' OR attempt_count > 0),
|
|
9463
|
-
CHECK(final_reason_detail IS NULL OR (length(final_reason_detail) <= 512 AND length(trim(final_reason_detail)) > 0))
|
|
9464
|
-
) STRICT`,
|
|
9465
|
-
`CREATE TABLE configuration_revisions (
|
|
9466
|
-
revision INTEGER PRIMARY KEY CHECK(revision > 0 AND revision <= 9007199254740991),
|
|
9467
|
-
content_hash TEXT NOT NULL CHECK(length(content_hash)=71 AND substr(content_hash,1,7)='sha256:' AND substr(content_hash,8) NOT GLOB '*[^0-9a-f]*'),
|
|
9468
|
-
kind TEXT NOT NULL CHECK(kind IN ('config','admin_state')),
|
|
9469
|
-
created_at INTEGER NOT NULL CHECK(created_at >= 0 AND created_at <= 9007199254740991)
|
|
9470
|
-
) STRICT`,
|
|
9471
|
-
`CREATE TABLE configuration_serving_snapshots (
|
|
9472
|
-
revision INTEGER NOT NULL CHECK(revision > 0 AND revision <= 9007199254740991),
|
|
9473
|
-
content_hash TEXT NOT NULL CHECK(length(content_hash)=71 AND substr(content_hash,1,7)='sha256:' AND substr(content_hash,8) NOT GLOB '*[^0-9a-f]*'),
|
|
9474
|
-
plugin_catalog_hash TEXT NOT NULL CHECK(length(plugin_catalog_hash)=71 AND substr(plugin_catalog_hash,1,7)='sha256:' AND substr(plugin_catalog_hash,8) NOT GLOB '*[^0-9a-f]*'),
|
|
9475
|
-
aggregate_json TEXT NOT NULL CHECK(json_valid(aggregate_json) AND json_type(aggregate_json)='object'
|
|
9476
|
-
AND length(CAST(aggregate_json AS BLOB)) BETWEEN 2 AND 1048576),
|
|
9477
|
-
PRIMARY KEY(revision,content_hash,plugin_catalog_hash),
|
|
9478
|
-
FOREIGN KEY(revision,content_hash)
|
|
9479
|
-
REFERENCES configuration_revisions(revision,content_hash)
|
|
9480
|
-
) STRICT, WITHOUT ROWID`,
|
|
9481
|
-
`CREATE TABLE configuration_state (
|
|
9482
|
-
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
9483
|
-
schema_version INTEGER NOT NULL CHECK(schema_version=4),
|
|
9484
|
-
active_revision INTEGER NOT NULL CHECK(active_revision > 0 AND active_revision <= 9007199254740991)
|
|
9485
|
-
REFERENCES configuration_revisions(revision),
|
|
9486
|
-
created_at INTEGER NOT NULL CHECK(created_at >= 0 AND created_at <= 9007199254740991),
|
|
9487
|
-
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at AND updated_at <= 9007199254740991)
|
|
9488
|
-
) STRICT`,
|
|
9489
|
-
`CREATE TABLE plugin_activations (
|
|
9490
|
-
plugin_name TEXT PRIMARY KEY CHECK(length(plugin_name) > 0 AND plugin_name=lower(plugin_name)
|
|
9491
|
-
AND plugin_name NOT GLOB '*[^a-z0-9-]*' AND substr(plugin_name,1,1) GLOB '[a-z]'
|
|
9492
|
-
AND substr(plugin_name,-1,1)<>'-' AND plugin_name NOT LIKE '%--%')
|
|
9493
|
-
) STRICT, WITHOUT ROWID`,
|
|
9494
|
-
`CREATE TABLE plugin_bindings (
|
|
9495
|
-
id TEXT PRIMARY KEY CHECK(length(id)=36 AND substr(id,9,1)='-' AND substr(id,14,1)='-' AND substr(id,19,1)='-' AND substr(id,24,1)='-' AND replace(id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
9496
|
-
scope_kind TEXT NOT NULL CHECK(scope_kind IN ('global','service','route','upstream')),
|
|
9497
|
-
scope_owner TEXT NOT NULL,
|
|
9498
|
-
service_id TEXT REFERENCES services(id) ON DELETE CASCADE,
|
|
9499
|
-
route_id TEXT REFERENCES routes(id) ON DELETE CASCADE,
|
|
9500
|
-
upstream_id TEXT REFERENCES upstreams(id) ON DELETE CASCADE,
|
|
9501
|
-
position INTEGER NOT NULL CHECK(position >= 0 AND position <= 9007199254740991),
|
|
9502
|
-
plugin_name TEXT NOT NULL CHECK(length(plugin_name) > 0 AND plugin_name=lower(plugin_name)
|
|
9503
|
-
AND plugin_name NOT GLOB '*[^a-z0-9-]*' AND substr(plugin_name,1,1) GLOB '[a-z]'
|
|
9504
|
-
AND substr(plugin_name,-1,1)<>'-' AND plugin_name NOT LIKE '%--%'),
|
|
9505
|
-
options_json TEXT CHECK(options_json IS NULL OR (json_valid(options_json) AND json_type(options_json)='object')),
|
|
9506
|
-
enabled INTEGER NOT NULL CHECK(enabled IN (0,1)),
|
|
9507
|
-
CHECK((scope_kind='global' AND scope_owner='' AND service_id IS NULL AND route_id IS NULL AND upstream_id IS NULL) OR
|
|
9508
|
-
(scope_kind='service' AND service_id IS NOT NULL AND scope_owner=service_id AND route_id IS NULL AND upstream_id IS NULL) OR
|
|
9509
|
-
(scope_kind='route' AND route_id IS NOT NULL AND scope_owner=route_id AND service_id IS NULL AND upstream_id IS NULL) OR
|
|
9510
|
-
(scope_kind='upstream' AND upstream_id IS NOT NULL AND scope_owner=upstream_id AND service_id IS NULL AND route_id IS NULL)),
|
|
9511
|
-
UNIQUE(scope_kind,scope_owner,position)
|
|
9512
|
-
) STRICT`,
|
|
9513
|
-
`CREATE TABLE routes (
|
|
9514
|
-
id TEXT PRIMARY KEY CHECK(length(id)=36 AND substr(id,9,1)='-' AND substr(id,14,1)='-' AND substr(id,19,1)='-' AND substr(id,24,1)='-' AND replace(id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
9515
|
-
position INTEGER NOT NULL UNIQUE CHECK(position >= 0 AND position <= 9007199254740991),
|
|
9516
|
-
path TEXT NOT NULL UNIQUE CHECK(substr(path,1,1)='/'),
|
|
9517
|
-
service_id TEXT REFERENCES services(id),
|
|
9518
|
-
policy_json TEXT NOT NULL CHECK(json_valid(policy_json) AND json_type(policy_json)='object')
|
|
9519
|
-
) STRICT`,
|
|
9520
|
-
`CREATE TABLE schema_migrations (
|
|
9521
|
-
version INTEGER PRIMARY KEY CHECK(version > 0 AND version <= 9007199254740991),
|
|
9522
|
-
name TEXT NOT NULL UNIQUE
|
|
9523
|
-
) STRICT`,
|
|
9524
|
-
`CREATE TABLE services (
|
|
9525
|
-
id TEXT PRIMARY KEY CHECK(length(id)=36 AND substr(id,9,1)='-' AND substr(id,14,1)='-' AND substr(id,19,1)='-' AND substr(id,24,1)='-' AND replace(id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
9526
|
-
position INTEGER NOT NULL UNIQUE CHECK(position >= 0 AND position <= 9007199254740991),
|
|
9527
|
-
name TEXT NOT NULL UNIQUE CHECK(length(name) > 0),
|
|
9528
|
-
policy_json TEXT NOT NULL CHECK(json_valid(policy_json) AND json_type(policy_json)='object')
|
|
9529
|
-
) STRICT`,
|
|
9530
|
-
`CREATE TABLE settings (
|
|
9531
|
-
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
9532
|
-
log_level TEXT CHECK(log_level IS NULL OR log_level IN ('trace','debug','info','warn','error','fatal')),
|
|
9533
|
-
body_parser_limit TEXT CHECK(body_parser_limit IS NULL OR CASE
|
|
9534
|
-
WHEN substr(body_parser_limit,-2) IN ('gb','mb','kb') THEN
|
|
9535
|
-
length(substr(body_parser_limit,1,length(body_parser_limit)-2)) > 0
|
|
9536
|
-
AND body_parser_limit=substr(body_parser_limit,1,length(body_parser_limit)-2)||substr(body_parser_limit,-2)
|
|
9537
|
-
AND substr(body_parser_limit,1,1) GLOB '[1-9]'
|
|
9538
|
-
AND substr(body_parser_limit,1,length(body_parser_limit)-2) NOT GLOB '*[^0-9]*'
|
|
9539
|
-
AND (length(substr(body_parser_limit,1,length(body_parser_limit)-2)) < 16 OR
|
|
9540
|
-
(length(substr(body_parser_limit,1,length(body_parser_limit)-2))=16 AND
|
|
9541
|
-
substr(body_parser_limit,1,length(body_parser_limit)-2)<='9007199254740991'))
|
|
9542
|
-
WHEN substr(body_parser_limit,-1)='b' THEN
|
|
9543
|
-
length(substr(body_parser_limit,1,length(body_parser_limit)-1)) > 0
|
|
9544
|
-
AND body_parser_limit=substr(body_parser_limit,1,length(body_parser_limit)-1)||'b'
|
|
9545
|
-
AND substr(body_parser_limit,1,1) GLOB '[1-9]'
|
|
9546
|
-
AND substr(body_parser_limit,1,length(body_parser_limit)-1) NOT GLOB '*[^0-9]*'
|
|
9547
|
-
AND (length(substr(body_parser_limit,1,length(body_parser_limit)-1)) < 16 OR
|
|
9548
|
-
(length(substr(body_parser_limit,1,length(body_parser_limit)-1))=16 AND
|
|
9549
|
-
substr(body_parser_limit,1,length(body_parser_limit)-1)<='9007199254740991'))
|
|
9550
|
-
ELSE 0 END),
|
|
9551
|
-
auth_json TEXT CHECK(auth_json IS NULL OR json_valid(auth_json)),
|
|
9552
|
-
logging_json TEXT CHECK(logging_json IS NULL OR json_valid(logging_json))
|
|
9553
|
-
, publication_json TEXT) STRICT`,
|
|
9554
|
-
`CREATE TABLE supervision_state (
|
|
9555
|
-
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
9556
|
-
instance_id TEXT NOT NULL UNIQUE CHECK(length(instance_id)=36),
|
|
9557
|
-
controller_epoch INTEGER NOT NULL CHECK(controller_epoch >= 0 AND controller_epoch < 9007199254740991),
|
|
9558
|
-
current_controller_id TEXT,
|
|
9559
|
-
updated_at INTEGER NOT NULL CHECK(updated_at >= 0 AND updated_at < 9007199254740991)
|
|
9560
|
-
) STRICT, WITHOUT ROWID`,
|
|
9561
|
-
`CREATE TABLE upstreams (
|
|
9562
|
-
id TEXT PRIMARY KEY CHECK(length(id)=36 AND substr(id,9,1)='-' AND substr(id,14,1)='-' AND substr(id,19,1)='-' AND substr(id,24,1)='-' AND replace(id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
9563
|
-
owner_kind TEXT NOT NULL CHECK(owner_kind IN ('service','route')),
|
|
9564
|
-
service_id TEXT REFERENCES services(id) ON DELETE CASCADE,
|
|
9565
|
-
route_id TEXT REFERENCES routes(id) ON DELETE CASCADE,
|
|
9566
|
-
position INTEGER NOT NULL CHECK(position >= 0 AND position <= 9007199254740991),
|
|
9567
|
-
target TEXT NOT NULL CHECK(length(target) > 0),
|
|
9568
|
-
weight REAL NOT NULL CHECK(weight > 0),
|
|
9569
|
-
priority REAL NOT NULL CHECK(priority > 0),
|
|
9570
|
-
is_disabled INTEGER NOT NULL CHECK(is_disabled IN (0,1)),
|
|
9571
|
-
policy_json TEXT NOT NULL CHECK(json_valid(policy_json) AND json_type(policy_json)='object'),
|
|
9572
|
-
CHECK((owner_kind='service' AND service_id IS NOT NULL AND route_id IS NULL) OR
|
|
9573
|
-
(owner_kind='route' AND route_id IS NOT NULL AND service_id IS NULL)),
|
|
9574
|
-
UNIQUE(owner_kind,service_id,position), UNIQUE(owner_kind,route_id,position)
|
|
9575
|
-
) STRICT`,
|
|
9576
|
-
`CREATE UNIQUE INDEX configuration_recoveries_active_target_revision
|
|
9577
|
-
ON configuration_recoveries(target_revision) WHERE state IN ('scheduled','running')`,
|
|
9578
|
-
`CREATE UNIQUE INDEX configuration_revisions_revision_content_hash
|
|
9579
|
-
ON configuration_revisions(revision,content_hash)`,
|
|
9580
|
-
`CREATE TRIGGER configuration_operations_terminal_immutable_delete
|
|
9581
|
-
BEFORE DELETE ON configuration_operations
|
|
9582
|
-
WHEN OLD.state IN ('converged','degraded')
|
|
9583
|
-
BEGIN
|
|
9584
|
-
SELECT RAISE(ABORT,'terminal configuration operations are immutable');
|
|
9585
|
-
END`,
|
|
9586
|
-
`CREATE TRIGGER configuration_operations_terminal_immutable_update
|
|
9587
|
-
BEFORE UPDATE ON configuration_operations
|
|
9588
|
-
WHEN OLD.state IN ('converged','degraded')
|
|
9589
|
-
BEGIN
|
|
9590
|
-
SELECT RAISE(ABORT,'terminal configuration operations are immutable');
|
|
9591
|
-
END`,
|
|
9592
|
-
`CREATE TRIGGER configuration_recoveries_no_delete BEFORE DELETE ON configuration_recoveries
|
|
9593
|
-
BEGIN SELECT RAISE(ABORT,'configuration recovery cannot be deleted'); END`,
|
|
9594
|
-
`CREATE TRIGGER configuration_recoveries_sequence_insert_guard BEFORE INSERT ON configuration_recoveries
|
|
9595
|
-
WHEN NEW.recovery_sequence != -1 BEGIN SELECT RAISE(ABORT,'recovery sequence is database assigned'); END`,
|
|
9596
|
-
`CREATE TRIGGER configuration_recoveries_sequence_update_guard BEFORE UPDATE OF recovery_sequence ON configuration_recoveries
|
|
9597
|
-
BEGIN SELECT RAISE(ABORT,'recovery sequence is immutable'); END`,
|
|
9598
|
-
`CREATE TRIGGER configuration_serving_snapshots_immutable_delete
|
|
9599
|
-
BEFORE DELETE ON configuration_serving_snapshots
|
|
9600
|
-
BEGIN
|
|
9601
|
-
SELECT RAISE(ABORT,'configuration serving snapshots are immutable');
|
|
9602
|
-
END`,
|
|
9603
|
-
`CREATE TRIGGER configuration_serving_snapshots_immutable_update
|
|
9604
|
-
BEFORE UPDATE ON configuration_serving_snapshots
|
|
9605
|
-
BEGIN
|
|
9606
|
-
SELECT RAISE(ABORT,'configuration serving snapshots are immutable');
|
|
9607
|
-
END`,
|
|
9608
|
-
`CREATE TRIGGER configuration_state_singleton_id_immutable
|
|
9609
|
-
BEFORE UPDATE OF id ON configuration_state
|
|
9610
|
-
BEGIN
|
|
9611
|
-
SELECT RAISE(ABORT,'configuration state singleton identity is immutable');
|
|
9612
|
-
END`,
|
|
9613
|
-
`CREATE TRIGGER configuration_state_singleton_no_delete
|
|
9614
|
-
BEFORE DELETE ON configuration_state
|
|
9615
|
-
BEGIN
|
|
9616
|
-
SELECT RAISE(ABORT,'configuration state singleton cannot be deleted');
|
|
9617
|
-
END`,
|
|
9618
|
-
`CREATE TRIGGER configuration_state_singleton_no_replace
|
|
9619
|
-
BEFORE INSERT ON configuration_state
|
|
9620
|
-
WHEN EXISTS(SELECT 1 FROM configuration_state)
|
|
9621
|
-
BEGIN
|
|
9622
|
-
SELECT RAISE(ABORT,'configuration state singleton cannot be replaced');
|
|
9623
|
-
END`
|
|
9624
|
-
];
|
|
9625
|
-
|
|
9626
|
-
// src/config-storage/migrations/baseline.ts
|
|
9627
|
-
var CONFIG_BASELINE_VERSION = 1;
|
|
9628
|
-
var CONFIG_BASELINE_NAME = "current_storage_baseline";
|
|
9629
|
-
var CONFIG_BASELINE = {
|
|
9630
|
-
version: CONFIG_BASELINE_VERSION,
|
|
9631
|
-
name: CONFIG_BASELINE_NAME,
|
|
9632
|
-
up(db) {
|
|
9633
|
-
for (const statement of CONFIG_SCHEMA_STATEMENTS)
|
|
9634
|
-
db.run(statement);
|
|
9635
|
-
const aggregate = { logical_configuration: { services: [], routes: [], plugins: [] }, plugin_activations: [] };
|
|
9636
|
-
replaceActiveMaterialization(db, aggregate);
|
|
9637
|
-
db.run("INSERT INTO configuration_revisions(revision,content_hash,kind,created_at) VALUES(1,?,'config',0)", [hashConfigurationContent(aggregate)]);
|
|
9638
|
-
db.run("INSERT INTO configuration_state(id,schema_version,active_revision,created_at,updated_at) VALUES(1,4,1,0,0)");
|
|
9639
|
-
db.run("INSERT INTO supervision_state(id,instance_id,controller_epoch,current_controller_id,updated_at) VALUES(1,?,0,NULL,0)", [randomUUID()]);
|
|
9529
|
+
requireSafeInteger(state.active_revision, "configuration_state.active_revision", 1);
|
|
9530
|
+
requireSafeInteger(state.created_at, "configuration_state.created_at");
|
|
9531
|
+
requireSafeInteger(state.updated_at, "configuration_state.updated_at", state.created_at);
|
|
9532
|
+
const revisions = sqliteAll(db, `SELECT revision,content_hash,kind,created_at
|
|
9533
|
+
FROM configuration_revisions ORDER BY revision`);
|
|
9534
|
+
if (revisions.length !== state.active_revision) {
|
|
9535
|
+
throw new ConfigRepositoryError("schema_corrupt", "configuration revisions are not contiguous");
|
|
9640
9536
|
}
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
) STRICT`);
|
|
9656
|
-
db.run(`CREATE TABLE configuration_retirement_evidence (
|
|
9657
|
-
mutation_id TEXT NOT NULL REFERENCES configuration_retirements(mutation_id),
|
|
9658
|
-
member_id TEXT NOT NULL,
|
|
9659
|
-
exit_state TEXT NOT NULL CHECK(exit_state IN ('live','confirmed','unknown')),
|
|
9660
|
-
cleanup_state TEXT NOT NULL CHECK(cleanup_state IN ('success','failed','unknown')),
|
|
9661
|
-
exit_boot_id TEXT,
|
|
9662
|
-
exit_deadline_ns TEXT,
|
|
9663
|
-
start_boot_id TEXT,
|
|
9664
|
-
start_deadline_ns TEXT,
|
|
9665
|
-
updated_at INTEGER NOT NULL CHECK(updated_at >= 0),
|
|
9666
|
-
CHECK((exit_boot_id IS NULL AND exit_deadline_ns IS NULL) OR (exit_boot_id IS NOT NULL AND exit_deadline_ns IS NOT NULL)),
|
|
9667
|
-
CHECK((start_boot_id IS NULL AND start_deadline_ns IS NULL) OR (start_boot_id IS NOT NULL AND start_deadline_ns IS NOT NULL)),
|
|
9668
|
-
PRIMARY KEY(mutation_id,member_id)
|
|
9669
|
-
) STRICT, WITHOUT ROWID`);
|
|
9670
|
-
db.run("CREATE TRIGGER retirement_start_deadline_immutable BEFORE UPDATE OF start_boot_id,start_deadline_ns ON configuration_retirement_evidence WHEN OLD.start_deadline_ns IS NOT NULL AND (NEW.start_boot_id IS NOT OLD.start_boot_id OR NEW.start_deadline_ns IS NOT OLD.start_deadline_ns) BEGIN SELECT RAISE(ABORT,'retirement start deadline is immutable'); END");
|
|
9671
|
-
db.run(`CREATE TABLE configuration_publication_serving_sets (
|
|
9672
|
-
mutation_id TEXT NOT NULL REFERENCES configuration_operations(mutation_id),
|
|
9673
|
-
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
9674
|
-
attempt_key TEXT NOT NULL CHECK(json_valid(attempt_key) AND json_type(attempt_key)='array'),
|
|
9675
|
-
workers_json TEXT NOT NULL CHECK(json_valid(workers_json) AND json_type(workers_json)='array'),
|
|
9676
|
-
exited_json TEXT NOT NULL CHECK(json_valid(exited_json) AND json_type(exited_json)='array'),
|
|
9677
|
-
PRIMARY KEY(mutation_id,generation,attempt_key)
|
|
9678
|
-
) STRICT, WITHOUT ROWID`);
|
|
9679
|
-
db.run("CREATE TRIGGER publication_serving_authority_no_delete BEFORE DELETE ON configuration_publication_serving_sets BEGIN SELECT RAISE(ABORT,'publication serving authority history is immutable'); END");
|
|
9680
|
-
db.run("CREATE TRIGGER publication_serving_authority_no_update BEFORE UPDATE OF mutation_id,generation,attempt_key,workers_json ON configuration_publication_serving_sets BEGIN SELECT RAISE(ABORT,'publication serving attempt identity is immutable'); END");
|
|
9681
|
-
db.run("CREATE TRIGGER publication_serving_authority_no_replace BEFORE INSERT ON configuration_publication_serving_sets WHEN EXISTS(SELECT 1 FROM configuration_publication_serving_sets WHERE mutation_id=NEW.mutation_id AND generation=NEW.generation AND attempt_key=NEW.attempt_key) BEGIN SELECT RAISE(ABORT,'publication serving authority history is immutable'); END");
|
|
9682
|
-
db.run(`CREATE TABLE runtime_recovery_budget (
|
|
9683
|
-
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
9684
|
-
tickets INTEGER NOT NULL CHECK(tickets BETWEEN 0 AND 2),
|
|
9685
|
-
ticket_id TEXT,
|
|
9686
|
-
ticket_boot_id TEXT,
|
|
9687
|
-
retry_at_ns TEXT,
|
|
9688
|
-
healthy_owner TEXT,
|
|
9689
|
-
healthy_boot_id TEXT,
|
|
9690
|
-
healthy_since_ns TEXT,
|
|
9691
|
-
healthy_last_ns TEXT,
|
|
9692
|
-
CHECK((tickets=0 AND ticket_id IS NULL AND ticket_boot_id IS NULL AND retry_at_ns IS NULL) OR
|
|
9693
|
-
(tickets>0 AND ticket_id IS NOT NULL AND ticket_boot_id IS NOT NULL AND retry_at_ns IS NOT NULL)),
|
|
9694
|
-
CHECK((healthy_owner IS NULL AND healthy_boot_id IS NULL AND healthy_since_ns IS NULL AND healthy_last_ns IS NULL) OR
|
|
9695
|
-
(healthy_owner IS NOT NULL AND healthy_boot_id IS NOT NULL AND healthy_since_ns IS NOT NULL AND healthy_last_ns IS NOT NULL))
|
|
9696
|
-
) STRICT`);
|
|
9697
|
-
db.run("INSERT INTO runtime_recovery_budget(id,tickets) VALUES(1,0)");
|
|
9698
|
-
db.run(`CREATE TABLE configuration_retirement_retry_requests (
|
|
9699
|
-
request_id TEXT PRIMARY KEY,
|
|
9700
|
-
mutation_id TEXT NOT NULL REFERENCES configuration_retirements(mutation_id),
|
|
9701
|
-
expected_revision INTEGER NOT NULL CHECK(expected_revision>0),
|
|
9702
|
-
created_at INTEGER NOT NULL CHECK(created_at>=0)
|
|
9703
|
-
) STRICT, WITHOUT ROWID`);
|
|
9704
|
-
db.run("CREATE TRIGGER retirement_retry_no_delete BEFORE DELETE ON configuration_retirement_retry_requests BEGIN SELECT RAISE(ABORT,'retirement retry identity is immutable'); END");
|
|
9705
|
-
db.run("CREATE TRIGGER retirement_retry_no_update BEFORE UPDATE ON configuration_retirement_retry_requests BEGIN SELECT RAISE(ABORT,'retirement retry identity is immutable'); END");
|
|
9706
|
-
db.run("CREATE TRIGGER retirement_retry_no_replace BEFORE INSERT ON configuration_retirement_retry_requests WHEN EXISTS(SELECT 1 FROM configuration_retirement_retry_requests WHERE request_id=NEW.request_id) BEGIN SELECT RAISE(ABORT,'retirement retry identity is immutable'); END");
|
|
9707
|
-
db.run(`CREATE TABLE runtime_recovery_tickets (
|
|
9708
|
-
ticket_id TEXT PRIMARY KEY,
|
|
9709
|
-
owner TEXT NOT NULL,
|
|
9710
|
-
boot_id TEXT NOT NULL,
|
|
9711
|
-
consumed_at_ns TEXT NOT NULL,
|
|
9712
|
-
retry_at_ns TEXT NOT NULL
|
|
9713
|
-
) STRICT, WITHOUT ROWID`);
|
|
9714
|
-
db.run("CREATE TRIGGER runtime_recovery_tickets_no_delete BEFORE DELETE ON runtime_recovery_tickets BEGIN SELECT RAISE(ABORT,'runtime recovery ticket history is immutable'); END");
|
|
9715
|
-
db.run("CREATE TRIGGER runtime_recovery_tickets_no_update BEFORE UPDATE ON runtime_recovery_tickets BEGIN SELECT RAISE(ABORT,'runtime recovery ticket history is immutable'); END");
|
|
9716
|
-
db.run("CREATE TRIGGER runtime_recovery_tickets_no_replace BEFORE INSERT ON runtime_recovery_tickets WHEN EXISTS(SELECT 1 FROM runtime_recovery_tickets WHERE ticket_id=NEW.ticket_id) BEGIN SELECT RAISE(ABORT,'runtime recovery ticket history is immutable'); END");
|
|
9717
|
-
db.run("CREATE TRIGGER runtime_recovery_budget_no_delete BEFORE DELETE ON runtime_recovery_budget BEGIN SELECT RAISE(ABORT,'runtime recovery budget cannot be deleted'); END");
|
|
9718
|
-
db.run("CREATE TRIGGER runtime_recovery_budget_no_replace BEFORE INSERT ON runtime_recovery_budget WHEN EXISTS(SELECT 1 FROM runtime_recovery_budget) BEGIN SELECT RAISE(ABORT,'runtime recovery budget cannot be replaced'); END");
|
|
9719
|
-
db.run("CREATE TRIGGER runtime_recovery_budget_fixed_identity BEFORE UPDATE OF id ON runtime_recovery_budget BEGIN SELECT RAISE(ABORT,'runtime recovery budget identity is immutable'); END");
|
|
9537
|
+
for (const [index, revision] of revisions.entries()) {
|
|
9538
|
+
requireSafeInteger(revision.revision, "configuration_revisions.revision", 1);
|
|
9539
|
+
requireSafeInteger(revision.created_at, "configuration_revisions.created_at");
|
|
9540
|
+
if (revision.revision !== index + 1 || !validateDigest(revision.content_hash) || revision.kind !== "config" && revision.kind !== "admin_state") {
|
|
9541
|
+
throw new ConfigRepositoryError("schema_corrupt", "configuration revision metadata is invalid");
|
|
9542
|
+
}
|
|
9543
|
+
}
|
|
9544
|
+
const initialRevision = revisions[0];
|
|
9545
|
+
if (initialRevision === undefined || initialRevision.content_hash !== INITIAL_REVISION_HASH || initialRevision.kind !== "config" || initialRevision.created_at !== 0) {
|
|
9546
|
+
throw new ConfigRepositoryError("schema_corrupt", "initial revision identity is invalid");
|
|
9547
|
+
}
|
|
9548
|
+
const operations = readAllOperations(db);
|
|
9549
|
+
if (operations.length !== revisions.length - 1) {
|
|
9550
|
+
throw new ConfigRepositoryError("schema_corrupt", "revision operation cardinality is invalid");
|
|
9720
9551
|
}
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
// src/config-storage/schema-fingerprint.ts
|
|
9728
|
-
function readSchemaObjectFingerprint(db) {
|
|
9729
|
-
return canonicalJson(sqliteAll(db, `SELECT type,name,tbl_name,sql
|
|
9730
|
-
FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type,name`));
|
|
9731
|
-
}
|
|
9732
|
-
var expectedDescriptors = new Map;
|
|
9733
|
-
function verifySchemaFingerprint(db, version = CONFIG_SCHEMA_VERSION, plan = CONFIG_MIGRATIONS) {
|
|
9734
|
-
if (!plan.some((migration) => migration.version === version)) {
|
|
9735
|
-
throw new ConfigRepositoryError("schema_corrupt", "unsupported configuration schema version");
|
|
9552
|
+
const operationsByRevision = new Map(operations.map((operation) => [operation.committed_revision, operation]));
|
|
9553
|
+
for (const revision of revisions.slice(1)) {
|
|
9554
|
+
const operation = operationsByRevision.get(revision.revision);
|
|
9555
|
+
if (operation === undefined || operation.expected_revision !== revision.revision - 1 || operation.kind !== revision.kind || operation.created_at !== revision.created_at) {
|
|
9556
|
+
throw new ConfigRepositoryError("schema_corrupt", "revision operation metadata is incoherent");
|
|
9557
|
+
}
|
|
9736
9558
|
}
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
expected = databaseSchemaDescriptor(reference);
|
|
9744
|
-
} finally {
|
|
9745
|
-
reference.close(true);
|
|
9559
|
+
const operationsByMutation = new Map(operations.map((operation) => [operation.mutation_id, operation]));
|
|
9560
|
+
const workersByMutation = new Map;
|
|
9561
|
+
for (const worker of readAllWorkers(db)) {
|
|
9562
|
+
const operation = operationsByMutation.get(worker.mutation_id);
|
|
9563
|
+
if (operation === undefined || worker.target_revision !== operation.committed_revision || worker.updated_at < operation.created_at || worker.updated_at > operation.updated_at) {
|
|
9564
|
+
throw new ConfigRepositoryError("schema_corrupt", "operation worker metadata is incoherent");
|
|
9746
9565
|
}
|
|
9747
|
-
|
|
9748
|
-
expectedDescriptors.set(version, expected);
|
|
9566
|
+
workersByMutation.set(worker.mutation_id, [...workersByMutation.get(worker.mutation_id) ?? [], worker]);
|
|
9749
9567
|
}
|
|
9750
|
-
|
|
9751
|
-
|
|
9568
|
+
for (const operation of operations) {
|
|
9569
|
+
const workers = workersByMutation.get(operation.mutation_id) ?? [];
|
|
9570
|
+
validateOperationRelations(operation, workers);
|
|
9571
|
+
if (operation.committed_revision !== state.active_revision && (operation.state === "committed" || operation.state === "publishing" || operation.state === "draining")) {
|
|
9572
|
+
throw new ConfigRepositoryError("schema_corrupt", "non-active operation is not terminal");
|
|
9573
|
+
}
|
|
9752
9574
|
}
|
|
9575
|
+
const activeRevisionRow = revisions.at(-1);
|
|
9576
|
+
if (activeRevisionRow === undefined)
|
|
9577
|
+
throw new ConfigRepositoryError("schema_corrupt", "active revision is missing");
|
|
9578
|
+
const activeOperation = operationsByRevision.get(state.active_revision) ?? null;
|
|
9579
|
+
const activeTargetSlots = activeOperation === null ? [] : (workersByMutation.get(activeOperation.mutation_id) ?? []).map(({ worker_slot }) => worker_slot).sort((left, right) => left - right);
|
|
9580
|
+
return {
|
|
9581
|
+
activeRevision: state.active_revision,
|
|
9582
|
+
activeRevisionRow,
|
|
9583
|
+
activeOperation,
|
|
9584
|
+
activeTargetSlots,
|
|
9585
|
+
managementIdentity: activeOperation === null ? null : readManagementRequestIdentity(db, activeOperation.mutation_id)
|
|
9586
|
+
};
|
|
9753
9587
|
}
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
return new ConfigRepositoryError("repository_failure", message, cause);
|
|
9768
|
-
}
|
|
9769
|
-
|
|
9770
|
-
// src/config-storage/migrations/index.ts
|
|
9771
|
-
function migrateConfigurationDatabase(db, plan = CONFIG_MIGRATIONS) {
|
|
9772
|
-
try {
|
|
9773
|
-
db.transaction(() => {
|
|
9774
|
-
if (!plan.length || plan[0] !== CONFIG_MIGRATIONS[0] || plan.some((m, i) => i > 0 && m.version !== plan[i - 1].version + 1) || new Set(plan.map((m) => m.name)).size !== plan.length)
|
|
9775
|
-
throw new ConfigRepositoryError("migration_failed", "invalid configuration migration plan");
|
|
9776
|
-
const count = db.query("SELECT count(*) AS count FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%'").get().count;
|
|
9777
|
-
let applied = [];
|
|
9778
|
-
if (count > 0) {
|
|
9779
|
-
applied = db.query("SELECT version,name FROM schema_migrations ORDER BY version").all();
|
|
9780
|
-
if (!applied.length || applied.length > plan.length || applied.some((r, i) => r.version !== plan[i]?.version || r.name !== plan[i]?.name))
|
|
9781
|
-
throw new ConfigRepositoryError("schema_corrupt", "configuration migration history is not an exact baseline prefix");
|
|
9782
|
-
verifySchemaFingerprint(db, applied.at(-1).version, plan);
|
|
9783
|
-
}
|
|
9784
|
-
for (const migration of plan.slice(applied.length)) {
|
|
9785
|
-
migration.up(db);
|
|
9786
|
-
db.run("INSERT INTO schema_migrations(version,name) VALUES(?,?)", [migration.version, migration.name]);
|
|
9787
|
-
}
|
|
9788
|
-
verifySchemaFingerprint(db, plan.at(-1).version, plan);
|
|
9789
|
-
}).immediate();
|
|
9790
|
-
} catch (error) {
|
|
9791
|
-
if (error instanceof ConfigRepositoryError)
|
|
9792
|
-
throw error;
|
|
9793
|
-
if (isSqliteBusyError(error))
|
|
9794
|
-
throw repositoryFailure("configuration migration was blocked by SQLite", error);
|
|
9795
|
-
throw new ConfigRepositoryError("migration_failed", "configuration migration failed", error);
|
|
9588
|
+
function verifyActiveRequestIdentity(audited, aggregate) {
|
|
9589
|
+
const operation = audited.activeOperation;
|
|
9590
|
+
if (operation === null)
|
|
9591
|
+
return;
|
|
9592
|
+
const requestHash = hashConfigurationRequest({
|
|
9593
|
+
...audited.managementIdentity,
|
|
9594
|
+
kind: operation.kind,
|
|
9595
|
+
expected_revision: operation.expected_revision,
|
|
9596
|
+
aggregate,
|
|
9597
|
+
target_worker_slots: audited.activeTargetSlots
|
|
9598
|
+
});
|
|
9599
|
+
if (requestHash !== operation.request_hash) {
|
|
9600
|
+
throw new ConfigRepositoryError("schema_corrupt", "active operation request hash is incoherent");
|
|
9796
9601
|
}
|
|
9797
9602
|
}
|
|
9798
9603
|
|
|
@@ -9856,241 +9661,643 @@ function prepareCommitCommand(input, compileOptions) {
|
|
|
9856
9661
|
};
|
|
9857
9662
|
}
|
|
9858
9663
|
|
|
9859
|
-
// src/config-storage/
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
const
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
requireSafeInteger(revision.created_at, "configuration_revisions.created_at");
|
|
9883
|
-
if (revision.revision !== index + 1 || !validateDigest(revision.content_hash) || revision.kind !== "config" && revision.kind !== "admin_state") {
|
|
9884
|
-
throw new ConfigRepositoryError("schema_corrupt", "configuration revision metadata is invalid");
|
|
9885
|
-
}
|
|
9664
|
+
// src/config-storage/consistent-read.ts
|
|
9665
|
+
function withConsistentRead(db, fn) {
|
|
9666
|
+
if (db.inTransaction)
|
|
9667
|
+
return fn();
|
|
9668
|
+
return db.transaction(fn).deferred();
|
|
9669
|
+
}
|
|
9670
|
+
|
|
9671
|
+
// src/config-storage/serving-snapshot.ts
|
|
9672
|
+
var MAX_AGGREGATE_BYTES = 1048576;
|
|
9673
|
+
function invalid4(message) {
|
|
9674
|
+
throw new ConfigRepositoryError("invalid_configuration", message);
|
|
9675
|
+
}
|
|
9676
|
+
function corrupt3(message) {
|
|
9677
|
+
throw new ConfigRepositoryError("serving_snapshot_corrupt", message);
|
|
9678
|
+
}
|
|
9679
|
+
function canonicalAggregate(aggregate) {
|
|
9680
|
+
const result = parseNormalizeCompileAggregate(aggregate, undefined);
|
|
9681
|
+
if (!result.ok)
|
|
9682
|
+
invalid4("configuration aggregate is invalid");
|
|
9683
|
+
const aggregateJson = canonicalJson(result.value);
|
|
9684
|
+
const bytes = new TextEncoder().encode(aggregateJson).byteLength;
|
|
9685
|
+
if (bytes < 2 || bytes > MAX_AGGREGATE_BYTES) {
|
|
9686
|
+
invalid4("configuration aggregate is too large");
|
|
9886
9687
|
}
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9688
|
+
return { aggregateJson, contentHash: hashConfigurationContent(result.value) };
|
|
9689
|
+
}
|
|
9690
|
+
function appendServingSnapshot(db, snapshot, pluginCatalogHash) {
|
|
9691
|
+
if (!Number.isSafeInteger(snapshot?.revision) || snapshot.revision <= 0 || typeof snapshot.content_hash !== "string" || !validateDigest(snapshot.content_hash) || !validateDigest(pluginCatalogHash)) {
|
|
9692
|
+
invalid4("serving snapshot identity is invalid");
|
|
9890
9693
|
}
|
|
9891
|
-
const
|
|
9892
|
-
if (
|
|
9893
|
-
|
|
9694
|
+
const normalized = canonicalAggregate(snapshot.aggregate);
|
|
9695
|
+
if (normalized.contentHash !== snapshot.content_hash) {
|
|
9696
|
+
invalid4("configuration aggregate content hash does not match");
|
|
9894
9697
|
}
|
|
9895
|
-
const
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
throw new ConfigRepositoryError("schema_corrupt", "revision operation metadata is incoherent");
|
|
9698
|
+
const transaction = db.transaction(() => {
|
|
9699
|
+
const revision = sqliteGet(db, "SELECT revision,content_hash FROM configuration_revisions WHERE revision=?", snapshot.revision);
|
|
9700
|
+
if (revision === null || revision.content_hash !== snapshot.content_hash) {
|
|
9701
|
+
invalid4("serving snapshot revision does not match configuration history");
|
|
9900
9702
|
}
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9703
|
+
const existing = sqliteGet(db, `SELECT revision,content_hash,plugin_catalog_hash,aggregate_json
|
|
9704
|
+
FROM configuration_serving_snapshots
|
|
9705
|
+
WHERE revision=? AND content_hash=? AND plugin_catalog_hash=?`, snapshot.revision, snapshot.content_hash, pluginCatalogHash);
|
|
9706
|
+
if (existing !== null) {
|
|
9707
|
+
if (existing.aggregate_json !== normalized.aggregateJson) {
|
|
9708
|
+
corrupt3("configuration serving snapshot is immutable and inconsistent");
|
|
9709
|
+
}
|
|
9710
|
+
return;
|
|
9908
9711
|
}
|
|
9909
|
-
|
|
9712
|
+
db.run(`INSERT INTO configuration_serving_snapshots
|
|
9713
|
+
(revision,content_hash,plugin_catalog_hash,aggregate_json) VALUES (?,?,?,?)`, [snapshot.revision, snapshot.content_hash, pluginCatalogHash, normalized.aggregateJson]);
|
|
9714
|
+
});
|
|
9715
|
+
transaction.immediate();
|
|
9716
|
+
}
|
|
9717
|
+
function getServingSnapshot(db, key) {
|
|
9718
|
+
if (!Number.isSafeInteger(key?.revision) || key.revision <= 0 || typeof key.content_hash !== "string" || !validateDigest(key.content_hash) || typeof key.plugin_catalog_hash !== "string" || !validateDigest(key.plugin_catalog_hash)) {
|
|
9719
|
+
invalid4("serving snapshot lookup identity is invalid");
|
|
9910
9720
|
}
|
|
9911
|
-
|
|
9912
|
-
const
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9721
|
+
return withConsistentRead(db, () => {
|
|
9722
|
+
const row = sqliteGet(db, `SELECT revision,content_hash,plugin_catalog_hash,aggregate_json
|
|
9723
|
+
FROM configuration_serving_snapshots
|
|
9724
|
+
WHERE revision=? AND content_hash=? AND plugin_catalog_hash=?`, key.revision, key.content_hash, key.plugin_catalog_hash);
|
|
9725
|
+
if (row === null)
|
|
9726
|
+
return null;
|
|
9727
|
+
if (row.revision !== key.revision || row.content_hash !== key.content_hash || row.plugin_catalog_hash !== key.plugin_catalog_hash || !Number.isSafeInteger(row.revision) || row.revision <= 0 || !validateDigest(row.content_hash) || !validateDigest(row.plugin_catalog_hash)) {
|
|
9728
|
+
corrupt3("configuration serving snapshot identity is invalid");
|
|
9729
|
+
}
|
|
9730
|
+
const bytes = new TextEncoder().encode(row.aggregate_json).byteLength;
|
|
9731
|
+
if (bytes < 2 || bytes > MAX_AGGREGATE_BYTES)
|
|
9732
|
+
corrupt3("configuration serving snapshot size is invalid");
|
|
9733
|
+
let parsed;
|
|
9734
|
+
try {
|
|
9735
|
+
parsed = JSON.parse(row.aggregate_json);
|
|
9736
|
+
} catch {
|
|
9737
|
+
corrupt3("configuration serving snapshot JSON is invalid");
|
|
9738
|
+
}
|
|
9739
|
+
let canonical;
|
|
9740
|
+
try {
|
|
9741
|
+
canonical = canonicalJson(parsed);
|
|
9742
|
+
} catch {
|
|
9743
|
+
corrupt3("configuration serving snapshot JSON is invalid");
|
|
9744
|
+
}
|
|
9745
|
+
if (canonical !== row.aggregate_json)
|
|
9746
|
+
corrupt3("configuration serving snapshot JSON is not canonical");
|
|
9747
|
+
const normalized = parseNormalizeCompileAggregate(parsed, undefined);
|
|
9748
|
+
if (!normalized.ok) {
|
|
9749
|
+
if (normalized.errors.some((error) => error.code === "unknown_field" && /^logical_configuration\.routes\[\d+\]\.(service_id|service)$/.test(error.path))) {
|
|
9750
|
+
throw new ConfigRepositoryError("invalid_configuration", "historical serving snapshot does not use the current configuration schema and cannot be loaded");
|
|
9751
|
+
}
|
|
9752
|
+
corrupt3("configuration serving snapshot aggregate is invalid");
|
|
9753
|
+
}
|
|
9754
|
+
if (canonicalJson(normalized.value) !== row.aggregate_json) {
|
|
9755
|
+
corrupt3("configuration serving snapshot aggregate normalization drifted");
|
|
9756
|
+
}
|
|
9757
|
+
if (hashConfigurationContent(normalized.value) !== key.content_hash) {
|
|
9758
|
+
corrupt3("configuration serving snapshot content hash is invalid");
|
|
9759
|
+
}
|
|
9760
|
+
const revision = sqliteGet(db, "SELECT revision,content_hash FROM configuration_revisions WHERE revision=?", key.revision);
|
|
9761
|
+
if (revision === null || revision.revision !== key.revision || revision.content_hash !== key.content_hash) {
|
|
9762
|
+
corrupt3("configuration serving snapshot revision is invalid");
|
|
9763
|
+
}
|
|
9764
|
+
return { revision: row.revision, content_hash: row.content_hash, aggregate: normalized.value };
|
|
9765
|
+
});
|
|
9766
|
+
}
|
|
9767
|
+
function auditServingSnapshotHistory(db) {
|
|
9768
|
+
const rows = db.query(`SELECT s.revision,s.content_hash,s.plugin_catalog_hash,s.aggregate_json,
|
|
9769
|
+
r.content_hash AS revision_hash FROM configuration_serving_snapshots s
|
|
9770
|
+
LEFT JOIN configuration_revisions r ON r.revision=s.revision`).all();
|
|
9771
|
+
for (const row of rows) {
|
|
9772
|
+
if (row.revision_hash !== row.content_hash || !validateDigest(row.content_hash) || !validateDigest(row.plugin_catalog_hash)) {
|
|
9773
|
+
corrupt3("historical serving snapshot identity is invalid");
|
|
9774
|
+
}
|
|
9775
|
+
try {
|
|
9776
|
+
const parsed = JSON.parse(row.aggregate_json);
|
|
9777
|
+
if (new TextEncoder().encode(row.aggregate_json).byteLength > MAX_AGGREGATE_BYTES || canonicalJson(parsed) !== row.aggregate_json || hashConfigurationContent(parsed) !== row.content_hash)
|
|
9778
|
+
corrupt3("historical serving snapshot content is invalid");
|
|
9779
|
+
} catch (error) {
|
|
9780
|
+
if (error instanceof ConfigRepositoryError)
|
|
9781
|
+
throw error;
|
|
9782
|
+
corrupt3("historical serving snapshot JSON is invalid");
|
|
9916
9783
|
}
|
|
9917
9784
|
}
|
|
9918
|
-
const activeRevisionRow = revisions.at(-1);
|
|
9919
|
-
if (activeRevisionRow === undefined)
|
|
9920
|
-
throw new ConfigRepositoryError("schema_corrupt", "active revision is missing");
|
|
9921
|
-
const activeOperation = operationsByRevision.get(state.active_revision) ?? null;
|
|
9922
|
-
const activeTargetSlots = activeOperation === null ? [] : (workersByMutation.get(activeOperation.mutation_id) ?? []).map(({ worker_slot }) => worker_slot).sort((left, right) => left - right);
|
|
9923
|
-
return {
|
|
9924
|
-
activeRevision: state.active_revision,
|
|
9925
|
-
activeRevisionRow,
|
|
9926
|
-
activeOperation,
|
|
9927
|
-
activeTargetSlots,
|
|
9928
|
-
managementIdentity: activeOperation === null ? null : readManagementRequestIdentity(db, activeOperation.mutation_id)
|
|
9929
|
-
};
|
|
9930
9785
|
}
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9786
|
+
|
|
9787
|
+
// src/config-storage/schema-fingerprint.ts
|
|
9788
|
+
import { Database } from "bun:sqlite";
|
|
9789
|
+
|
|
9790
|
+
// src/database-schema.ts
|
|
9791
|
+
function pragmaName(name) {
|
|
9792
|
+
return `'${name.replaceAll("'", "''")}'`;
|
|
9793
|
+
}
|
|
9794
|
+
function normalizedSql(sql) {
|
|
9795
|
+
return (sql ?? "").match(/'(?:''|[^'])*'|"(?:""|[^"])*"|`[^`]*`|\[[^\]]*\]|[^'"`\[]+/g)?.map((token) => /^['"`\[]/.test(token) ? token : token.replace(/\s+/g, " ")).join("").trim() ?? "";
|
|
9796
|
+
}
|
|
9797
|
+
function indexDescriptor(db, table) {
|
|
9798
|
+
const indexes = sqliteAll(db, 'SELECT name,"unique",origin,partial FROM pragma_index_list(?)', table);
|
|
9799
|
+
return indexes.map((index) => ({
|
|
9800
|
+
unique: index.unique,
|
|
9801
|
+
origin: index.origin,
|
|
9802
|
+
partial: index.partial,
|
|
9803
|
+
columns: sqliteAll(db, `PRAGMA index_xinfo(${pragmaName(index.name)})`).map(({ seqno, cid, name, desc, coll, key }) => ({ seqno, cid, name, desc, coll, key }))
|
|
9804
|
+
})).sort((left, right) => canonicalJson(left).localeCompare(canonicalJson(right)));
|
|
9805
|
+
}
|
|
9806
|
+
function databaseSchemaDescriptor(db) {
|
|
9807
|
+
const definitions = new Map(sqliteAll(db, `SELECT name,sql FROM sqlite_schema
|
|
9808
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'`).map((row) => [row.name, row.sql]));
|
|
9809
|
+
const tables = sqliteAll(db, `SELECT name,type,ncol,wr,strict FROM pragma_table_list
|
|
9810
|
+
WHERE schema='main' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
9811
|
+
const objects = sqliteAll(db, `SELECT type,name,tbl_name,sql
|
|
9812
|
+
FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type,name`).map((row) => ({
|
|
9813
|
+
type: row.type,
|
|
9814
|
+
name: row.name,
|
|
9815
|
+
table: row.tbl_name,
|
|
9816
|
+
sql: normalizedSql(row.sql)
|
|
9817
|
+
}));
|
|
9818
|
+
return canonicalJson({
|
|
9819
|
+
objects,
|
|
9820
|
+
tables: tables.map((table) => ({
|
|
9821
|
+
name: table.name,
|
|
9822
|
+
type: table.type,
|
|
9823
|
+
ncol: table.ncol,
|
|
9824
|
+
wr: table.wr,
|
|
9825
|
+
strict: table.strict,
|
|
9826
|
+
sql: normalizedSql(definitions.get(table.name) ?? null),
|
|
9827
|
+
columns: sqliteAll(db, `SELECT cid,name,type,"notnull",dflt_value,pk,hidden
|
|
9828
|
+
FROM pragma_table_xinfo(?) ORDER BY cid`, table.name),
|
|
9829
|
+
foreignKeys: sqliteAll(db, `SELECT id,seq,"table","from","to",on_update,on_delete,"match"
|
|
9830
|
+
FROM pragma_foreign_key_list(?) ORDER BY id,seq`, table.name),
|
|
9831
|
+
indexes: indexDescriptor(db, table.name)
|
|
9832
|
+
}))
|
|
9941
9833
|
});
|
|
9942
|
-
if (requestHash !== operation.request_hash) {
|
|
9943
|
-
throw new ConfigRepositoryError("schema_corrupt", "active operation request hash is incoherent");
|
|
9944
|
-
}
|
|
9945
9834
|
}
|
|
9946
9835
|
|
|
9947
|
-
// src/config-storage/
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9836
|
+
// src/config-storage/migrations/service-references.ts
|
|
9837
|
+
var SERVICE_REFERENCES_MIGRATION = {
|
|
9838
|
+
version: 5,
|
|
9839
|
+
name: "service_references",
|
|
9840
|
+
up(db) {
|
|
9841
|
+
db.run(`CREATE TABLE service_references (
|
|
9842
|
+
id TEXT PRIMARY KEY CHECK(length(id)=36 AND substr(id,9,1)='-' AND substr(id,14,1)='-' AND substr(id,19,1)='-' AND substr(id,24,1)='-' AND replace(id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
9843
|
+
owner_kind TEXT NOT NULL CHECK(owner_kind IN ('service','route')),
|
|
9844
|
+
owner_service_id TEXT REFERENCES services(id) ON DELETE CASCADE,
|
|
9845
|
+
route_id TEXT REFERENCES routes(id) ON DELETE CASCADE,
|
|
9846
|
+
position INTEGER NOT NULL CHECK(position BETWEEN 0 AND 9007199254740991),
|
|
9847
|
+
service_id TEXT NOT NULL REFERENCES services(id),
|
|
9848
|
+
weight REAL NOT NULL CHECK(weight>0),
|
|
9849
|
+
priority REAL NOT NULL CHECK(priority>0),
|
|
9850
|
+
is_disabled INTEGER NOT NULL CHECK(is_disabled IN (0,1)),
|
|
9851
|
+
CHECK((owner_kind='service' AND owner_service_id IS NOT NULL AND route_id IS NULL AND owner_service_id<>service_id)
|
|
9852
|
+
OR (owner_kind='route' AND route_id IS NOT NULL AND owner_service_id IS NULL)),
|
|
9853
|
+
UNIQUE(owner_service_id,position), UNIQUE(route_id,position),
|
|
9854
|
+
UNIQUE(owner_service_id,service_id), UNIQUE(route_id,service_id)
|
|
9855
|
+
) STRICT`);
|
|
9856
|
+
db.run(`INSERT INTO service_references(id,owner_kind,owner_service_id,route_id,position,service_id,weight,priority,is_disabled)
|
|
9857
|
+
SELECT lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))),2)
|
|
9858
|
+
|| '-8' || substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
|
|
9859
|
+
'route',NULL,id,0,service_id,100,1,0 FROM routes WHERE service_id IS NOT NULL`);
|
|
9860
|
+
db.run("ALTER TABLE routes DROP COLUMN service_id");
|
|
9861
|
+
}
|
|
9862
|
+
};
|
|
9863
|
+
|
|
9864
|
+
// src/config-storage/migrations/management-protection.ts
|
|
9865
|
+
var MANAGEMENT_PROTECTION_MIGRATION = {
|
|
9866
|
+
version: 4,
|
|
9867
|
+
name: "management_protection",
|
|
9868
|
+
up(db) {
|
|
9869
|
+
db.run(`CREATE TABLE management_protection (
|
|
9870
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
9871
|
+
required INTEGER NOT NULL CHECK(required IN (0,1)),
|
|
9872
|
+
selected_provider TEXT REFERENCES plugin_activations(plugin_name) DEFERRABLE INITIALLY DEFERRED,
|
|
9873
|
+
generation INTEGER NOT NULL CHECK(generation BETWEEN 0 AND 9007199254740991),
|
|
9874
|
+
CHECK((required=0 AND selected_provider IS NULL) OR (required=1 AND selected_provider IS NOT NULL))
|
|
9875
|
+
) STRICT`);
|
|
9876
|
+
db.run("INSERT INTO management_protection(id,required,selected_provider,generation) VALUES(1,0,NULL,0)");
|
|
9877
|
+
db.run("CREATE TRIGGER management_protection_no_delete BEFORE DELETE ON management_protection BEGIN SELECT RAISE(ABORT,'management protection cannot be deleted'); END");
|
|
9878
|
+
db.run("CREATE TRIGGER management_protection_no_replace BEFORE INSERT ON management_protection WHEN EXISTS(SELECT 1 FROM management_protection) BEGIN SELECT RAISE(ABORT,'management protection cannot be replaced'); END");
|
|
9879
|
+
db.run("CREATE TRIGGER management_protection_fixed_identity BEFORE UPDATE OF id ON management_protection BEGIN SELECT RAISE(ABORT,'management protection identity is immutable'); END");
|
|
9880
|
+
db.run("ALTER TABLE configuration_operations ADD COLUMN management_provider TEXT");
|
|
9881
|
+
db.run("ALTER TABLE configuration_operations ADD COLUMN offline_management INTEGER NOT NULL DEFAULT 0 CHECK(offline_management IN (0,1))");
|
|
9882
|
+
}
|
|
9883
|
+
};
|
|
9884
|
+
|
|
9885
|
+
// src/config-storage/migrations/ui-menus.ts
|
|
9886
|
+
var UI_MENUS_MIGRATION = {
|
|
9887
|
+
version: 3,
|
|
9888
|
+
name: "instance_ui_menus",
|
|
9889
|
+
up(db) {
|
|
9890
|
+
db.run(`CREATE TABLE ui_menu_layout (
|
|
9891
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
9892
|
+
revision INTEGER NOT NULL CHECK(revision >= 0 AND revision <= 9007199254740991),
|
|
9893
|
+
layout_json TEXT CHECK(layout_json IS NULL OR (json_valid(layout_json) AND json_type(layout_json)='array'
|
|
9894
|
+
AND length(CAST(layout_json AS BLOB)) <= 262144)),
|
|
9895
|
+
CHECK((revision=0 AND layout_json IS NULL) OR (revision>0 AND layout_json IS NOT NULL))
|
|
9896
|
+
) STRICT`);
|
|
9897
|
+
db.run("INSERT INTO ui_menu_layout(id,revision,layout_json) VALUES(1,0,NULL)");
|
|
9898
|
+
db.run("CREATE TRIGGER ui_menu_layout_no_delete BEFORE DELETE ON ui_menu_layout BEGIN SELECT RAISE(ABORT,'menu singleton cannot be deleted'); END");
|
|
9899
|
+
db.run("CREATE TRIGGER ui_menu_layout_no_replace BEFORE INSERT ON ui_menu_layout WHEN EXISTS(SELECT 1 FROM ui_menu_layout) BEGIN SELECT RAISE(ABORT,'menu singleton cannot be replaced'); END");
|
|
9900
|
+
db.run("CREATE TRIGGER ui_menu_layout_fixed_identity BEFORE UPDATE OF id ON ui_menu_layout BEGIN SELECT RAISE(ABORT,'menu singleton identity is immutable'); END");
|
|
9901
|
+
}
|
|
9902
|
+
};
|
|
9903
|
+
|
|
9904
|
+
// src/config-storage/migrations/baseline.ts
|
|
9905
|
+
import { randomUUID } from "crypto";
|
|
9906
|
+
|
|
9907
|
+
// src/config-storage/schema.ts
|
|
9908
|
+
var CONFIG_SCHEMA_STATEMENTS = [
|
|
9909
|
+
`CREATE TABLE configuration_operation_workers (
|
|
9910
|
+
mutation_id TEXT NOT NULL,
|
|
9911
|
+
worker_slot INTEGER NOT NULL CHECK(worker_slot >= 0 AND worker_slot <= 9007199254740991),
|
|
9912
|
+
target_revision INTEGER NOT NULL CHECK(target_revision > 0 AND target_revision <= 9007199254740991),
|
|
9913
|
+
drain_recovery_generation INTEGER NOT NULL DEFAULT 0
|
|
9914
|
+
CHECK(drain_recovery_generation >= 0 AND drain_recovery_generation <= 9007199254740991),
|
|
9915
|
+
attempt_no INTEGER NOT NULL DEFAULT 0 CHECK(attempt_no >= 0 AND attempt_no <= 9007199254740991),
|
|
9916
|
+
last_begin_previous_attempt_no INTEGER CHECK(last_begin_previous_attempt_no IS NULL OR
|
|
9917
|
+
(last_begin_previous_attempt_no >= 0 AND last_begin_previous_attempt_no < 9007199254740991)),
|
|
9918
|
+
last_begin_reason TEXT CHECK(last_begin_reason IS NULL OR last_begin_reason IN ('initial','retry','master_recovery')),
|
|
9919
|
+
state TEXT NOT NULL CHECK(state IN ('pending','converged','failed')),
|
|
9920
|
+
applied_revision INTEGER CHECK(applied_revision IS NULL OR (applied_revision > 0 AND applied_revision <= 9007199254740991)),
|
|
9921
|
+
last_error TEXT,
|
|
9922
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= 0 AND updated_at <= 9007199254740991),
|
|
9923
|
+
PRIMARY KEY(mutation_id,worker_slot),
|
|
9924
|
+
FOREIGN KEY(mutation_id,target_revision) REFERENCES configuration_operations(mutation_id,committed_revision) ON DELETE CASCADE,
|
|
9925
|
+
CHECK((attempt_no=0 AND last_begin_previous_attempt_no IS NULL AND last_begin_reason IS NULL) OR
|
|
9926
|
+
(attempt_no=1 AND last_begin_previous_attempt_no=0 AND last_begin_reason IN ('initial','master_recovery')) OR
|
|
9927
|
+
(attempt_no > 1 AND last_begin_previous_attempt_no=attempt_no-1 AND
|
|
9928
|
+
last_begin_reason IN ('retry','master_recovery'))),
|
|
9929
|
+
CHECK((state='pending' AND applied_revision IS NULL AND last_error IS NULL) OR
|
|
9930
|
+
(state='converged' AND attempt_no > 0 AND applied_revision=target_revision AND last_error IS NULL) OR
|
|
9931
|
+
(state='failed' AND attempt_no > 0 AND last_error IS NOT NULL AND
|
|
9932
|
+
length(last_error) <= 512 AND length(trim(last_error)) > 0))
|
|
9933
|
+
) STRICT, WITHOUT ROWID`,
|
|
9934
|
+
`CREATE TABLE configuration_operations (
|
|
9935
|
+
mutation_id TEXT PRIMARY KEY CHECK(length(mutation_id) BETWEEN 1 AND 128),
|
|
9936
|
+
request_hash TEXT NOT NULL CHECK(length(request_hash)=71 AND substr(request_hash,1,7)='sha256:' AND substr(request_hash,8) NOT GLOB '*[^0-9a-f]*'),
|
|
9937
|
+
expected_revision INTEGER NOT NULL CHECK(expected_revision > 0 AND expected_revision <= 9007199254740991),
|
|
9938
|
+
committed_revision INTEGER NOT NULL CHECK(committed_revision > 0 AND committed_revision <= 9007199254740991)
|
|
9939
|
+
REFERENCES configuration_revisions(revision),
|
|
9940
|
+
kind TEXT NOT NULL CHECK(kind IN ('config','admin_state')),
|
|
9941
|
+
target_worker_count INTEGER NOT NULL CHECK(target_worker_count >= 0 AND target_worker_count <= 9007199254740991),
|
|
9942
|
+
state TEXT NOT NULL CHECK(state IN ('committed','publishing','draining','converged','degraded')),
|
|
9943
|
+
result_status INTEGER,
|
|
9944
|
+
error_code TEXT,
|
|
9945
|
+
error_detail TEXT,
|
|
9946
|
+
drain_recovery_generation INTEGER NOT NULL DEFAULT 0
|
|
9947
|
+
CHECK(drain_recovery_generation >= 0 AND drain_recovery_generation <= 9007199254740991),
|
|
9948
|
+
last_drain_recovery_previous_generation INTEGER
|
|
9949
|
+
CHECK(last_drain_recovery_previous_generation IS NULL OR
|
|
9950
|
+
(last_drain_recovery_previous_generation >= 0 AND
|
|
9951
|
+
last_drain_recovery_previous_generation < 9007199254740991)),
|
|
9952
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0 AND created_at <= 9007199254740991),
|
|
9953
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at AND updated_at <= 9007199254740991),
|
|
9954
|
+
UNIQUE(mutation_id,committed_revision),
|
|
9955
|
+
CHECK((drain_recovery_generation=0 AND last_drain_recovery_previous_generation IS NULL) OR
|
|
9956
|
+
(drain_recovery_generation > 0 AND
|
|
9957
|
+
last_drain_recovery_previous_generation=drain_recovery_generation-1)),
|
|
9958
|
+
CHECK((state IN ('committed','publishing') AND drain_recovery_generation=0 AND
|
|
9959
|
+
result_status IS NULL AND error_code IS NULL AND error_detail IS NULL) OR
|
|
9960
|
+
(state='draining' AND result_status IS NULL AND error_code IS NULL AND error_detail IS NULL) OR
|
|
9961
|
+
(state='converged' AND drain_recovery_generation=0 AND
|
|
9962
|
+
result_status=200 AND error_code IS NULL AND error_detail IS NULL) OR
|
|
9963
|
+
(state='degraded' AND result_status=202 AND
|
|
9964
|
+
error_code IN ('replacement_convergence_failed','old_worker_drain_failed','control_readiness_failed') AND
|
|
9965
|
+
(error_code <> 'replacement_convergence_failed' OR drain_recovery_generation=0) AND
|
|
9966
|
+
error_detail IS NOT NULL AND length(error_detail) <= 512 AND length(trim(error_detail)) > 0))
|
|
9967
|
+
) STRICT`,
|
|
9968
|
+
`CREATE TABLE configuration_recoveries (
|
|
9969
|
+
recovery_sequence INTEGER PRIMARY KEY AUTOINCREMENT CHECK(recovery_sequence > 0),
|
|
9970
|
+
recovery_id TEXT NOT NULL UNIQUE CHECK(length(recovery_id)=36 AND substr(recovery_id,9,1)='-' AND substr(recovery_id,14,1)='-' AND substr(recovery_id,19,1)='-' AND substr(recovery_id,24,1)='-' AND replace(recovery_id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
9971
|
+
source_mutation_id TEXT NOT NULL,
|
|
9972
|
+
target_revision INTEGER NOT NULL CHECK(target_revision > 0 AND target_revision <= 9007199254740991),
|
|
9973
|
+
trigger TEXT NOT NULL CHECK(trigger IN ('automatic','manual')),
|
|
9974
|
+
state TEXT NOT NULL CHECK(state IN ('scheduled','running','succeeded','stopped')),
|
|
9975
|
+
attempt_count INTEGER NOT NULL CHECK(attempt_count >= 0 AND attempt_count <= 6),
|
|
9976
|
+
max_attempts INTEGER NOT NULL CHECK(max_attempts=6),
|
|
9977
|
+
next_retry_at INTEGER CHECK(next_retry_at IS NULL OR (next_retry_at >= 0 AND next_retry_at <= 9007199254740991)),
|
|
9978
|
+
final_reason_code TEXT CHECK(final_reason_code IS NULL OR final_reason_code IN
|
|
9979
|
+
('target_serving','already_serving','deterministic_worker_rejection','deterministic_protocol_failure',
|
|
9980
|
+
'deterministic_control_failure','retry_exhausted','revision_superseded','safety_outcome_unknown','fatal_source_failure')), final_reason_detail TEXT,
|
|
9981
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0 AND created_at <= 9007199254740991),
|
|
9982
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at AND updated_at <= 9007199254740991),
|
|
9983
|
+
FOREIGN KEY(source_mutation_id) REFERENCES configuration_operations(mutation_id),
|
|
9984
|
+
FOREIGN KEY(target_revision) REFERENCES configuration_revisions(revision),
|
|
9985
|
+
CHECK((state='scheduled' AND final_reason_code IS NULL AND final_reason_detail IS NULL) OR
|
|
9986
|
+
(state='running' AND next_retry_at IS NULL AND final_reason_code IS NULL AND final_reason_detail IS NULL) OR
|
|
9987
|
+
(state='succeeded' AND ((final_reason_code='already_serving' AND attempt_count=0) OR
|
|
9988
|
+
(final_reason_code='target_serving' AND attempt_count > 0)) AND next_retry_at IS NULL) OR
|
|
9989
|
+
(state='stopped' AND final_reason_code IN ('deterministic_worker_rejection','deterministic_protocol_failure',
|
|
9990
|
+
'deterministic_control_failure','revision_superseded') AND next_retry_at IS NULL) OR
|
|
9991
|
+
(state='stopped' AND final_reason_code='retry_exhausted' AND attempt_count=6 AND next_retry_at IS NULL) OR
|
|
9992
|
+
(state='stopped' AND final_reason_code='safety_outcome_unknown' AND attempt_count > 0 AND next_retry_at IS NULL) OR
|
|
9993
|
+
(state='stopped' AND final_reason_code='fatal_source_failure' AND attempt_count=0 AND next_retry_at IS NULL)),
|
|
9994
|
+
CHECK(state <> 'scheduled' OR next_retry_at IS NULL OR next_retry_at > updated_at),
|
|
9995
|
+
CHECK(state <> 'scheduled' OR attempt_count < 6), CHECK(state <> 'running' OR attempt_count > 0),
|
|
9996
|
+
CHECK(final_reason_detail IS NULL OR (length(final_reason_detail) <= 512 AND length(trim(final_reason_detail)) > 0))
|
|
9997
|
+
) STRICT`,
|
|
9998
|
+
`CREATE TABLE configuration_revisions (
|
|
9999
|
+
revision INTEGER PRIMARY KEY CHECK(revision > 0 AND revision <= 9007199254740991),
|
|
10000
|
+
content_hash TEXT NOT NULL CHECK(length(content_hash)=71 AND substr(content_hash,1,7)='sha256:' AND substr(content_hash,8) NOT GLOB '*[^0-9a-f]*'),
|
|
10001
|
+
kind TEXT NOT NULL CHECK(kind IN ('config','admin_state')),
|
|
10002
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0 AND created_at <= 9007199254740991)
|
|
10003
|
+
) STRICT`,
|
|
10004
|
+
`CREATE TABLE configuration_serving_snapshots (
|
|
10005
|
+
revision INTEGER NOT NULL CHECK(revision > 0 AND revision <= 9007199254740991),
|
|
10006
|
+
content_hash TEXT NOT NULL CHECK(length(content_hash)=71 AND substr(content_hash,1,7)='sha256:' AND substr(content_hash,8) NOT GLOB '*[^0-9a-f]*'),
|
|
10007
|
+
plugin_catalog_hash TEXT NOT NULL CHECK(length(plugin_catalog_hash)=71 AND substr(plugin_catalog_hash,1,7)='sha256:' AND substr(plugin_catalog_hash,8) NOT GLOB '*[^0-9a-f]*'),
|
|
10008
|
+
aggregate_json TEXT NOT NULL CHECK(json_valid(aggregate_json) AND json_type(aggregate_json)='object'
|
|
10009
|
+
AND length(CAST(aggregate_json AS BLOB)) BETWEEN 2 AND 1048576),
|
|
10010
|
+
PRIMARY KEY(revision,content_hash,plugin_catalog_hash),
|
|
10011
|
+
FOREIGN KEY(revision,content_hash)
|
|
10012
|
+
REFERENCES configuration_revisions(revision,content_hash)
|
|
10013
|
+
) STRICT, WITHOUT ROWID`,
|
|
10014
|
+
`CREATE TABLE configuration_state (
|
|
10015
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
10016
|
+
schema_version INTEGER NOT NULL CHECK(schema_version=4),
|
|
10017
|
+
active_revision INTEGER NOT NULL CHECK(active_revision > 0 AND active_revision <= 9007199254740991)
|
|
10018
|
+
REFERENCES configuration_revisions(revision),
|
|
10019
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0 AND created_at <= 9007199254740991),
|
|
10020
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at AND updated_at <= 9007199254740991)
|
|
10021
|
+
) STRICT`,
|
|
10022
|
+
`CREATE TABLE plugin_activations (
|
|
10023
|
+
plugin_name TEXT PRIMARY KEY CHECK(length(plugin_name) > 0 AND plugin_name=lower(plugin_name)
|
|
10024
|
+
AND plugin_name NOT GLOB '*[^a-z0-9-]*' AND substr(plugin_name,1,1) GLOB '[a-z]'
|
|
10025
|
+
AND substr(plugin_name,-1,1)<>'-' AND plugin_name NOT LIKE '%--%')
|
|
10026
|
+
) STRICT, WITHOUT ROWID`,
|
|
10027
|
+
`CREATE TABLE plugin_bindings (
|
|
10028
|
+
id TEXT PRIMARY KEY CHECK(length(id)=36 AND substr(id,9,1)='-' AND substr(id,14,1)='-' AND substr(id,19,1)='-' AND substr(id,24,1)='-' AND replace(id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
10029
|
+
scope_kind TEXT NOT NULL CHECK(scope_kind IN ('global','service','route','upstream')),
|
|
10030
|
+
scope_owner TEXT NOT NULL,
|
|
10031
|
+
service_id TEXT REFERENCES services(id) ON DELETE CASCADE,
|
|
10032
|
+
route_id TEXT REFERENCES routes(id) ON DELETE CASCADE,
|
|
10033
|
+
upstream_id TEXT REFERENCES upstreams(id) ON DELETE CASCADE,
|
|
10034
|
+
position INTEGER NOT NULL CHECK(position >= 0 AND position <= 9007199254740991),
|
|
10035
|
+
plugin_name TEXT NOT NULL CHECK(length(plugin_name) > 0 AND plugin_name=lower(plugin_name)
|
|
10036
|
+
AND plugin_name NOT GLOB '*[^a-z0-9-]*' AND substr(plugin_name,1,1) GLOB '[a-z]'
|
|
10037
|
+
AND substr(plugin_name,-1,1)<>'-' AND plugin_name NOT LIKE '%--%'),
|
|
10038
|
+
options_json TEXT CHECK(options_json IS NULL OR (json_valid(options_json) AND json_type(options_json)='object')),
|
|
10039
|
+
enabled INTEGER NOT NULL CHECK(enabled IN (0,1)),
|
|
10040
|
+
CHECK((scope_kind='global' AND scope_owner='' AND service_id IS NULL AND route_id IS NULL AND upstream_id IS NULL) OR
|
|
10041
|
+
(scope_kind='service' AND service_id IS NOT NULL AND scope_owner=service_id AND route_id IS NULL AND upstream_id IS NULL) OR
|
|
10042
|
+
(scope_kind='route' AND route_id IS NOT NULL AND scope_owner=route_id AND service_id IS NULL AND upstream_id IS NULL) OR
|
|
10043
|
+
(scope_kind='upstream' AND upstream_id IS NOT NULL AND scope_owner=upstream_id AND service_id IS NULL AND route_id IS NULL)),
|
|
10044
|
+
UNIQUE(scope_kind,scope_owner,position)
|
|
10045
|
+
) STRICT`,
|
|
10046
|
+
`CREATE TABLE routes (
|
|
10047
|
+
id TEXT PRIMARY KEY CHECK(length(id)=36 AND substr(id,9,1)='-' AND substr(id,14,1)='-' AND substr(id,19,1)='-' AND substr(id,24,1)='-' AND replace(id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
10048
|
+
position INTEGER NOT NULL UNIQUE CHECK(position >= 0 AND position <= 9007199254740991),
|
|
10049
|
+
path TEXT NOT NULL UNIQUE CHECK(substr(path,1,1)='/'),
|
|
10050
|
+
service_id TEXT REFERENCES services(id),
|
|
10051
|
+
policy_json TEXT NOT NULL CHECK(json_valid(policy_json) AND json_type(policy_json)='object')
|
|
10052
|
+
) STRICT`,
|
|
10053
|
+
`CREATE TABLE schema_migrations (
|
|
10054
|
+
version INTEGER PRIMARY KEY CHECK(version > 0 AND version <= 9007199254740991),
|
|
10055
|
+
name TEXT NOT NULL UNIQUE
|
|
10056
|
+
) STRICT`,
|
|
10057
|
+
`CREATE TABLE services (
|
|
10058
|
+
id TEXT PRIMARY KEY CHECK(length(id)=36 AND substr(id,9,1)='-' AND substr(id,14,1)='-' AND substr(id,19,1)='-' AND substr(id,24,1)='-' AND replace(id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
10059
|
+
position INTEGER NOT NULL UNIQUE CHECK(position >= 0 AND position <= 9007199254740991),
|
|
10060
|
+
name TEXT NOT NULL UNIQUE CHECK(length(name) > 0),
|
|
10061
|
+
policy_json TEXT NOT NULL CHECK(json_valid(policy_json) AND json_type(policy_json)='object')
|
|
10062
|
+
) STRICT`,
|
|
10063
|
+
`CREATE TABLE settings (
|
|
10064
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
10065
|
+
log_level TEXT CHECK(log_level IS NULL OR log_level IN ('trace','debug','info','warn','error','fatal')),
|
|
10066
|
+
body_parser_limit TEXT CHECK(body_parser_limit IS NULL OR CASE
|
|
10067
|
+
WHEN substr(body_parser_limit,-2) IN ('gb','mb','kb') THEN
|
|
10068
|
+
length(substr(body_parser_limit,1,length(body_parser_limit)-2)) > 0
|
|
10069
|
+
AND body_parser_limit=substr(body_parser_limit,1,length(body_parser_limit)-2)||substr(body_parser_limit,-2)
|
|
10070
|
+
AND substr(body_parser_limit,1,1) GLOB '[1-9]'
|
|
10071
|
+
AND substr(body_parser_limit,1,length(body_parser_limit)-2) NOT GLOB '*[^0-9]*'
|
|
10072
|
+
AND (length(substr(body_parser_limit,1,length(body_parser_limit)-2)) < 16 OR
|
|
10073
|
+
(length(substr(body_parser_limit,1,length(body_parser_limit)-2))=16 AND
|
|
10074
|
+
substr(body_parser_limit,1,length(body_parser_limit)-2)<='9007199254740991'))
|
|
10075
|
+
WHEN substr(body_parser_limit,-1)='b' THEN
|
|
10076
|
+
length(substr(body_parser_limit,1,length(body_parser_limit)-1)) > 0
|
|
10077
|
+
AND body_parser_limit=substr(body_parser_limit,1,length(body_parser_limit)-1)||'b'
|
|
10078
|
+
AND substr(body_parser_limit,1,1) GLOB '[1-9]'
|
|
10079
|
+
AND substr(body_parser_limit,1,length(body_parser_limit)-1) NOT GLOB '*[^0-9]*'
|
|
10080
|
+
AND (length(substr(body_parser_limit,1,length(body_parser_limit)-1)) < 16 OR
|
|
10081
|
+
(length(substr(body_parser_limit,1,length(body_parser_limit)-1))=16 AND
|
|
10082
|
+
substr(body_parser_limit,1,length(body_parser_limit)-1)<='9007199254740991'))
|
|
10083
|
+
ELSE 0 END),
|
|
10084
|
+
auth_json TEXT CHECK(auth_json IS NULL OR json_valid(auth_json)),
|
|
10085
|
+
logging_json TEXT CHECK(logging_json IS NULL OR json_valid(logging_json))
|
|
10086
|
+
, publication_json TEXT) STRICT`,
|
|
10087
|
+
`CREATE TABLE supervision_state (
|
|
10088
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
10089
|
+
instance_id TEXT NOT NULL UNIQUE CHECK(length(instance_id)=36),
|
|
10090
|
+
controller_epoch INTEGER NOT NULL CHECK(controller_epoch >= 0 AND controller_epoch < 9007199254740991),
|
|
10091
|
+
current_controller_id TEXT,
|
|
10092
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= 0 AND updated_at < 9007199254740991)
|
|
10093
|
+
) STRICT, WITHOUT ROWID`,
|
|
10094
|
+
`CREATE TABLE upstreams (
|
|
10095
|
+
id TEXT PRIMARY KEY CHECK(length(id)=36 AND substr(id,9,1)='-' AND substr(id,14,1)='-' AND substr(id,19,1)='-' AND substr(id,24,1)='-' AND replace(id,'-','') NOT GLOB '*[^0-9a-f]*'),
|
|
10096
|
+
owner_kind TEXT NOT NULL CHECK(owner_kind IN ('service','route')),
|
|
10097
|
+
service_id TEXT REFERENCES services(id) ON DELETE CASCADE,
|
|
10098
|
+
route_id TEXT REFERENCES routes(id) ON DELETE CASCADE,
|
|
10099
|
+
position INTEGER NOT NULL CHECK(position >= 0 AND position <= 9007199254740991),
|
|
10100
|
+
target TEXT NOT NULL CHECK(length(target) > 0),
|
|
10101
|
+
weight REAL NOT NULL CHECK(weight > 0),
|
|
10102
|
+
priority REAL NOT NULL CHECK(priority > 0),
|
|
10103
|
+
is_disabled INTEGER NOT NULL CHECK(is_disabled IN (0,1)),
|
|
10104
|
+
policy_json TEXT NOT NULL CHECK(json_valid(policy_json) AND json_type(policy_json)='object'),
|
|
10105
|
+
CHECK((owner_kind='service' AND service_id IS NOT NULL AND route_id IS NULL) OR
|
|
10106
|
+
(owner_kind='route' AND route_id IS NOT NULL AND service_id IS NULL)),
|
|
10107
|
+
UNIQUE(owner_kind,service_id,position), UNIQUE(owner_kind,route_id,position)
|
|
10108
|
+
) STRICT`,
|
|
10109
|
+
`CREATE UNIQUE INDEX configuration_recoveries_active_target_revision
|
|
10110
|
+
ON configuration_recoveries(target_revision) WHERE state IN ('scheduled','running')`,
|
|
10111
|
+
`CREATE UNIQUE INDEX configuration_revisions_revision_content_hash
|
|
10112
|
+
ON configuration_revisions(revision,content_hash)`,
|
|
10113
|
+
`CREATE TRIGGER configuration_operations_terminal_immutable_delete
|
|
10114
|
+
BEFORE DELETE ON configuration_operations
|
|
10115
|
+
WHEN OLD.state IN ('converged','degraded')
|
|
10116
|
+
BEGIN
|
|
10117
|
+
SELECT RAISE(ABORT,'terminal configuration operations are immutable');
|
|
10118
|
+
END`,
|
|
10119
|
+
`CREATE TRIGGER configuration_operations_terminal_immutable_update
|
|
10120
|
+
BEFORE UPDATE ON configuration_operations
|
|
10121
|
+
WHEN OLD.state IN ('converged','degraded')
|
|
10122
|
+
BEGIN
|
|
10123
|
+
SELECT RAISE(ABORT,'terminal configuration operations are immutable');
|
|
10124
|
+
END`,
|
|
10125
|
+
`CREATE TRIGGER configuration_recoveries_no_delete BEFORE DELETE ON configuration_recoveries
|
|
10126
|
+
BEGIN SELECT RAISE(ABORT,'configuration recovery cannot be deleted'); END`,
|
|
10127
|
+
`CREATE TRIGGER configuration_recoveries_sequence_insert_guard BEFORE INSERT ON configuration_recoveries
|
|
10128
|
+
WHEN NEW.recovery_sequence != -1 BEGIN SELECT RAISE(ABORT,'recovery sequence is database assigned'); END`,
|
|
10129
|
+
`CREATE TRIGGER configuration_recoveries_sequence_update_guard BEFORE UPDATE OF recovery_sequence ON configuration_recoveries
|
|
10130
|
+
BEGIN SELECT RAISE(ABORT,'recovery sequence is immutable'); END`,
|
|
10131
|
+
`CREATE TRIGGER configuration_serving_snapshots_immutable_delete
|
|
10132
|
+
BEFORE DELETE ON configuration_serving_snapshots
|
|
10133
|
+
BEGIN
|
|
10134
|
+
SELECT RAISE(ABORT,'configuration serving snapshots are immutable');
|
|
10135
|
+
END`,
|
|
10136
|
+
`CREATE TRIGGER configuration_serving_snapshots_immutable_update
|
|
10137
|
+
BEFORE UPDATE ON configuration_serving_snapshots
|
|
10138
|
+
BEGIN
|
|
10139
|
+
SELECT RAISE(ABORT,'configuration serving snapshots are immutable');
|
|
10140
|
+
END`,
|
|
10141
|
+
`CREATE TRIGGER configuration_state_singleton_id_immutable
|
|
10142
|
+
BEFORE UPDATE OF id ON configuration_state
|
|
10143
|
+
BEGIN
|
|
10144
|
+
SELECT RAISE(ABORT,'configuration state singleton identity is immutable');
|
|
10145
|
+
END`,
|
|
10146
|
+
`CREATE TRIGGER configuration_state_singleton_no_delete
|
|
10147
|
+
BEFORE DELETE ON configuration_state
|
|
10148
|
+
BEGIN
|
|
10149
|
+
SELECT RAISE(ABORT,'configuration state singleton cannot be deleted');
|
|
10150
|
+
END`,
|
|
10151
|
+
`CREATE TRIGGER configuration_state_singleton_no_replace
|
|
10152
|
+
BEFORE INSERT ON configuration_state
|
|
10153
|
+
WHEN EXISTS(SELECT 1 FROM configuration_state)
|
|
10154
|
+
BEGIN
|
|
10155
|
+
SELECT RAISE(ABORT,'configuration state singleton cannot be replaced');
|
|
10156
|
+
END`
|
|
10157
|
+
];
|
|
10158
|
+
|
|
10159
|
+
// src/config-storage/migrations/baseline.ts
|
|
10160
|
+
var CONFIG_BASELINE_VERSION = 1;
|
|
10161
|
+
var CONFIG_BASELINE_NAME = "current_storage_baseline";
|
|
10162
|
+
var CONFIG_BASELINE = {
|
|
10163
|
+
version: CONFIG_BASELINE_VERSION,
|
|
10164
|
+
name: CONFIG_BASELINE_NAME,
|
|
10165
|
+
up(db) {
|
|
10166
|
+
for (const statement of CONFIG_SCHEMA_STATEMENTS)
|
|
10167
|
+
db.run(statement);
|
|
10168
|
+
const aggregate = { logical_configuration: { services: [], routes: [], plugins: [] }, plugin_activations: [] };
|
|
10169
|
+
replaceActiveMaterialization(db, aggregate);
|
|
10170
|
+
db.run("INSERT INTO configuration_revisions(revision,content_hash,kind,created_at) VALUES(1,?,'config',0)", [hashConfigurationContent(aggregate)]);
|
|
10171
|
+
db.run("INSERT INTO configuration_state(id,schema_version,active_revision,created_at,updated_at) VALUES(1,4,1,0,0)");
|
|
10172
|
+
db.run("INSERT INTO supervision_state(id,instance_id,controller_epoch,current_controller_id,updated_at) VALUES(1,?,0,NULL,0)", [randomUUID()]);
|
|
9963
10173
|
}
|
|
9964
|
-
}
|
|
10174
|
+
};
|
|
9965
10175
|
|
|
9966
|
-
// src/config-storage/
|
|
9967
|
-
var
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
10176
|
+
// src/config-storage/migrations/publication-retirement.ts
|
|
10177
|
+
var PUBLICATION_RETIREMENT_MIGRATION = {
|
|
10178
|
+
version: 2,
|
|
10179
|
+
name: "publication_retirement_and_runtime_recovery",
|
|
10180
|
+
up(db) {
|
|
10181
|
+
db.run(`CREATE TABLE configuration_retirements (
|
|
10182
|
+
mutation_id TEXT PRIMARY KEY REFERENCES configuration_operations(mutation_id),
|
|
10183
|
+
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
10184
|
+
members_json TEXT NOT NULL CHECK(json_valid(members_json) AND json_type(members_json)='array'),
|
|
10185
|
+
state TEXT NOT NULL CHECK(state IN ('pending','stopped','released')),
|
|
10186
|
+
reason_code TEXT CHECK(reason_code IS NULL OR reason_code IN ('exit_unknown','cleanup_unknown','release_unknown')),
|
|
10187
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= 0)
|
|
10188
|
+
) STRICT`);
|
|
10189
|
+
db.run(`CREATE TABLE configuration_retirement_evidence (
|
|
10190
|
+
mutation_id TEXT NOT NULL REFERENCES configuration_retirements(mutation_id),
|
|
10191
|
+
member_id TEXT NOT NULL,
|
|
10192
|
+
exit_state TEXT NOT NULL CHECK(exit_state IN ('live','confirmed','unknown')),
|
|
10193
|
+
cleanup_state TEXT NOT NULL CHECK(cleanup_state IN ('success','failed','unknown')),
|
|
10194
|
+
exit_boot_id TEXT,
|
|
10195
|
+
exit_deadline_ns TEXT,
|
|
10196
|
+
start_boot_id TEXT,
|
|
10197
|
+
start_deadline_ns TEXT,
|
|
10198
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= 0),
|
|
10199
|
+
CHECK((exit_boot_id IS NULL AND exit_deadline_ns IS NULL) OR (exit_boot_id IS NOT NULL AND exit_deadline_ns IS NOT NULL)),
|
|
10200
|
+
CHECK((start_boot_id IS NULL AND start_deadline_ns IS NULL) OR (start_boot_id IS NOT NULL AND start_deadline_ns IS NOT NULL)),
|
|
10201
|
+
PRIMARY KEY(mutation_id,member_id)
|
|
10202
|
+
) STRICT, WITHOUT ROWID`);
|
|
10203
|
+
db.run("CREATE TRIGGER retirement_start_deadline_immutable BEFORE UPDATE OF start_boot_id,start_deadline_ns ON configuration_retirement_evidence WHEN OLD.start_deadline_ns IS NOT NULL AND (NEW.start_boot_id IS NOT OLD.start_boot_id OR NEW.start_deadline_ns IS NOT OLD.start_deadline_ns) BEGIN SELECT RAISE(ABORT,'retirement start deadline is immutable'); END");
|
|
10204
|
+
db.run(`CREATE TABLE configuration_publication_serving_sets (
|
|
10205
|
+
mutation_id TEXT NOT NULL REFERENCES configuration_operations(mutation_id),
|
|
10206
|
+
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
10207
|
+
attempt_key TEXT NOT NULL CHECK(json_valid(attempt_key) AND json_type(attempt_key)='array'),
|
|
10208
|
+
workers_json TEXT NOT NULL CHECK(json_valid(workers_json) AND json_type(workers_json)='array'),
|
|
10209
|
+
exited_json TEXT NOT NULL CHECK(json_valid(exited_json) AND json_type(exited_json)='array'),
|
|
10210
|
+
PRIMARY KEY(mutation_id,generation,attempt_key)
|
|
10211
|
+
) STRICT, WITHOUT ROWID`);
|
|
10212
|
+
db.run("CREATE TRIGGER publication_serving_authority_no_delete BEFORE DELETE ON configuration_publication_serving_sets BEGIN SELECT RAISE(ABORT,'publication serving authority history is immutable'); END");
|
|
10213
|
+
db.run("CREATE TRIGGER publication_serving_authority_no_update BEFORE UPDATE OF mutation_id,generation,attempt_key,workers_json ON configuration_publication_serving_sets BEGIN SELECT RAISE(ABORT,'publication serving attempt identity is immutable'); END");
|
|
10214
|
+
db.run("CREATE TRIGGER publication_serving_authority_no_replace BEFORE INSERT ON configuration_publication_serving_sets WHEN EXISTS(SELECT 1 FROM configuration_publication_serving_sets WHERE mutation_id=NEW.mutation_id AND generation=NEW.generation AND attempt_key=NEW.attempt_key) BEGIN SELECT RAISE(ABORT,'publication serving authority history is immutable'); END");
|
|
10215
|
+
db.run(`CREATE TABLE runtime_recovery_budget (
|
|
10216
|
+
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
10217
|
+
tickets INTEGER NOT NULL CHECK(tickets BETWEEN 0 AND 2),
|
|
10218
|
+
ticket_id TEXT,
|
|
10219
|
+
ticket_boot_id TEXT,
|
|
10220
|
+
retry_at_ns TEXT,
|
|
10221
|
+
healthy_owner TEXT,
|
|
10222
|
+
healthy_boot_id TEXT,
|
|
10223
|
+
healthy_since_ns TEXT,
|
|
10224
|
+
healthy_last_ns TEXT,
|
|
10225
|
+
CHECK((tickets=0 AND ticket_id IS NULL AND ticket_boot_id IS NULL AND retry_at_ns IS NULL) OR
|
|
10226
|
+
(tickets>0 AND ticket_id IS NOT NULL AND ticket_boot_id IS NOT NULL AND retry_at_ns IS NOT NULL)),
|
|
10227
|
+
CHECK((healthy_owner IS NULL AND healthy_boot_id IS NULL AND healthy_since_ns IS NULL AND healthy_last_ns IS NULL) OR
|
|
10228
|
+
(healthy_owner IS NOT NULL AND healthy_boot_id IS NOT NULL AND healthy_since_ns IS NOT NULL AND healthy_last_ns IS NOT NULL))
|
|
10229
|
+
) STRICT`);
|
|
10230
|
+
db.run("INSERT INTO runtime_recovery_budget(id,tickets) VALUES(1,0)");
|
|
10231
|
+
db.run(`CREATE TABLE configuration_retirement_retry_requests (
|
|
10232
|
+
request_id TEXT PRIMARY KEY,
|
|
10233
|
+
mutation_id TEXT NOT NULL REFERENCES configuration_retirements(mutation_id),
|
|
10234
|
+
expected_revision INTEGER NOT NULL CHECK(expected_revision>0),
|
|
10235
|
+
created_at INTEGER NOT NULL CHECK(created_at>=0)
|
|
10236
|
+
) STRICT, WITHOUT ROWID`);
|
|
10237
|
+
db.run("CREATE TRIGGER retirement_retry_no_delete BEFORE DELETE ON configuration_retirement_retry_requests BEGIN SELECT RAISE(ABORT,'retirement retry identity is immutable'); END");
|
|
10238
|
+
db.run("CREATE TRIGGER retirement_retry_no_update BEFORE UPDATE ON configuration_retirement_retry_requests BEGIN SELECT RAISE(ABORT,'retirement retry identity is immutable'); END");
|
|
10239
|
+
db.run("CREATE TRIGGER retirement_retry_no_replace BEFORE INSERT ON configuration_retirement_retry_requests WHEN EXISTS(SELECT 1 FROM configuration_retirement_retry_requests WHERE request_id=NEW.request_id) BEGIN SELECT RAISE(ABORT,'retirement retry identity is immutable'); END");
|
|
10240
|
+
db.run(`CREATE TABLE runtime_recovery_tickets (
|
|
10241
|
+
ticket_id TEXT PRIMARY KEY,
|
|
10242
|
+
owner TEXT NOT NULL,
|
|
10243
|
+
boot_id TEXT NOT NULL,
|
|
10244
|
+
consumed_at_ns TEXT NOT NULL,
|
|
10245
|
+
retry_at_ns TEXT NOT NULL
|
|
10246
|
+
) STRICT, WITHOUT ROWID`);
|
|
10247
|
+
db.run("CREATE TRIGGER runtime_recovery_tickets_no_delete BEFORE DELETE ON runtime_recovery_tickets BEGIN SELECT RAISE(ABORT,'runtime recovery ticket history is immutable'); END");
|
|
10248
|
+
db.run("CREATE TRIGGER runtime_recovery_tickets_no_update BEFORE UPDATE ON runtime_recovery_tickets BEGIN SELECT RAISE(ABORT,'runtime recovery ticket history is immutable'); END");
|
|
10249
|
+
db.run("CREATE TRIGGER runtime_recovery_tickets_no_replace BEFORE INSERT ON runtime_recovery_tickets WHEN EXISTS(SELECT 1 FROM runtime_recovery_tickets WHERE ticket_id=NEW.ticket_id) BEGIN SELECT RAISE(ABORT,'runtime recovery ticket history is immutable'); END");
|
|
10250
|
+
db.run("CREATE TRIGGER runtime_recovery_budget_no_delete BEFORE DELETE ON runtime_recovery_budget BEGIN SELECT RAISE(ABORT,'runtime recovery budget cannot be deleted'); END");
|
|
10251
|
+
db.run("CREATE TRIGGER runtime_recovery_budget_no_replace BEFORE INSERT ON runtime_recovery_budget WHEN EXISTS(SELECT 1 FROM runtime_recovery_budget) BEGIN SELECT RAISE(ABORT,'runtime recovery budget cannot be replaced'); END");
|
|
10252
|
+
db.run("CREATE TRIGGER runtime_recovery_budget_fixed_identity BEFORE UPDATE OF id ON runtime_recovery_budget BEGIN SELECT RAISE(ABORT,'runtime recovery budget identity is immutable'); END");
|
|
9991
10253
|
}
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
10254
|
+
};
|
|
10255
|
+
|
|
10256
|
+
// src/config-storage/migrations/plan.ts
|
|
10257
|
+
var CONFIG_MIGRATIONS = [CONFIG_BASELINE, PUBLICATION_RETIREMENT_MIGRATION, UI_MENUS_MIGRATION, MANAGEMENT_PROTECTION_MIGRATION, SERVICE_REFERENCES_MIGRATION];
|
|
10258
|
+
var CONFIG_SCHEMA_VERSION = CONFIG_MIGRATIONS.at(-1).version;
|
|
10259
|
+
|
|
10260
|
+
// src/config-storage/schema-fingerprint.ts
|
|
10261
|
+
function readSchemaObjectFingerprint(db) {
|
|
10262
|
+
return canonicalJson(sqliteAll(db, `SELECT type,name,tbl_name,sql
|
|
10263
|
+
FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type,name`));
|
|
9998
10264
|
}
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10265
|
+
var expectedDescriptors = new Map;
|
|
10266
|
+
function verifySchemaFingerprint(db, version = CONFIG_SCHEMA_VERSION, plan = CONFIG_MIGRATIONS) {
|
|
10267
|
+
if (!plan.some((migration) => migration.version === version)) {
|
|
10268
|
+
throw new ConfigRepositoryError("schema_corrupt", "unsupported configuration schema version");
|
|
10269
|
+
}
|
|
10270
|
+
let expected = plan === CONFIG_MIGRATIONS ? expectedDescriptors.get(version) : undefined;
|
|
10271
|
+
if (expected === undefined) {
|
|
10272
|
+
const reference = new Database(":memory:", { strict: true });
|
|
10273
|
+
try {
|
|
10274
|
+
for (const migration of plan.filter((migration) => migration.version <= version))
|
|
10275
|
+
migration.up(reference);
|
|
10276
|
+
expected = databaseSchemaDescriptor(reference);
|
|
10277
|
+
} finally {
|
|
10278
|
+
reference.close(true);
|
|
10006
10279
|
}
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
throw new ConfigRepositoryError("schema_corrupt", "upstream owner is missing");
|
|
10010
|
-
requireUuid(ownerId, "upstreams.owner_id");
|
|
10011
|
-
const key = `${ownerKind}:${ownerId}`;
|
|
10012
|
-
const values = groups.get(key) ?? [];
|
|
10013
|
-
values.push({
|
|
10014
|
-
...parseCanonicalObject(row.policy_json, "upstream policy_json", UPSTREAM_RESERVED),
|
|
10015
|
-
id: row.id,
|
|
10016
|
-
position: requireSafeInteger(row.position, "upstreams.position"),
|
|
10017
|
-
target: row.target,
|
|
10018
|
-
weight: requirePositiveFinite(row.weight, "upstreams.weight"),
|
|
10019
|
-
priority: requirePositiveFinite(row.priority, "upstreams.priority"),
|
|
10020
|
-
is_disabled: requireBooleanInteger(row.is_disabled, "upstreams.is_disabled"),
|
|
10021
|
-
plugins: takeGroup(bindings, `upstream:${row.id}`)
|
|
10022
|
-
});
|
|
10023
|
-
groups.set(key, values);
|
|
10280
|
+
if (plan === CONFIG_MIGRATIONS)
|
|
10281
|
+
expectedDescriptors.set(version, expected);
|
|
10024
10282
|
}
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
// src/config-storage/read-materialization.ts
|
|
10029
|
-
var SERVICE_RESERVED = new Set(["id", "position", "name", "endpoints", "plugins"]);
|
|
10030
|
-
var ROUTE_RESERVED = new Set(["id", "position", "path", "service_id", "service", "endpoints", "plugins"]);
|
|
10031
|
-
function readSettings(db) {
|
|
10032
|
-
const hasPublication = sqliteAll(db, "PRAGMA table_info(settings)").some(({ name }) => name === "publication_json");
|
|
10033
|
-
const rows = sqliteAll(db, `SELECT log_level,body_parser_limit,auth_json,logging_json${hasPublication ? ",publication_json" : ",NULL AS publication_json"} FROM settings`);
|
|
10034
|
-
const row = rows[0];
|
|
10035
|
-
if (rows.length !== 1 || row === undefined)
|
|
10036
|
-
throw new ConfigRepositoryError("schema_corrupt", "settings singleton is broken");
|
|
10037
|
-
return {
|
|
10038
|
-
...row.log_level === null ? {} : { log_level: requireOptionalGlobalScalar(row.log_level, "log_level") },
|
|
10039
|
-
...row.body_parser_limit === null ? {} : {
|
|
10040
|
-
body_parser_limit: requireOptionalGlobalScalar(row.body_parser_limit, "body_parser_limit")
|
|
10041
|
-
},
|
|
10042
|
-
...row.auth_json === null ? {} : { auth: parseCanonicalObject(row.auth_json, "auth_json") },
|
|
10043
|
-
...row.logging_json === null ? {} : { logging: parseCanonicalObject(row.logging_json, "logging_json") },
|
|
10044
|
-
...row.publication_json === null ? {} : { publication: parseCanonicalObject(row.publication_json, "publication_json") }
|
|
10045
|
-
};
|
|
10046
|
-
}
|
|
10047
|
-
function readRawActiveAggregate(db) {
|
|
10048
|
-
const bindings = groupBindings(sqliteAll(db, `SELECT id,scope_kind,scope_owner,service_id,route_id,
|
|
10049
|
-
upstream_id,position,plugin_name,options_json,enabled FROM plugin_bindings ORDER BY scope_kind,scope_owner,position`));
|
|
10050
|
-
const upstreams = groupUpstreams(sqliteAll(db, `SELECT id,owner_kind,service_id,route_id,
|
|
10051
|
-
position,target,weight,priority,is_disabled,policy_json FROM upstreams ORDER BY owner_kind,position`), bindings);
|
|
10052
|
-
const services = sqliteAll(db, "SELECT id,position,name,policy_json FROM services ORDER BY position").map((row) => ({
|
|
10053
|
-
...parseCanonicalObject(row.policy_json, "service policy_json", SERVICE_RESERVED),
|
|
10054
|
-
id: requireUuid(row.id, "services.id"),
|
|
10055
|
-
position: requireSafeInteger(row.position, "services.position"),
|
|
10056
|
-
name: row.name,
|
|
10057
|
-
endpoints: takeGroup(upstreams, `service:${row.id}`),
|
|
10058
|
-
plugins: takeGroup(bindings, `service:${row.id}`)
|
|
10059
|
-
}));
|
|
10060
|
-
const routes = sqliteAll(db, "SELECT id,position,path,service_id,policy_json FROM routes ORDER BY position").map((row) => ({
|
|
10061
|
-
...parseCanonicalObject(row.policy_json, "route policy_json", ROUTE_RESERVED),
|
|
10062
|
-
id: requireUuid(row.id, "routes.id"),
|
|
10063
|
-
position: requireSafeInteger(row.position, "routes.position"),
|
|
10064
|
-
path: row.path,
|
|
10065
|
-
...row.service_id === null ? { endpoints: takeGroup(upstreams, `route:${row.id}`) } : { service_id: requireUuid(row.service_id, "routes.service_id") },
|
|
10066
|
-
plugins: takeGroup(bindings, `route:${row.id}`)
|
|
10067
|
-
}));
|
|
10068
|
-
const activations = sqliteAll(db, "SELECT plugin_name FROM plugin_activations ORDER BY plugin_name");
|
|
10069
|
-
const input = { logical_configuration: {
|
|
10070
|
-
...readSettings(db),
|
|
10071
|
-
services,
|
|
10072
|
-
routes,
|
|
10073
|
-
plugins: takeGroup(bindings, "global:")
|
|
10074
|
-
}, plugin_activations: activations.map(({ plugin_name }) => ({
|
|
10075
|
-
plugin_name: requirePluginName(plugin_name, "plugin_activations.plugin_name")
|
|
10076
|
-
})) };
|
|
10077
|
-
if (bindings.size !== 0 || upstreams.size !== 0) {
|
|
10078
|
-
throw new ConfigRepositoryError("schema_corrupt", "normalized relation rows were not consumed");
|
|
10283
|
+
if (databaseSchemaDescriptor(db) !== expected) {
|
|
10284
|
+
throw new ConfigRepositoryError("schema_corrupt", "configuration schema fingerprint is invalid");
|
|
10079
10285
|
}
|
|
10080
|
-
return input;
|
|
10081
|
-
}
|
|
10082
|
-
function readActiveAggregate(db) {
|
|
10083
|
-
const result = parseNormalizeCompileAggregate(readRawActiveAggregate(db));
|
|
10084
|
-
if (!result.ok)
|
|
10085
|
-
throw new ConfigRepositoryError("schema_corrupt", "active configuration is invalid", result.errors);
|
|
10086
|
-
return result.value;
|
|
10087
10286
|
}
|
|
10088
10287
|
|
|
10089
|
-
// src/config-storage/
|
|
10090
|
-
function
|
|
10091
|
-
if (
|
|
10092
|
-
return
|
|
10093
|
-
|
|
10288
|
+
// src/config-storage/sqlite-errors.ts
|
|
10289
|
+
function isSqliteBusyError(error) {
|
|
10290
|
+
if (!(error instanceof Error))
|
|
10291
|
+
return false;
|
|
10292
|
+
const sqliteError = error;
|
|
10293
|
+
const errno = sqliteError.errno;
|
|
10294
|
+
if (typeof errno === "number" && ((errno & 255) === 5 || (errno & 255) === 6))
|
|
10295
|
+
return true;
|
|
10296
|
+
const code = sqliteError.code;
|
|
10297
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED" || code?.startsWith("SQLITE_BUSY_") === true || code?.startsWith("SQLITE_LOCKED_") === true;
|
|
10298
|
+
}
|
|
10299
|
+
function repositoryFailure(message, cause) {
|
|
10300
|
+
return new ConfigRepositoryError("repository_failure", message, cause);
|
|
10094
10301
|
}
|
|
10095
10302
|
|
|
10096
10303
|
// src/config-storage/repository-snapshot.ts
|
|
@@ -10098,6 +10305,8 @@ function verifyIntegrity(db, allowActiveRecoveryDrift = false, checkDatabasePage
|
|
|
10098
10305
|
verifySchemaFingerprint(db);
|
|
10099
10306
|
readManagementProtection(db);
|
|
10100
10307
|
verifyRecoveryIntegrity(db, allowActiveRecoveryDrift);
|
|
10308
|
+
if (checkDatabasePages)
|
|
10309
|
+
auditServingSnapshotHistory(db);
|
|
10101
10310
|
const integrity = checkDatabasePages ? sqliteGet(db, "PRAGMA integrity_check")?.integrity_check : "ok";
|
|
10102
10311
|
const foreignKeyFailures = sqliteAll(db, "PRAGMA foreign_key_check");
|
|
10103
10312
|
if (integrity !== "ok" || foreignKeyFailures.length > 0) {
|
|
@@ -10144,101 +10353,109 @@ function verifyRepositoryIntegrity(db) {
|
|
|
10144
10353
|
}
|
|
10145
10354
|
}
|
|
10146
10355
|
|
|
10147
|
-
// src/config-storage/
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
if (revision === null || revision.content_hash !== snapshot.content_hash) {
|
|
10177
|
-
invalid4("serving snapshot revision does not match configuration history");
|
|
10178
|
-
}
|
|
10179
|
-
const existing = sqliteGet(db, `SELECT revision,content_hash,plugin_catalog_hash,aggregate_json
|
|
10180
|
-
FROM configuration_serving_snapshots
|
|
10181
|
-
WHERE revision=? AND content_hash=? AND plugin_catalog_hash=?`, snapshot.revision, snapshot.content_hash, pluginCatalogHash);
|
|
10182
|
-
if (existing !== null) {
|
|
10183
|
-
if (existing.aggregate_json !== normalized.aggregateJson) {
|
|
10184
|
-
corrupt3("configuration serving snapshot is immutable and inconsistent");
|
|
10185
|
-
}
|
|
10186
|
-
return;
|
|
10356
|
+
// src/config-storage/service-reference-upgrade.ts
|
|
10357
|
+
function convertServiceReferenceRevision(db, options) {
|
|
10358
|
+
const unfinished = db.query(`
|
|
10359
|
+
SELECT 'publication' AS kind,mutation_id AS identity,state FROM configuration_operations
|
|
10360
|
+
WHERE state IN ('committed','publishing','draining')
|
|
10361
|
+
UNION ALL SELECT 'retirement',mutation_id,state FROM configuration_retirements WHERE state<>'released'
|
|
10362
|
+
UNION ALL SELECT 'recovery',recovery_id,state FROM configuration_recoveries
|
|
10363
|
+
WHERE state IN ('scheduled','running') OR (state='stopped'
|
|
10364
|
+
AND target_revision=(SELECT active_revision FROM configuration_state WHERE id=1)
|
|
10365
|
+
AND recovery_sequence=(SELECT max(recovery_sequence) FROM configuration_recoveries
|
|
10366
|
+
WHERE target_revision=(SELECT active_revision FROM configuration_state WHERE id=1)))
|
|
10367
|
+
LIMIT 1`).get();
|
|
10368
|
+
if (unfinished)
|
|
10369
|
+
throw new ConfigRepositoryError("migration_failed", `service reference upgrade cannot resume ${unfinished.kind} ${unfinished.identity} (${unfinished.state}); finish it with the previous executable before upgrading`);
|
|
10370
|
+
verifyRepositoryIntegrity(db);
|
|
10371
|
+
const audited = auditConfigurationTables(db);
|
|
10372
|
+
const aggregate = readActiveAggregate(db);
|
|
10373
|
+
const previous = {
|
|
10374
|
+
...aggregate,
|
|
10375
|
+
logical_configuration: {
|
|
10376
|
+
...aggregate.logical_configuration,
|
|
10377
|
+
routes: aggregate.logical_configuration.routes.map((route) => {
|
|
10378
|
+
if (!route.service_references)
|
|
10379
|
+
return route;
|
|
10380
|
+
const { service_references, ...rest } = route;
|
|
10381
|
+
if (service_references.length !== 1)
|
|
10382
|
+
throw new ConfigRepositoryError("migration_failed", "unexpected route references during upgrade");
|
|
10383
|
+
return { ...rest, service_id: service_references[0].service_id };
|
|
10384
|
+
})
|
|
10187
10385
|
}
|
|
10188
|
-
|
|
10189
|
-
|
|
10386
|
+
};
|
|
10387
|
+
const previousHash = hashConfigurationContent(previous);
|
|
10388
|
+
if (previousHash !== audited.activeRevisionRow.content_hash) {
|
|
10389
|
+
throw new ConfigRepositoryError("schema_corrupt", "pre-upgrade configuration content hash does not match");
|
|
10390
|
+
}
|
|
10391
|
+
verifyActiveRequestIdentity(audited, previous);
|
|
10392
|
+
if (hashConfigurationContent(aggregate) === previousHash)
|
|
10393
|
+
return;
|
|
10394
|
+
const snapshot = { revision: audited.activeRevision, content_hash: previousHash, aggregate: previous };
|
|
10395
|
+
const operationId = randomUUID2(), now = Math.max(Date.now(), audited.activeOperation?.updated_at ?? 0);
|
|
10396
|
+
const prepared = prepareCommitCommand({
|
|
10397
|
+
mutation_id: operationId,
|
|
10398
|
+
expected_revision: snapshot.revision,
|
|
10399
|
+
aggregate,
|
|
10400
|
+
kind: "config",
|
|
10401
|
+
created_at: now,
|
|
10402
|
+
target_worker_slots: []
|
|
10190
10403
|
});
|
|
10191
|
-
|
|
10404
|
+
const provider = readManagementProtection(db).selectedProvider;
|
|
10405
|
+
const command = {
|
|
10406
|
+
...prepared,
|
|
10407
|
+
managementProvider: provider,
|
|
10408
|
+
requestHash: hashConfigurationRequest({
|
|
10409
|
+
kind: prepared.kind,
|
|
10410
|
+
expected_revision: prepared.expectedRevision,
|
|
10411
|
+
aggregate: prepared.aggregate,
|
|
10412
|
+
target_worker_slots: [],
|
|
10413
|
+
management_provider: provider
|
|
10414
|
+
})
|
|
10415
|
+
};
|
|
10416
|
+
const result = commitTransaction(db, command, options, snapshot);
|
|
10417
|
+
if (result.kind !== "committed")
|
|
10418
|
+
throw new ConfigRepositoryError("migration_failed", `service reference revision commit failed (${result.kind})`);
|
|
10419
|
+
beginPublication(db, operationId, now);
|
|
10420
|
+
markDraining(db, operationId, now);
|
|
10421
|
+
finalizePublication(db, operationId, { outcome: "converged", old_workers_exited: true }, now);
|
|
10422
|
+
readRepositorySnapshot(db);
|
|
10192
10423
|
}
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10424
|
+
|
|
10425
|
+
// src/config-storage/migrations/index.ts
|
|
10426
|
+
function migrateConfigurationDatabase(db, plan = CONFIG_MIGRATIONS, options = {}) {
|
|
10427
|
+
try {
|
|
10428
|
+
db.transaction(() => {
|
|
10429
|
+
if (!plan.length || plan[0] !== CONFIG_MIGRATIONS[0] || plan.some((m, i) => i > 0 && m.version !== plan[i - 1].version + 1) || new Set(plan.map((m) => m.name)).size !== plan.length)
|
|
10430
|
+
throw new ConfigRepositoryError("migration_failed", "invalid configuration migration plan");
|
|
10431
|
+
const count = db.query("SELECT count(*) AS count FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%'").get().count;
|
|
10432
|
+
let applied = [];
|
|
10433
|
+
if (count > 0) {
|
|
10434
|
+
applied = db.query("SELECT version,name FROM schema_migrations ORDER BY version").all();
|
|
10435
|
+
if (!applied.length || applied.length > plan.length || applied.some((r, i) => r.version !== plan[i]?.version || r.name !== plan[i]?.name))
|
|
10436
|
+
throw new ConfigRepositoryError("schema_corrupt", "configuration migration history is not an exact baseline prefix");
|
|
10437
|
+
verifySchemaFingerprint(db, applied.at(-1).version, plan);
|
|
10438
|
+
}
|
|
10439
|
+
for (const migration of plan.slice(applied.length)) {
|
|
10440
|
+
migration.up(db);
|
|
10441
|
+
db.run("INSERT INTO schema_migrations(version,name) VALUES(?,?)", [migration.version, migration.name]);
|
|
10442
|
+
}
|
|
10443
|
+
verifySchemaFingerprint(db, plan.at(-1).version, plan);
|
|
10444
|
+
if (plan === CONFIG_MIGRATIONS && applied.length > 0 && applied.at(-1).version < 5) {
|
|
10445
|
+
convertServiceReferenceRevision(db, options);
|
|
10446
|
+
}
|
|
10447
|
+
}).immediate();
|
|
10448
|
+
} catch (error) {
|
|
10449
|
+
if (error instanceof ConfigRepositoryError)
|
|
10450
|
+
throw error;
|
|
10451
|
+
if (isSqliteBusyError(error))
|
|
10452
|
+
throw repositoryFailure("configuration migration was blocked by SQLite", error);
|
|
10453
|
+
throw new ConfigRepositoryError("migration_failed", "configuration migration failed", error);
|
|
10196
10454
|
}
|
|
10197
|
-
return withConsistentRead(db, () => {
|
|
10198
|
-
const row = sqliteGet(db, `SELECT revision,content_hash,plugin_catalog_hash,aggregate_json
|
|
10199
|
-
FROM configuration_serving_snapshots
|
|
10200
|
-
WHERE revision=? AND content_hash=? AND plugin_catalog_hash=?`, key.revision, key.content_hash, key.plugin_catalog_hash);
|
|
10201
|
-
if (row === null)
|
|
10202
|
-
return null;
|
|
10203
|
-
if (row.revision !== key.revision || row.content_hash !== key.content_hash || row.plugin_catalog_hash !== key.plugin_catalog_hash || !Number.isSafeInteger(row.revision) || row.revision <= 0 || !validateDigest(row.content_hash) || !validateDigest(row.plugin_catalog_hash)) {
|
|
10204
|
-
corrupt3("configuration serving snapshot identity is invalid");
|
|
10205
|
-
}
|
|
10206
|
-
const bytes = new TextEncoder().encode(row.aggregate_json).byteLength;
|
|
10207
|
-
if (bytes < 2 || bytes > MAX_AGGREGATE_BYTES)
|
|
10208
|
-
corrupt3("configuration serving snapshot size is invalid");
|
|
10209
|
-
let parsed;
|
|
10210
|
-
try {
|
|
10211
|
-
parsed = JSON.parse(row.aggregate_json);
|
|
10212
|
-
} catch {
|
|
10213
|
-
corrupt3("configuration serving snapshot JSON is invalid");
|
|
10214
|
-
}
|
|
10215
|
-
let canonical;
|
|
10216
|
-
try {
|
|
10217
|
-
canonical = canonicalJson(parsed);
|
|
10218
|
-
} catch {
|
|
10219
|
-
corrupt3("configuration serving snapshot JSON is invalid");
|
|
10220
|
-
}
|
|
10221
|
-
if (canonical !== row.aggregate_json)
|
|
10222
|
-
corrupt3("configuration serving snapshot JSON is not canonical");
|
|
10223
|
-
const normalized = parseNormalizeCompileAggregate(parsed, undefined);
|
|
10224
|
-
if (!normalized.ok)
|
|
10225
|
-
corrupt3("configuration serving snapshot aggregate is invalid");
|
|
10226
|
-
if (canonicalJson(normalized.value) !== row.aggregate_json) {
|
|
10227
|
-
corrupt3("configuration serving snapshot aggregate normalization drifted");
|
|
10228
|
-
}
|
|
10229
|
-
if (hashConfigurationContent(normalized.value) !== key.content_hash) {
|
|
10230
|
-
corrupt3("configuration serving snapshot content hash is invalid");
|
|
10231
|
-
}
|
|
10232
|
-
const revision = sqliteGet(db, "SELECT revision,content_hash FROM configuration_revisions WHERE revision=?", key.revision);
|
|
10233
|
-
if (revision === null || revision.revision !== key.revision || revision.content_hash !== key.content_hash) {
|
|
10234
|
-
corrupt3("configuration serving snapshot revision is invalid");
|
|
10235
|
-
}
|
|
10236
|
-
return { revision: row.revision, content_hash: row.content_hash, aggregate: normalized.value };
|
|
10237
|
-
});
|
|
10238
10455
|
}
|
|
10239
10456
|
|
|
10240
10457
|
// src/config-storage/config-repository.ts
|
|
10241
|
-
import { randomUUID as
|
|
10458
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
10242
10459
|
|
|
10243
10460
|
// src/config-storage/sqlite-version.ts
|
|
10244
10461
|
class SqliteVersionError extends Error {
|
|
@@ -10446,7 +10663,7 @@ class ConfigRepository {
|
|
|
10446
10663
|
mkdirSync(dirname2(dbPath), { recursive: true });
|
|
10447
10664
|
db = new Database3(dbPath, { create: true, readwrite: true, strict: true });
|
|
10448
10665
|
configureConnection(db);
|
|
10449
|
-
migrateConfigurationDatabase(db);
|
|
10666
|
+
migrateConfigurationDatabase(db, undefined, options);
|
|
10450
10667
|
verifyRepositoryIntegrity(db);
|
|
10451
10668
|
const repository = new ConfigRepository(db, options, freezeSnapshot(readRepositorySnapshot(db)));
|
|
10452
10669
|
repository.removeUnavailablePlugins();
|
|
@@ -10525,7 +10742,7 @@ class ConfigRepository {
|
|
|
10525
10742
|
return;
|
|
10526
10743
|
const protection = readManagementProtection(this.db);
|
|
10527
10744
|
const provider = protection.selectedProvider !== null && available.has(protection.selectedProvider) ? protection.selectedProvider : null;
|
|
10528
|
-
const now = Date.now(), operationId =
|
|
10745
|
+
const now = Date.now(), operationId = randomUUID3();
|
|
10529
10746
|
const prepared = prepareCommitCommand({
|
|
10530
10747
|
mutation_id: operationId,
|
|
10531
10748
|
expected_revision: snapshot.revision,
|
|
@@ -10780,14 +10997,14 @@ class ConfigRepository {
|
|
|
10780
10997
|
if (before.state !== "converged" && before.state !== "degraded" && outcome.outcome === "degraded" && (outcome.error_code === "replacement_convergence_failed" || outcome.error_code === "control_readiness_failed")) {
|
|
10781
10998
|
switch (outcome.recovery_disposition) {
|
|
10782
10999
|
case "retryable":
|
|
10783
|
-
createAutomaticRecovery(this.db,
|
|
11000
|
+
createAutomaticRecovery(this.db, randomUUID3(), mutationId, operation.committed_revision, updatedAt);
|
|
10784
11001
|
this.options.faultInjection?.("after_automatic_recovery");
|
|
10785
11002
|
break;
|
|
10786
11003
|
case "deterministic_worker_rejection":
|
|
10787
11004
|
case "deterministic_protocol_failure":
|
|
10788
11005
|
case "deterministic_control_failure":
|
|
10789
11006
|
case "fatal":
|
|
10790
|
-
createStoppedRecovery(this.db,
|
|
11007
|
+
createStoppedRecovery(this.db, randomUUID3(), mutationId, operation.committed_revision, outcome.recovery_disposition === "fatal" ? "fatal_source_failure" : outcome.recovery_disposition, outcome.error_detail, updatedAt);
|
|
10791
11008
|
break;
|
|
10792
11009
|
default: {
|
|
10793
11010
|
const unhandled = outcome.recovery_disposition;
|