@rex0220/ksql-flownet 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ja.md +81 -0
- package/README.md +117 -0
- package/dist/bundle/bundle-builder.d.ts +49 -0
- package/dist/bundle/bundle-builder.js +208 -0
- package/dist/bundle/index.d.ts +4 -0
- package/dist/bundle/index.js +4 -0
- package/dist/bundle/upload.d.ts +28 -0
- package/dist/bundle/upload.js +72 -0
- package/dist/bundle/zip-store.d.ts +17 -0
- package/dist/bundle/zip-store.js +175 -0
- package/dist/cli/archive-run-command.d.ts +12 -0
- package/dist/cli/archive-run-command.js +131 -0
- package/dist/cli/cancel-run-command.d.ts +9 -0
- package/dist/cli/cancel-run-command.js +77 -0
- package/dist/cli/force-unlock-network-command.d.ts +8 -0
- package/dist/cli/force-unlock-network-command.js +129 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +132 -0
- package/dist/cli/plan-command.d.ts +2 -0
- package/dist/cli/plan-command.js +100 -0
- package/dist/cli/poll-requests-command.d.ts +3 -0
- package/dist/cli/poll-requests-command.js +74 -0
- package/dist/cli/record-job-unlock-command.d.ts +11 -0
- package/dist/cli/record-job-unlock-command.js +78 -0
- package/dist/cli/resolve-node-command.d.ts +13 -0
- package/dist/cli/resolve-node-command.js +174 -0
- package/dist/cli/run-network-command.d.ts +14 -0
- package/dist/cli/run-network-command.js +418 -0
- package/dist/cli/status-command.d.ts +5 -0
- package/dist/cli/status-command.js +137 -0
- package/dist/cli/validate-command.d.ts +6 -0
- package/dist/cli/validate-command.js +17 -0
- package/dist/dag/descendants.d.ts +9 -0
- package/dist/dag/descendants.js +67 -0
- package/dist/dag/topological-sort.d.ts +7 -0
- package/dist/dag/topological-sort.js +32 -0
- package/dist/domain/business-key.d.ts +15 -0
- package/dist/domain/business-key.js +249 -0
- package/dist/domain/canonical-lock-key.d.ts +17 -0
- package/dist/domain/canonical-lock-key.js +60 -0
- package/dist/domain/canonical-record-key.d.ts +18 -0
- package/dist/domain/canonical-record-key.js +70 -0
- package/dist/domain/load-network.d.ts +4 -0
- package/dist/domain/load-network.js +47 -0
- package/dist/domain/network-definition.d.ts +47 -0
- package/dist/domain/network-definition.js +7 -0
- package/dist/domain/persistence-model.d.ts +198 -0
- package/dist/domain/persistence-model.js +2 -0
- package/dist/domain/run-aggregate.d.ts +3 -0
- package/dist/domain/run-aggregate.js +15 -0
- package/dist/domain/validate-network-path.d.ts +11 -0
- package/dist/domain/validate-network-path.js +33 -0
- package/dist/domain/validate-network.d.ts +3 -0
- package/dist/domain/validate-network.js +180 -0
- package/dist/executor/attempt-executor.d.ts +43 -0
- package/dist/executor/attempt-executor.js +236 -0
- package/dist/executor/job-log-reader.d.ts +37 -0
- package/dist/executor/job-log-reader.js +91 -0
- package/dist/executor/ksql-flow-cli.d.ts +86 -0
- package/dist/executor/ksql-flow-cli.js +161 -0
- package/dist/executor/preflight.d.ts +41 -0
- package/dist/executor/preflight.js +150 -0
- package/dist/executor/result-classifier.d.ts +69 -0
- package/dist/executor/result-classifier.js +237 -0
- package/dist/executor/run-subprocess.d.ts +64 -0
- package/dist/executor/run-subprocess.js +175 -0
- package/dist/io/input-baseline.d.ts +19 -0
- package/dist/io/input-baseline.js +214 -0
- package/dist/io/io-config.d.ts +6 -0
- package/dist/io/io-config.js +33 -0
- package/dist/io/io-path.d.ts +37 -0
- package/dist/io/io-path.js +279 -0
- package/dist/orchestration/archive-run.d.ts +53 -0
- package/dist/orchestration/archive-run.js +323 -0
- package/dist/orchestration/cancel-request.d.ts +18 -0
- package/dist/orchestration/cancel-request.js +67 -0
- package/dist/orchestration/ensure-run.d.ts +83 -0
- package/dist/orchestration/ensure-run.js +632 -0
- package/dist/orchestration/job-lock-audit.d.ts +24 -0
- package/dist/orchestration/job-lock-audit.js +123 -0
- package/dist/orchestration/orphan-attempt-adjudication.d.ts +39 -0
- package/dist/orchestration/orphan-attempt-adjudication.js +167 -0
- package/dist/orchestration/reconciliation.d.ts +41 -0
- package/dist/orchestration/reconciliation.js +526 -0
- package/dist/orchestration/resolve-node.d.ts +29 -0
- package/dist/orchestration/resolve-node.js +105 -0
- package/dist/orchestration/run-activity.d.ts +13 -0
- package/dist/orchestration/run-activity.js +14 -0
- package/dist/orchestration/sequential-scheduler.d.ts +79 -0
- package/dist/orchestration/sequential-scheduler.js +706 -0
- package/dist/orchestration/status.d.ts +91 -0
- package/dist/orchestration/status.js +171 -0
- package/dist/persistence/in-memory-repository.d.ts +41 -0
- package/dist/persistence/in-memory-repository.js +333 -0
- package/dist/persistence/kintone/client.d.ts +36 -0
- package/dist/persistence/kintone/client.js +93 -0
- package/dist/persistence/kintone/design-notes.d.ts +38 -0
- package/dist/persistence/kintone/design-notes.js +38 -0
- package/dist/persistence/kintone/repository.d.ts +44 -0
- package/dist/persistence/kintone/repository.js +836 -0
- package/dist/persistence/kintone/schema.d.ts +116 -0
- package/dist/persistence/kintone/schema.js +115 -0
- package/dist/persistence/network-lock-reader.d.ts +24 -0
- package/dist/persistence/network-lock-reader.js +43 -0
- package/dist/persistence/network-lock-recovery.d.ts +66 -0
- package/dist/persistence/network-lock-recovery.js +392 -0
- package/dist/persistence/network-lock.d.ts +97 -0
- package/dist/persistence/network-lock.js +408 -0
- package/dist/persistence/repository.d.ts +101 -0
- package/dist/persistence/repository.js +11 -0
- package/dist/persistence/state-transition.d.ts +4 -0
- package/dist/persistence/state-transition.js +25 -0
- package/dist/requests/flownet-child-client.d.ts +78 -0
- package/dist/requests/flownet-child-client.js +243 -0
- package/dist/requests/kintone-request-store.d.ts +44 -0
- package/dist/requests/kintone-request-store.js +202 -0
- package/dist/requests/poll-requests-config.d.ts +26 -0
- package/dist/requests/poll-requests-config.js +105 -0
- package/dist/requests/request-model.d.ts +57 -0
- package/dist/requests/request-model.js +305 -0
- package/dist/requests/request-poller.d.ts +33 -0
- package/dist/requests/request-poller.js +353 -0
- package/dist/requests/request-result.d.ts +18 -0
- package/dist/requests/request-result.js +215 -0
- package/dist/requests/start-request.d.ts +26 -0
- package/dist/requests/start-request.js +83 -0
- package/package.json +63 -0
- package/schemas/.gitkeep +1 -0
- package/schemas/network-definition.schema.json +121 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const HELP_TEXT = `Usage: ksql-flownet [options] [command]
|
|
5
|
+
|
|
6
|
+
kSQL-FlowNet Control Plane CLI
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-h, --help display help for command
|
|
10
|
+
-V, --version display version
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
validate <network> validate a network definition and its SQL files
|
|
14
|
+
plan <network> [--scheduled-for <timestamp>] [--business-key <key>]
|
|
15
|
+
display the business key and stable execution plan (read-only)
|
|
16
|
+
run-network <network> [--business-key <key>] [--scheduled-for <timestamp>]
|
|
17
|
+
[--resume] [--resume-run <run_id>] [--rerun-from <node_id>]
|
|
18
|
+
[--json]
|
|
19
|
+
[--ksql-flow-bin <path>] [--ksql-flow-config <path>]
|
|
20
|
+
[--ksql-flow-workdir <path>]
|
|
21
|
+
ensure and execute a Network Run sequentially
|
|
22
|
+
poll-requests [--check]
|
|
23
|
+
claim and process app operation requests (one-shot),
|
|
24
|
+
or validate configuration and read access without writes
|
|
25
|
+
resolve-node --run-id <run_id> --node-id <node_id> --to <status>
|
|
26
|
+
--reason-file <path> --evidence-ref <ref>
|
|
27
|
+
--stop-confirmed-by <subject> --stop-evidence-ref <ref>
|
|
28
|
+
[--manual-completion | --compensation] [--approved-by <subject>]
|
|
29
|
+
resolve an UNKNOWN or non-idempotent FAILED node
|
|
30
|
+
record-job-unlock --result-file <path> --run-id <run_id> --node-id <node_id>
|
|
31
|
+
--reason-file <path> --evidence-ref <ref>
|
|
32
|
+
--stop-confirmed-by <subject>
|
|
33
|
+
associate a kSQL-Flow LOCK_RECOVERY_RESULT with audit
|
|
34
|
+
force-unlock-network <network_id> --profile <profile>
|
|
35
|
+
--expected-owner-invocation-id <invocation_id>
|
|
36
|
+
--reason-file <path> --evidence-ref <ref>
|
|
37
|
+
--stop-confirmed-by <subject> --stop-evidence-ref <ref>
|
|
38
|
+
--stop-method <method>
|
|
39
|
+
force-release a stale Network lock with an audit record
|
|
40
|
+
cancel-run --run-id <run_id> [--release] --reason-file <path>
|
|
41
|
+
request or release a Run hold at the next node boundary
|
|
42
|
+
archive-run <network.yaml> --run-id <run_id> --reason-file <path> [--profile <profile>]
|
|
43
|
+
archive a terminal failed or cancelled Run
|
|
44
|
+
status <network_id> --profile <profile>
|
|
45
|
+
[--run-id <run_id> | --business-key <key>] [--json]
|
|
46
|
+
inspect lock and Run recovery state (read-only)
|
|
47
|
+
`;
|
|
48
|
+
function getVersion() {
|
|
49
|
+
const packageJsonPath = fileURLToPath(new URL("../../package.json", import.meta.url));
|
|
50
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
51
|
+
if (typeof parsed !== "object" ||
|
|
52
|
+
parsed === null ||
|
|
53
|
+
!("version" in parsed) ||
|
|
54
|
+
typeof parsed.version !== "string") {
|
|
55
|
+
throw new Error("package.json does not contain a valid version");
|
|
56
|
+
}
|
|
57
|
+
return parsed.version;
|
|
58
|
+
}
|
|
59
|
+
function printHelp() {
|
|
60
|
+
process.stdout.write(HELP_TEXT);
|
|
61
|
+
}
|
|
62
|
+
async function main(args) {
|
|
63
|
+
const [command, ...commandArgs] = args;
|
|
64
|
+
if (command === undefined || command === "--help" || command === "-h") {
|
|
65
|
+
printHelp();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (command === "--version" || command === "-V") {
|
|
69
|
+
process.stdout.write(`${getVersion()}\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (command === "validate") {
|
|
73
|
+
if (commandArgs.length !== 1) {
|
|
74
|
+
process.stderr.write("Error: validate requires exactly one <network> path.\n");
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const { runValidateCommand } = await import("./validate-command.js");
|
|
79
|
+
process.exitCode = runValidateCommand(commandArgs[0]);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (command === "plan") {
|
|
83
|
+
const { runPlanCommand } = await import("./plan-command.js");
|
|
84
|
+
process.exitCode = runPlanCommand(commandArgs);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (command === "run-network") {
|
|
88
|
+
const { runRunNetworkCommand } = await import("./run-network-command.js");
|
|
89
|
+
process.exitCode = await runRunNetworkCommand(commandArgs);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (command === "poll-requests") {
|
|
93
|
+
const { runPollRequestsCommand } = await import("./poll-requests-command.js");
|
|
94
|
+
process.exitCode = await runPollRequestsCommand(commandArgs);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (command === "resolve-node") {
|
|
98
|
+
const { runResolveNodeCommand } = await import("./resolve-node-command.js");
|
|
99
|
+
process.exitCode = await runResolveNodeCommand(commandArgs);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (command === "record-job-unlock") {
|
|
103
|
+
const { runRecordJobUnlockCommand } = await import("./record-job-unlock-command.js");
|
|
104
|
+
process.exitCode = await runRecordJobUnlockCommand(commandArgs);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (command === "force-unlock-network") {
|
|
108
|
+
const { runForceUnlockNetworkCommand } = await import("./force-unlock-network-command.js");
|
|
109
|
+
process.exitCode = await runForceUnlockNetworkCommand(commandArgs);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (command === "status") {
|
|
113
|
+
const { runStatusCommand } = await import("./status-command.js");
|
|
114
|
+
process.exitCode = await runStatusCommand(commandArgs);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (command === "cancel-run") {
|
|
118
|
+
const { runCancelRunCommand } = await import("./cancel-run-command.js");
|
|
119
|
+
process.exitCode = await runCancelRunCommand(commandArgs);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (command === "archive-run") {
|
|
123
|
+
const { runArchiveRunCommand } = await import("./archive-run-command.js");
|
|
124
|
+
process.exitCode = await runArchiveRunCommand(commandArgs);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
process.stderr.write(`Error: unknown command '${command}'.\n`);
|
|
128
|
+
process.stderr.write("Run 'ksql-flownet --help' for usage.\n");
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
await main(process.argv.slice(2));
|
|
132
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { stableTopologicalSort } from "../dag/topological-sort.js";
|
|
2
|
+
import { resolveBusinessKey } from "../domain/business-key.js";
|
|
3
|
+
import { validateNetworkPath, writeValidationErrors, } from "./validate-command.js";
|
|
4
|
+
function parsePlanArguments(args) {
|
|
5
|
+
let networkPath;
|
|
6
|
+
let scheduledFor;
|
|
7
|
+
let businessKey;
|
|
8
|
+
const errors = [];
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const argument = args[index];
|
|
11
|
+
if (argument === "--scheduled-for" || argument === "--business-key") {
|
|
12
|
+
const value = args[index + 1];
|
|
13
|
+
if (value === undefined || value.startsWith("--")) {
|
|
14
|
+
errors.push(`${argument} requires a value`);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (argument === "--scheduled-for") {
|
|
18
|
+
if (scheduledFor !== undefined)
|
|
19
|
+
errors.push(`${argument} was specified more than once`);
|
|
20
|
+
scheduledFor = value;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
if (businessKey !== undefined)
|
|
24
|
+
errors.push(`${argument} was specified more than once`);
|
|
25
|
+
businessKey = value;
|
|
26
|
+
}
|
|
27
|
+
index += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (argument.startsWith("-")) {
|
|
31
|
+
errors.push(`unknown option '${argument}'`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (networkPath === undefined)
|
|
35
|
+
networkPath = argument;
|
|
36
|
+
else
|
|
37
|
+
errors.push(`unexpected argument '${argument}'`);
|
|
38
|
+
}
|
|
39
|
+
if (networkPath === undefined)
|
|
40
|
+
errors.push("plan requires one <network> path");
|
|
41
|
+
const result = { errors };
|
|
42
|
+
if (networkPath !== undefined)
|
|
43
|
+
Object.assign(result, { networkPath });
|
|
44
|
+
if (scheduledFor !== undefined)
|
|
45
|
+
Object.assign(result, { scheduledFor });
|
|
46
|
+
if (businessKey !== undefined)
|
|
47
|
+
Object.assign(result, { businessKey });
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
export function runPlanCommand(args) {
|
|
51
|
+
const parsed = parsePlanArguments(args);
|
|
52
|
+
if (parsed.errors.length > 0 || parsed.networkPath === undefined) {
|
|
53
|
+
process.stderr.write(`Invalid plan arguments:\n${parsed.errors.map((error) => `- ${error}`).join("\n")}\n`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
const validated = validateNetworkPath(parsed.networkPath);
|
|
57
|
+
const errors = [...validated.errors];
|
|
58
|
+
let businessKey;
|
|
59
|
+
if (validated.definition !== undefined) {
|
|
60
|
+
const input = {
|
|
61
|
+
networkId: validated.definition.network_id,
|
|
62
|
+
policy: validated.definition.business_key_policy,
|
|
63
|
+
...(parsed.scheduledFor === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { scheduledFor: parsed.scheduledFor }),
|
|
66
|
+
...(parsed.businessKey === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { businessKey: parsed.businessKey }),
|
|
69
|
+
};
|
|
70
|
+
const resolved = resolveBusinessKey(input);
|
|
71
|
+
for (const error of resolved.errors) {
|
|
72
|
+
if (!errors.some((existing) => existing.code === error.code &&
|
|
73
|
+
existing.path === error.path &&
|
|
74
|
+
existing.message === error.message)) {
|
|
75
|
+
errors.push(error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
businessKey = resolved.businessKey;
|
|
79
|
+
}
|
|
80
|
+
if (errors.length > 0) {
|
|
81
|
+
writeValidationErrors(errors);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
if (validated.definition === undefined || businessKey === undefined) {
|
|
85
|
+
process.stderr.write("Error: plan could not be produced.\n");
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
const nodeById = new Map(validated.definition.nodes.map((node) => [node.id, node]));
|
|
89
|
+
const order = stableTopologicalSort(validated.definition.nodes).order;
|
|
90
|
+
const lines = order.map((nodeId, index) => {
|
|
91
|
+
const node = nodeById.get(nodeId);
|
|
92
|
+
if (node === undefined)
|
|
93
|
+
throw new Error(`missing sorted node '${nodeId}'`);
|
|
94
|
+
const dependencies = node.depends_on.length === 0 ? "-" : node.depends_on.join(", ");
|
|
95
|
+
return `${index + 1}. ${node.id} | job_id=${node.job_id} | idempotent=${String(node.idempotent)} | depends_on=${dependencies}`;
|
|
96
|
+
});
|
|
97
|
+
process.stdout.write(`Business key: ${businessKey}\nExecution plan:\n${lines.join("\n")}\n`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=plan-command.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { FlownetChildClient } from "../requests/flownet-child-client.js";
|
|
3
|
+
import { KintoneRequestStore } from "../requests/kintone-request-store.js";
|
|
4
|
+
import { DEFAULT_REQUEST_HEARTBEAT_INTERVAL_MS, DEFAULT_REQUEST_STALE_AFTER_MS, loadPollRequestsConfig, } from "../requests/poll-requests-config.js";
|
|
5
|
+
import { pollRequests, } from "../requests/request-poller.js";
|
|
6
|
+
import { requiredEnvironment } from "./resolve-node-command.js";
|
|
7
|
+
export async function runPollRequestsCommand(args, dependencies) {
|
|
8
|
+
const checkOnly = args.length === 1 && args[0] === "--check";
|
|
9
|
+
if (args.length > 0 && !checkOnly) {
|
|
10
|
+
process.stderr.write("Invalid poll-requests arguments: only --check is accepted.\n");
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const resolvedDependencies = dependencies ?? productionDependencies();
|
|
15
|
+
if (checkOnly) {
|
|
16
|
+
await resolvedDependencies.store.listRequested();
|
|
17
|
+
process.stdout.write(`poll-requests check: ok networks=${resolvedDependencies.config.networks.length} request_app=readable\n`);
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const summary = await pollRequests(resolvedDependencies);
|
|
21
|
+
process.stdout.write(`poll-requests: requested=${summary.requested} claimed=${summary.claimed} completed=${summary.completed} cancelled=${summary.cancelled} invalid=${summary.invalid} stale=${summary.stale} skipped=${summary.skippedMalformed}\n`);
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const code = errorCode(error);
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
process.stderr.write(`Error [${code}]: ${message}\n`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function productionDependencies() {
|
|
32
|
+
const baseUrl = requiredEnvironment("KSQL_FLOWNET_BASE_URL").replace(/\/$/u, "");
|
|
33
|
+
const appId = positiveIntegerEnvironment("KSQL_FLOWNET_REQUEST_APP_ID");
|
|
34
|
+
const apiToken = requiredEnvironment("KSQL_FLOWNET_REQUEST_API_TOKEN");
|
|
35
|
+
const heartbeatIntervalMs = integerEnvironment("KSQL_FLOWNET_REQUEST_HEARTBEAT_INTERVAL_MS", DEFAULT_REQUEST_HEARTBEAT_INTERVAL_MS);
|
|
36
|
+
const staleAfterMs = integerEnvironment("KSQL_FLOWNET_REQUEST_STALE_AFTER_MS", DEFAULT_REQUEST_STALE_AFTER_MS);
|
|
37
|
+
const config = loadPollRequestsConfig(requiredEnvironment("KSQL_FLOWNET_REQUEST_ALLOWLIST_PATH"), { heartbeatIntervalMs, staleAfterMs });
|
|
38
|
+
return {
|
|
39
|
+
store: new KintoneRequestStore({ baseUrl, appId, apiToken }),
|
|
40
|
+
child: new FlownetChildClient({
|
|
41
|
+
profile: requiredEnvironment("KSQL_FLOWNET_PROFILE"),
|
|
42
|
+
}),
|
|
43
|
+
config,
|
|
44
|
+
host: process.env.KSQL_FLOWNET_HOST ?? hostname(),
|
|
45
|
+
log(code, detail) {
|
|
46
|
+
process.stderr.write(`Warning [${code}]: ${detail}\n`);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function positiveIntegerEnvironment(name) {
|
|
51
|
+
const value = Number(requiredEnvironment(name));
|
|
52
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
53
|
+
throw new Error(`${name} must be a positive integer`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function integerEnvironment(name, fallback) {
|
|
57
|
+
const raw = process.env[name];
|
|
58
|
+
if (raw === undefined || raw.trim() === "")
|
|
59
|
+
return fallback;
|
|
60
|
+
const value = Number(raw);
|
|
61
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
62
|
+
throw new Error(`${name} must be a positive integer`);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function errorCode(error) {
|
|
66
|
+
if (typeof error === "object" &&
|
|
67
|
+
error !== null &&
|
|
68
|
+
"code" in error &&
|
|
69
|
+
typeof error.code === "string") {
|
|
70
|
+
return error.code;
|
|
71
|
+
}
|
|
72
|
+
return "POLL_REQUESTS_FAILED";
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=poll-requests-command.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PersistenceRepository } from "../persistence/repository.js";
|
|
2
|
+
export interface RecordJobUnlockCommandDependencies {
|
|
3
|
+
repository: PersistenceRepository;
|
|
4
|
+
servicePrincipal: string;
|
|
5
|
+
requestedBy: string;
|
|
6
|
+
readFile?: (path: string) => string;
|
|
7
|
+
now?: () => Date;
|
|
8
|
+
uuid?: () => string;
|
|
9
|
+
}
|
|
10
|
+
export declare function runRecordJobUnlockCommand(args: readonly string[], dependencies?: RecordJobUnlockCommandDependencies): Promise<number>;
|
|
11
|
+
//# sourceMappingURL=record-job-unlock-command.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { recordJobUnlock } from "../orchestration/job-lock-audit.js";
|
|
3
|
+
import { repositoryFromEnvironment, requiredEnvironment, } from "./resolve-node-command.js";
|
|
4
|
+
export async function runRecordJobUnlockCommand(args, dependencies) {
|
|
5
|
+
const parsed = parseArguments(args);
|
|
6
|
+
if (parsed.errors.length > 0) {
|
|
7
|
+
process.stderr.write(`Invalid record-job-unlock arguments:\n${parsed.errors.map((error) => `- ${error}`).join("\n")}\n`);
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const runtime = dependencies ?? {
|
|
12
|
+
repository: repositoryFromEnvironment(),
|
|
13
|
+
servicePrincipal: requiredEnvironment("KSQL_FLOWNET_SERVICE_PRINCIPAL"),
|
|
14
|
+
requestedBy: requiredEnvironment("KSQL_FLOWNET_REQUESTED_BY"),
|
|
15
|
+
};
|
|
16
|
+
const read = runtime.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
17
|
+
const rawResult = read(parsed.values.get("--result-file"));
|
|
18
|
+
const audit = await recordJobUnlock({
|
|
19
|
+
repository: runtime.repository,
|
|
20
|
+
runId: parsed.values.get("--run-id"),
|
|
21
|
+
nodeId: parsed.values.get("--node-id"),
|
|
22
|
+
result: JSON.parse(rawResult),
|
|
23
|
+
reason: read(parsed.values.get("--reason-file")),
|
|
24
|
+
evidenceRef: parsed.values.get("--evidence-ref"),
|
|
25
|
+
servicePrincipal: runtime.servicePrincipal,
|
|
26
|
+
requestedBy: runtime.requestedBy,
|
|
27
|
+
stopConfirmedBy: parsed.values.get("--stop-confirmed-by"),
|
|
28
|
+
...(runtime.now === undefined ? {} : { now: runtime.now }),
|
|
29
|
+
...(runtime.uuid === undefined ? {} : { uuid: runtime.uuid }),
|
|
30
|
+
});
|
|
31
|
+
process.stdout.write(`RECORDED: ${audit.lock_recovery_result.outcome} for ${audit.lock_recovery_result.jobKey} (${audit.event_id}).\n`);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
36
|
+
? String(error.code)
|
|
37
|
+
: "RECORD_JOB_UNLOCK_FAILED";
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
process.stderr.write(`Error [${code}]: ${message}\n`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function parseArguments(args) {
|
|
44
|
+
const values = new Map();
|
|
45
|
+
const errors = [];
|
|
46
|
+
const allowed = new Set([
|
|
47
|
+
"--result-file",
|
|
48
|
+
"--run-id",
|
|
49
|
+
"--node-id",
|
|
50
|
+
"--reason-file",
|
|
51
|
+
"--evidence-ref",
|
|
52
|
+
"--stop-confirmed-by",
|
|
53
|
+
]);
|
|
54
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
55
|
+
const key = args[index];
|
|
56
|
+
if (!key.startsWith("--")) {
|
|
57
|
+
errors.push(`unexpected argument '${key}'`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const value = args[index + 1];
|
|
61
|
+
if (value === undefined || value.startsWith("--")) {
|
|
62
|
+
errors.push(`${key} requires a value`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!allowed.has(key))
|
|
66
|
+
errors.push(`unknown option '${key}'`);
|
|
67
|
+
if (values.has(key))
|
|
68
|
+
errors.push(`${key} was specified more than once`);
|
|
69
|
+
values.set(key, value);
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
for (const key of allowed) {
|
|
73
|
+
if (!values.has(key))
|
|
74
|
+
errors.push(`${key} is required`);
|
|
75
|
+
}
|
|
76
|
+
return { values, errors };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=record-job-unlock-command.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { KintonePersistenceRepository } from "../persistence/kintone/repository.js";
|
|
2
|
+
import type { PersistenceRepository } from "../persistence/repository.js";
|
|
3
|
+
export interface ResolveNodeCommandDependencies {
|
|
4
|
+
repository: PersistenceRepository;
|
|
5
|
+
servicePrincipal: string;
|
|
6
|
+
requestedBy: string;
|
|
7
|
+
readFile?: (path: string) => string;
|
|
8
|
+
now?: () => Date;
|
|
9
|
+
}
|
|
10
|
+
export declare function runResolveNodeCommand(args: readonly string[], dependencies?: ResolveNodeCommandDependencies): Promise<number>;
|
|
11
|
+
export declare function repositoryFromEnvironment(): KintonePersistenceRepository;
|
|
12
|
+
export declare function requiredEnvironment(name: string): string;
|
|
13
|
+
//# sourceMappingURL=resolve-node-command.d.ts.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolveNode, } from "../orchestration/resolve-node.js";
|
|
3
|
+
import { KintonePersistenceRepository } from "../persistence/kintone/repository.js";
|
|
4
|
+
export async function runResolveNodeCommand(args, dependencies) {
|
|
5
|
+
const parsed = parseArguments(args);
|
|
6
|
+
if (parsed.errors.length > 0) {
|
|
7
|
+
process.stderr.write(`Invalid resolve-node arguments:\n${parsed.errors.map((error) => `- ${error}`).join("\n")}\n`);
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const runtime = dependencies ?? productionDependencies();
|
|
12
|
+
const reason = (runtime.readFile ?? ((path) => readFileSync(path, "utf8")))(parsed.reasonFile);
|
|
13
|
+
const result = await resolveNode({
|
|
14
|
+
repository: runtime.repository,
|
|
15
|
+
runId: parsed.runId,
|
|
16
|
+
nodeId: parsed.nodeId,
|
|
17
|
+
outcome: parsed.outcome,
|
|
18
|
+
resolutionType: parsed.manualCompletion
|
|
19
|
+
? "NODE_MANUAL_COMPLETION_CONFIRMED"
|
|
20
|
+
: parsed.compensation
|
|
21
|
+
? "NODE_COMPENSATION_COMPLETED"
|
|
22
|
+
: "OUTCOME_CONFIRMED",
|
|
23
|
+
reason,
|
|
24
|
+
evidenceRef: parsed.evidenceRef,
|
|
25
|
+
servicePrincipal: runtime.servicePrincipal,
|
|
26
|
+
requestedBy: runtime.requestedBy,
|
|
27
|
+
approvedBy: parsed.approvedBy ?? "",
|
|
28
|
+
stopConfirmedBy: parsed.stopConfirmedBy,
|
|
29
|
+
stopEvidenceRef: parsed.stopEvidenceRef,
|
|
30
|
+
...(runtime.now === undefined ? {} : { now: runtime.now }),
|
|
31
|
+
});
|
|
32
|
+
process.stdout.write(`RESOLVED: ${parsed.runId}/${parsed.nodeId} -> ${result.nodeState.value.status} (revision ${result.nodeState.revision}).\n`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const code = errorCode(error, "RESOLVE_NODE_FAILED");
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
process.stderr.write(`Error [${code}]: ${message}\n`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function parseArguments(args) {
|
|
43
|
+
const result = {
|
|
44
|
+
manualCompletion: false,
|
|
45
|
+
compensation: false,
|
|
46
|
+
errors: [],
|
|
47
|
+
};
|
|
48
|
+
const values = new Map();
|
|
49
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
50
|
+
const argument = args[index];
|
|
51
|
+
if (argument === "--manual-completion" || argument === "--compensation") {
|
|
52
|
+
const key = argument.slice(2);
|
|
53
|
+
if (key === "manual-completion") {
|
|
54
|
+
if (result.manualCompletion)
|
|
55
|
+
result.errors.push(`${argument} was specified more than once`);
|
|
56
|
+
result.manualCompletion = true;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
if (result.compensation)
|
|
60
|
+
result.errors.push(`${argument} was specified more than once`);
|
|
61
|
+
result.compensation = true;
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!argument.startsWith("--")) {
|
|
66
|
+
result.errors.push(`unexpected argument '${argument}'`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const value = args[index + 1];
|
|
70
|
+
if (value === undefined || value.startsWith("--")) {
|
|
71
|
+
result.errors.push(`${argument} requires a value`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (values.has(argument))
|
|
75
|
+
result.errors.push(`${argument} was specified more than once`);
|
|
76
|
+
values.set(argument, value);
|
|
77
|
+
index += 1;
|
|
78
|
+
}
|
|
79
|
+
const allowed = new Set([
|
|
80
|
+
"--run-id",
|
|
81
|
+
"--node-id",
|
|
82
|
+
"--to",
|
|
83
|
+
"--reason-file",
|
|
84
|
+
"--evidence-ref",
|
|
85
|
+
"--stop-confirmed-by",
|
|
86
|
+
"--stop-evidence-ref",
|
|
87
|
+
"--approved-by",
|
|
88
|
+
]);
|
|
89
|
+
for (const key of values.keys()) {
|
|
90
|
+
if (!allowed.has(key))
|
|
91
|
+
result.errors.push(`unknown option '${key}'`);
|
|
92
|
+
}
|
|
93
|
+
for (const key of [
|
|
94
|
+
"--run-id",
|
|
95
|
+
"--node-id",
|
|
96
|
+
"--to",
|
|
97
|
+
"--reason-file",
|
|
98
|
+
"--evidence-ref",
|
|
99
|
+
"--stop-confirmed-by",
|
|
100
|
+
"--stop-evidence-ref",
|
|
101
|
+
]) {
|
|
102
|
+
if (!values.has(key))
|
|
103
|
+
result.errors.push(`${key} is required`);
|
|
104
|
+
}
|
|
105
|
+
const outcome = values.get("--to");
|
|
106
|
+
if (outcome !== undefined &&
|
|
107
|
+
outcome !== "SUCCESS" &&
|
|
108
|
+
outcome !== "FAILED" &&
|
|
109
|
+
outcome !== "CANCELLED") {
|
|
110
|
+
result.errors.push("--to must be SUCCESS, FAILED, or CANCELLED");
|
|
111
|
+
}
|
|
112
|
+
if (result.manualCompletion && result.compensation)
|
|
113
|
+
result.errors.push("--manual-completion and --compensation are mutually exclusive");
|
|
114
|
+
return {
|
|
115
|
+
...result,
|
|
116
|
+
...(values.has("--run-id") ? { runId: values.get("--run-id") } : {}),
|
|
117
|
+
...(values.has("--node-id") ? { nodeId: values.get("--node-id") } : {}),
|
|
118
|
+
...(outcome === "SUCCESS" || outcome === "FAILED" || outcome === "CANCELLED"
|
|
119
|
+
? { outcome }
|
|
120
|
+
: {}),
|
|
121
|
+
...(values.has("--reason-file")
|
|
122
|
+
? { reasonFile: values.get("--reason-file") }
|
|
123
|
+
: {}),
|
|
124
|
+
...(values.has("--evidence-ref")
|
|
125
|
+
? { evidenceRef: values.get("--evidence-ref") }
|
|
126
|
+
: {}),
|
|
127
|
+
...(values.has("--stop-confirmed-by")
|
|
128
|
+
? { stopConfirmedBy: values.get("--stop-confirmed-by") }
|
|
129
|
+
: {}),
|
|
130
|
+
...(values.has("--stop-evidence-ref")
|
|
131
|
+
? { stopEvidenceRef: values.get("--stop-evidence-ref") }
|
|
132
|
+
: {}),
|
|
133
|
+
...(values.has("--approved-by")
|
|
134
|
+
? { approvedBy: values.get("--approved-by") }
|
|
135
|
+
: {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function productionDependencies() {
|
|
139
|
+
return {
|
|
140
|
+
repository: repositoryFromEnvironment(),
|
|
141
|
+
servicePrincipal: requiredEnvironment("KSQL_FLOWNET_SERVICE_PRINCIPAL"),
|
|
142
|
+
requestedBy: requiredEnvironment("KSQL_FLOWNET_REQUESTED_BY"),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export function repositoryFromEnvironment() {
|
|
146
|
+
return new KintonePersistenceRepository({
|
|
147
|
+
baseUrl: requiredEnvironment("KSQL_FLOWNET_BASE_URL").replace(/\/$/u, ""),
|
|
148
|
+
stateAppId: positiveIntegerEnvironment("KSQL_FLOWNET_STATE_APP_ID"),
|
|
149
|
+
stateApiToken: requiredEnvironment("KSQL_FLOWNET_STATE_API_TOKEN"),
|
|
150
|
+
auditAppId: positiveIntegerEnvironment("KSQL_FLOWNET_AUDIT_APP_ID"),
|
|
151
|
+
auditApiToken: requiredEnvironment("KSQL_FLOWNET_AUDIT_API_TOKEN"),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
export function requiredEnvironment(name) {
|
|
155
|
+
const value = process.env[name];
|
|
156
|
+
if (value === undefined || value.trim() === "")
|
|
157
|
+
throw new Error(`${name} is required`);
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function positiveIntegerEnvironment(name) {
|
|
161
|
+
const value = Number(requiredEnvironment(name));
|
|
162
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
163
|
+
throw new Error(`${name} must be a positive integer`);
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function errorCode(error, fallback) {
|
|
167
|
+
return typeof error === "object" &&
|
|
168
|
+
error !== null &&
|
|
169
|
+
"code" in error &&
|
|
170
|
+
typeof error.code === "string"
|
|
171
|
+
? error.code
|
|
172
|
+
: fallback;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=resolve-node-command.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ensureRun, type EnsureRunInput } from "../orchestration/ensure-run.js";
|
|
2
|
+
import { type SequentialSchedulerSummary } from "../orchestration/sequential-scheduler.js";
|
|
3
|
+
export type EnsureRunInvoker = (input: Omit<EnsureRunInput, "repository" | "lockManager" | "executor" | "bundleStore">) => ReturnType<typeof ensureRun>;
|
|
4
|
+
export interface RunNetworkCommandDependencies {
|
|
5
|
+
readonly invoke: EnsureRunInvoker;
|
|
6
|
+
readonly schedule: (result: Awaited<ReturnType<EnsureRunInvoker>>) => Promise<SequentialSchedulerSummary>;
|
|
7
|
+
readonly profile: string;
|
|
8
|
+
readonly requestedBy: string;
|
|
9
|
+
readonly host: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runRunNetworkCommand(args: readonly string[], dependencies?: RunNetworkCommandDependencies): Promise<number>;
|
|
12
|
+
export declare function resolveOwnerInstanceId(host: string, configured?: string | undefined, pid?: number): string;
|
|
13
|
+
export declare function ksqlFlowBinArgsEnvironment(environment?: NodeJS.ProcessEnv): string[];
|
|
14
|
+
//# sourceMappingURL=run-network-command.d.ts.map
|