blokctl 1.1.0 → 1.3.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/dist/commands/create/project.d.ts +5 -0
- package/dist/commands/create/project.js +164 -27
- package/dist/commands/create/utils/Examples.d.ts +4 -4
- package/dist/commands/create/utils/Examples.js +77 -105
- package/dist/commands/create/workflow.js +8 -6
- package/dist/commands/dev/index.js +39 -1
- package/dist/commands/install/node.d.ts +1 -0
- package/dist/commands/install/node.js +3 -39
- package/dist/commands/migrate/index.js +25 -0
- package/dist/commands/migrate/nodesTs.d.ts +31 -0
- package/dist/commands/migrate/nodesTs.js +484 -0
- package/dist/commands/migrate/refs.d.ts +15 -0
- package/dist/commands/migrate/refs.js +706 -0
- package/dist/commands/nodes/index.js +10 -0
- package/dist/commands/nodes/listNodes.d.ts +2 -0
- package/dist/commands/nodes/listNodes.js +20 -15
- package/dist/commands/nodes/syncNodes.d.ts +7 -0
- package/dist/commands/nodes/syncNodes.js +138 -0
- package/dist/commands/nodes/syncNodes.test.d.ts +1 -0
- package/dist/commands/nodes/syncNodes.test.js +322 -0
- package/dist/commands/publish/workflow.d.ts +1 -0
- package/dist/commands/publish/workflow.js +9 -0
- package/dist/commands/publish/workflow.test.d.ts +1 -0
- package/dist/commands/publish/workflow.test.js +22 -0
- package/dist/commands/runtime/add.js +92 -29
- package/dist/commands/runtime/index.js +1 -0
- package/dist/services/observability-stack.real-prometheus.test.d.ts +1 -0
- package/dist/services/observability-stack.real-prometheus.test.js +95 -0
- package/dist/services/observability-stack.real-tempo.test.d.ts +1 -0
- package/dist/services/observability-stack.real-tempo.test.js +86 -0
- package/dist/services/runtime-detector.d.ts +2 -0
- package/dist/services/runtime-detector.js +35 -11
- package/dist/services/runtime-setup.d.ts +1 -0
- package/dist/services/runtime-setup.js +17 -7
- package/dist/studio-dist/assets/index-QEvKRE4T.js +42 -0
- package/dist/studio-dist/index.html +1 -1
- package/package.json +3 -2
- package/dist/studio-dist/assets/index-CnFqCRQe.js +0 -42
|
@@ -5,7 +5,7 @@ import color from "picocolors";
|
|
|
5
5
|
import { isNonInteractive } from "../../services/non-interactive.js";
|
|
6
6
|
import { detectRuntimes, getRuntimeDefinition } from "../../services/runtime-detector.js";
|
|
7
7
|
import { ensureRuntimeGitignore, rewriteRuntimeEnvBlock, rewriteSupervisordRuntimes, runtimeEnvKey, withRuntime, } from "../../services/runtime-mutations.js";
|
|
8
|
-
import { setupRuntime } from "../../services/runtime-setup.js";
|
|
8
|
+
import { buildRuntimeConfig, setupRuntime, } from "../../services/runtime-setup.js";
|
|
9
9
|
import { RuntimeCommandError, assertGrpcPortFree, assertSidecarKind, readConfigSafe, reportRuntimeError, resolveProjectRoot, resolveSdkSource, } from "./shared.js";
|
|
10
10
|
export async function runtimeAdd(kindArg, options) {
|
|
11
11
|
try {
|
|
@@ -54,6 +54,31 @@ export async function runtimeAdd(kindArg, options) {
|
|
|
54
54
|
const sdkDir = path.join(root, ".blok", "runtimes", kind);
|
|
55
55
|
const alreadyInstalled = Boolean(config.runtimes?.[kind]) || fs.existsSync(sdkDir);
|
|
56
56
|
p.intro(color.inverse(` Add ${def.label} runtime `));
|
|
57
|
+
if (options.enable === true) {
|
|
58
|
+
if (config.runtimes?.[kind]) {
|
|
59
|
+
p.outro(color.dim(`${def.label} is already wired into .blok/config.json.`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!fs.existsSync(sdkDir)) {
|
|
63
|
+
throw new RuntimeCommandError(`${def.label} isn't scaffolded at ${path.relative(root, sdkDir)}. Run \`blokctl runtime add ${kind}\` (without --enable) to install it first.`);
|
|
64
|
+
}
|
|
65
|
+
const rt = (detected ?? (await detectRuntimes())).find((d) => d.kind === kind);
|
|
66
|
+
if (!rt)
|
|
67
|
+
throw new RuntimeCommandError(`Unknown runtime "${kind}".`);
|
|
68
|
+
const grpcPort = grpcPortOverride ?? rt.defaultGrpcPort;
|
|
69
|
+
const clash = Object.values(config.runtimes ?? {}).find((rc) => rc.kind !== kind && rc.grpcPort === grpcPort);
|
|
70
|
+
if (clash) {
|
|
71
|
+
throw new RuntimeCommandError(`gRPC port ${grpcPort} is already used by the ${clash.label} runtime. Pass --grpc-port <n> to pick another.`);
|
|
72
|
+
}
|
|
73
|
+
const rc = buildRuntimeConfig(rt, root);
|
|
74
|
+
if (grpcPortOverride !== undefined) {
|
|
75
|
+
rc.grpcPort = grpcPortOverride;
|
|
76
|
+
if (rc.grpcStartCmd)
|
|
77
|
+
rc.grpcStartCmd = rc.grpcStartCmd.split(String(rt.defaultGrpcPort)).join(String(grpcPortOverride));
|
|
78
|
+
}
|
|
79
|
+
finalizeRuntime(root, config, rc, kind, def.label);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
57
82
|
if (alreadyInstalled && options.force !== true) {
|
|
58
83
|
if (nonInteractive) {
|
|
59
84
|
p.outro(color.dim(`${def.label} is already installed. Re-run with --force to reinstall.`));
|
|
@@ -107,37 +132,75 @@ export async function runtimeAdd(kindArg, options) {
|
|
|
107
132
|
rc.grpcStartCmd = rc.grpcStartCmd.split(String(rt.defaultGrpcPort)).join(String(grpcPortOverride));
|
|
108
133
|
}
|
|
109
134
|
s.stop(`${def.label} runtime ready`);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
fs.mkdirSync(path.join(root, ".blok"), { recursive: true });
|
|
113
|
-
fs.writeFileSync(path.join(root, ".blok", "config.json"), `${JSON.stringify(nextConfig, null, 2)}\n`);
|
|
114
|
-
const envPath = path.join(root, ".env.local");
|
|
115
|
-
const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
116
|
-
fs.writeFileSync(envPath, rewriteRuntimeEnvBlock(envContent, remaining));
|
|
117
|
-
const supervisordPath = path.join(root, "supervisord.conf");
|
|
118
|
-
if (fs.existsSync(supervisordPath)) {
|
|
119
|
-
fs.writeFileSync(supervisordPath, rewriteSupervisordRuntimes(fs.readFileSync(supervisordPath, "utf8"), remaining));
|
|
120
|
-
}
|
|
121
|
-
const gitignorePath = path.join(root, ".gitignore");
|
|
122
|
-
if (fs.existsSync(gitignorePath)) {
|
|
123
|
-
const before = fs.readFileSync(gitignorePath, "utf8");
|
|
124
|
-
const after = ensureRuntimeGitignore(before);
|
|
125
|
-
if (after !== before)
|
|
126
|
-
fs.writeFileSync(gitignorePath, after);
|
|
135
|
+
if (stampHelloExample(root, source, kind)) {
|
|
136
|
+
p.log.success(`Shipped the ${def.label} hello example — ${color.cyan(`POST /runtimes/${kind}/hello`)} (registered in src/Workflows.ts)`);
|
|
127
137
|
}
|
|
128
|
-
|
|
129
|
-
`${color.green("✓")} .blok/config.json ${color.dim(`runtimes.${kind}`)}`,
|
|
130
|
-
`${color.green("✓")} .env.local ${color.dim(`RUNTIME_${runtimeEnvKey(kind)}_GRPC_PORT=${rc.grpcPort}`)}`,
|
|
131
|
-
fs.existsSync(supervisordPath)
|
|
132
|
-
? `${color.green("✓")} supervisord.conf ${color.dim(`[program:${kind}_runtime]`)}`
|
|
133
|
-
: "",
|
|
134
|
-
`${color.green("✓")} runtimes/${kind}/nodes/ ${color.dim("(your runtime nodes go here)")}`,
|
|
135
|
-
]
|
|
136
|
-
.filter(Boolean)
|
|
137
|
-
.join("\n"), `${def.label} added`);
|
|
138
|
-
p.outro(`Run ${color.cyan("blokctl dev")} to start it, then add ${color.cyan(`type: "runtime.${kind}"`)} steps to your workflows.`);
|
|
138
|
+
finalizeRuntime(root, config, rc, kind, def.label);
|
|
139
139
|
}
|
|
140
140
|
catch (err) {
|
|
141
141
|
reportRuntimeError(err);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
function stampHelloExample(root, source, kind) {
|
|
145
|
+
const file = `runtime-${kind}-hello.ts`;
|
|
146
|
+
const srcFile = path.join(source, "examples", "ts-workflows", file);
|
|
147
|
+
const examplesDir = path.join(root, "src", "workflows", "examples");
|
|
148
|
+
const workflowsTs = path.join(root, "src", "Workflows.ts");
|
|
149
|
+
if (!fs.existsSync(srcFile) || !fs.existsSync(examplesDir) || !fs.existsSync(workflowsTs))
|
|
150
|
+
return false;
|
|
151
|
+
const importName = `Runtime${kind.charAt(0).toUpperCase()}${kind.slice(1)}Hello`;
|
|
152
|
+
const importLine = `import ${importName} from "./workflows/examples/runtime-${kind}-hello";`;
|
|
153
|
+
const entryLine = `\t"runtime-${kind}-hello": ${importName},`;
|
|
154
|
+
const content = fs.readFileSync(workflowsTs, "utf8");
|
|
155
|
+
if (content.includes(`./workflows/examples/runtime-${kind}-hello`)) {
|
|
156
|
+
fs.copyFileSync(srcFile, path.join(examplesDir, file));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
const lines = content.split("\n");
|
|
160
|
+
const openIdx = lines.findIndex((l) => /^const workflows\b.*=\s*\{\s*$/.test(l));
|
|
161
|
+
let lastImport = -1;
|
|
162
|
+
for (let i = 0; i < (openIdx === -1 ? lines.length : openIdx); i++) {
|
|
163
|
+
if (/^import /.test(lines[i]))
|
|
164
|
+
lastImport = i;
|
|
165
|
+
}
|
|
166
|
+
if (openIdx === -1 || lastImport === -1) {
|
|
167
|
+
p.log.warn(`src/Workflows.ts doesn't look generated — register the ${kind} hello example yourself:\n ${importLine}\n ${entryLine.trim()}`);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
lines.splice(openIdx + 1, 0, entryLine);
|
|
171
|
+
lines.splice(lastImport + 1, 0, importLine);
|
|
172
|
+
fs.copyFileSync(srcFile, path.join(examplesDir, file));
|
|
173
|
+
fs.writeFileSync(workflowsTs, lines.join("\n"));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
function finalizeRuntime(root, config, rc, kind, label) {
|
|
177
|
+
const nextConfig = withRuntime(config, rc);
|
|
178
|
+
const remaining = Object.values(nextConfig.runtimes ?? {});
|
|
179
|
+
fs.mkdirSync(path.join(root, ".blok"), { recursive: true });
|
|
180
|
+
fs.writeFileSync(path.join(root, ".blok", "config.json"), `${JSON.stringify(nextConfig, null, 2)}\n`);
|
|
181
|
+
const envPath = path.join(root, ".env.local");
|
|
182
|
+
const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
183
|
+
fs.writeFileSync(envPath, rewriteRuntimeEnvBlock(envContent, remaining));
|
|
184
|
+
const supervisordPath = path.join(root, "supervisord.conf");
|
|
185
|
+
if (fs.existsSync(supervisordPath)) {
|
|
186
|
+
fs.writeFileSync(supervisordPath, rewriteSupervisordRuntimes(fs.readFileSync(supervisordPath, "utf8"), remaining));
|
|
187
|
+
}
|
|
188
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
189
|
+
if (fs.existsSync(gitignorePath)) {
|
|
190
|
+
const before = fs.readFileSync(gitignorePath, "utf8");
|
|
191
|
+
const after = ensureRuntimeGitignore(before);
|
|
192
|
+
if (after !== before)
|
|
193
|
+
fs.writeFileSync(gitignorePath, after);
|
|
194
|
+
}
|
|
195
|
+
p.note([
|
|
196
|
+
`${color.green("✓")} .blok/config.json ${color.dim(`runtimes.${kind}`)}`,
|
|
197
|
+
`${color.green("✓")} .env.local ${color.dim(`RUNTIME_${runtimeEnvKey(kind)}_GRPC_PORT=${rc.grpcPort}`)}`,
|
|
198
|
+
fs.existsSync(supervisordPath)
|
|
199
|
+
? `${color.green("✓")} supervisord.conf ${color.dim(`[program:${kind}_runtime]`)}`
|
|
200
|
+
: "",
|
|
201
|
+
`${color.green("✓")} runtimes/${kind}/nodes/ ${color.dim("(your runtime nodes go here)")}`,
|
|
202
|
+
]
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.join("\n"), `${label} added`);
|
|
205
|
+
p.outro(`Run ${color.cyan("blokctl dev")} to start it, then add ${color.cyan(`type: "runtime.${kind}"`)} steps to your workflows.`);
|
|
206
|
+
}
|
|
@@ -15,6 +15,7 @@ runtime
|
|
|
15
15
|
.option("--local <path>", "Use a local blok repo for SDK source instead of fetching by version")
|
|
16
16
|
.option("--grpc-port <port>", "Override the gRPC port for this runtime")
|
|
17
17
|
.option("--force", "Reinstall if the runtime is already present")
|
|
18
|
+
.option("--enable", "Wire an already-scaffolded runtime (SDK dir on disk) into config.json without reinstalling")
|
|
18
19
|
.option("--skip-toolchain-check", "Add even if the language toolchain isn't detected")
|
|
19
20
|
.option("-y, --yes", "Skip prompts (non-interactive)")
|
|
20
21
|
.action(async (runtimeArg, options) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
const RUN = process.env.BLOK_INTEGRATION_PROMETHEUS;
|
|
5
|
+
const d = RUN ? describe : describe.skip;
|
|
6
|
+
const PROMETHEUS_API = process.env.BLOK_PROMETHEUS_API_URL ?? "http://localhost:9090";
|
|
7
|
+
function run(command, args) {
|
|
8
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
9
|
+
if (result.status !== 0)
|
|
10
|
+
throw new Error(result.stderr || result.stdout || `${command} ${args.join(" ")} failed`);
|
|
11
|
+
return `${result.stdout}\n${result.stderr}`;
|
|
12
|
+
}
|
|
13
|
+
function removeContainer(name) {
|
|
14
|
+
spawnSync("docker", ["rm", "-f", name], { encoding: "utf8" });
|
|
15
|
+
}
|
|
16
|
+
async function queryPrometheus(query) {
|
|
17
|
+
const response = await fetch(`${PROMETHEUS_API}/api/v1/query?query=${encodeURIComponent(query)}`);
|
|
18
|
+
expect(response.status).toBe(200);
|
|
19
|
+
return (await response.json());
|
|
20
|
+
}
|
|
21
|
+
async function activeTargets() {
|
|
22
|
+
const response = await fetch(`${PROMETHEUS_API}/api/v1/targets?state=active`);
|
|
23
|
+
expect(response.status).toBe(200);
|
|
24
|
+
const body = (await response.json());
|
|
25
|
+
return body.data?.activeTargets ?? [];
|
|
26
|
+
}
|
|
27
|
+
async function waitForTarget(instance) {
|
|
28
|
+
let target;
|
|
29
|
+
for (let i = 0; i < 30; i++) {
|
|
30
|
+
target = (await activeTargets()).find((candidate) => candidate.labels.instance === instance);
|
|
31
|
+
if (target?.health === "up")
|
|
32
|
+
return target;
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
34
|
+
}
|
|
35
|
+
return target;
|
|
36
|
+
}
|
|
37
|
+
async function waitForQuery(query) {
|
|
38
|
+
let last;
|
|
39
|
+
for (let i = 0; i < 30; i++) {
|
|
40
|
+
last = await queryPrometheus(query);
|
|
41
|
+
if ((last.data?.result?.length ?? 0) > 0)
|
|
42
|
+
return last;
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
44
|
+
}
|
|
45
|
+
return last ?? queryPrometheus(query);
|
|
46
|
+
}
|
|
47
|
+
d("observability stack - Prometheus live", () => {
|
|
48
|
+
it("scrapes the gRPC metrics target on the shared metrics port", async () => {
|
|
49
|
+
const name = `blok-prom-target-${randomBytes(4).toString("hex")}`;
|
|
50
|
+
const metricsServer = [
|
|
51
|
+
"from http.server import BaseHTTPRequestHandler, HTTPServer",
|
|
52
|
+
"class H(BaseHTTPRequestHandler):",
|
|
53
|
+
" def log_message(self, *args): pass",
|
|
54
|
+
" def do_GET(self):",
|
|
55
|
+
" body = b'# TYPE blok_prom_port_live_test_total counter\\nblok_prom_port_live_test_total 7\\n'",
|
|
56
|
+
" self.send_response(200)",
|
|
57
|
+
" self.send_header('Content-Type', 'text/plain; version=0.0.4')",
|
|
58
|
+
" self.send_header('Content-Length', str(len(body)))",
|
|
59
|
+
" self.end_headers()",
|
|
60
|
+
" self.wfile.write(body)",
|
|
61
|
+
"HTTPServer(('0.0.0.0', 9464), H).serve_forever()",
|
|
62
|
+
].join("\n");
|
|
63
|
+
try {
|
|
64
|
+
run("docker", [
|
|
65
|
+
"run",
|
|
66
|
+
"--rm",
|
|
67
|
+
"-d",
|
|
68
|
+
"--name",
|
|
69
|
+
name,
|
|
70
|
+
"--network",
|
|
71
|
+
"shared-network",
|
|
72
|
+
"--network-alias",
|
|
73
|
+
"blok-grpc",
|
|
74
|
+
"python:3.12-alpine",
|
|
75
|
+
"python",
|
|
76
|
+
"-c",
|
|
77
|
+
metricsServer,
|
|
78
|
+
]);
|
|
79
|
+
const target = await waitForTarget("blok-grpc:9464");
|
|
80
|
+
expect(target?.scrapeUrl).toBe("http://blok-grpc:9464/metrics");
|
|
81
|
+
expect(target?.health).toBe("up");
|
|
82
|
+
const up = await waitForQuery('up{job="blok-grpc",instance="blok-grpc:9464"} == 1');
|
|
83
|
+
expect(up.status).toBe("success");
|
|
84
|
+
expect(up.data?.result).toHaveLength(1);
|
|
85
|
+
expect(up.data?.result?.[0]?.value[1]).toBe("1");
|
|
86
|
+
const metric = await waitForQuery("blok_prom_port_live_test_total");
|
|
87
|
+
expect(metric.status).toBe("success");
|
|
88
|
+
expect(metric.data?.result).toHaveLength(1);
|
|
89
|
+
expect(metric.data?.result?.[0]?.value[1]).toBe("7");
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
removeContainer(name);
|
|
93
|
+
}
|
|
94
|
+
}, 60_000);
|
|
95
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
const RUN = process.env.BLOK_INTEGRATION_TEMPO;
|
|
7
|
+
const d = RUN ? describe : describe.skip;
|
|
8
|
+
const TEMPO_API = process.env.BLOK_TEMPO_API_URL ?? "http://localhost:3201";
|
|
9
|
+
const TEMPO_OTLP = process.env.BLOK_TEMPO_OTLP_URL ?? "http://localhost:4318/v1/traces";
|
|
10
|
+
function findRepoRoot() {
|
|
11
|
+
let dir = import.meta.dirname;
|
|
12
|
+
for (let i = 0; i < 8; i++) {
|
|
13
|
+
if (fs.existsSync(path.join(dir, "infra", "metrics", "tempo.yaml")))
|
|
14
|
+
return dir;
|
|
15
|
+
const up = path.dirname(dir);
|
|
16
|
+
if (up === dir)
|
|
17
|
+
break;
|
|
18
|
+
dir = up;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function hex(bytes) {
|
|
23
|
+
return randomBytes(bytes).toString("hex");
|
|
24
|
+
}
|
|
25
|
+
function dockerLogs(container) {
|
|
26
|
+
const result = spawnSync("docker", ["logs", container], { encoding: "utf8" });
|
|
27
|
+
if (result.status !== 0)
|
|
28
|
+
throw new Error(result.stderr || result.stdout || `docker logs ${container} failed`);
|
|
29
|
+
return `${result.stdout}\n${result.stderr}`;
|
|
30
|
+
}
|
|
31
|
+
async function waitForTrace(traceId) {
|
|
32
|
+
let last;
|
|
33
|
+
for (let i = 0; i < 20; i++) {
|
|
34
|
+
last = await fetch(`${TEMPO_API}/api/traces/${traceId}`);
|
|
35
|
+
if (last.ok)
|
|
36
|
+
return last;
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
38
|
+
}
|
|
39
|
+
return last ?? fetch(`${TEMPO_API}/api/traces/${traceId}`);
|
|
40
|
+
}
|
|
41
|
+
d("observability stack - Tempo live", () => {
|
|
42
|
+
it("boots without the dangling memcached cache and ingests an OTLP span", async () => {
|
|
43
|
+
const repo = findRepoRoot();
|
|
44
|
+
if (!repo)
|
|
45
|
+
throw new Error("repo root not found");
|
|
46
|
+
const tempoConfig = fs.readFileSync(path.join(repo, "infra", "metrics", "tempo.yaml"), "utf8");
|
|
47
|
+
expect(tempoConfig).not.toContain("memcached:11211");
|
|
48
|
+
const traceId = hex(16);
|
|
49
|
+
const spanId = hex(8);
|
|
50
|
+
const now = BigInt(Date.now()) * 1000000n;
|
|
51
|
+
const response = await fetch(TEMPO_OTLP, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
resourceSpans: [
|
|
56
|
+
{
|
|
57
|
+
resource: { attributes: [{ key: "service.name", value: { stringValue: "blok-tempo-live-test" } }] },
|
|
58
|
+
scopeSpans: [
|
|
59
|
+
{
|
|
60
|
+
scope: { name: "blokctl-live-test" },
|
|
61
|
+
spans: [
|
|
62
|
+
{
|
|
63
|
+
traceId,
|
|
64
|
+
spanId,
|
|
65
|
+
name: "tempo-memcached-regression",
|
|
66
|
+
kind: 1,
|
|
67
|
+
startTimeUnixNano: now.toString(),
|
|
68
|
+
endTimeUnixNano: (now + 1000000n).toString(),
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
expect(response.status).toBe(200);
|
|
78
|
+
const trace = await waitForTrace(traceId);
|
|
79
|
+
expect(trace.ok).toBe(true);
|
|
80
|
+
const traceBody = await trace.text();
|
|
81
|
+
expect(traceBody).toContain("blok-tempo-live-test");
|
|
82
|
+
expect(traceBody).toContain("tempo-memcached-regression");
|
|
83
|
+
const logs = dockerLogs("tempo");
|
|
84
|
+
expect(logs.toLowerCase()).not.toContain("memcached");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -3,6 +3,7 @@ export interface RuntimeInfo {
|
|
|
3
3
|
label: string;
|
|
4
4
|
available: boolean;
|
|
5
5
|
version?: string;
|
|
6
|
+
minVersion?: string;
|
|
6
7
|
installHint: string;
|
|
7
8
|
defaultPort: number;
|
|
8
9
|
defaultGrpcPort: number;
|
|
@@ -21,6 +22,7 @@ export interface RuntimeInfo {
|
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
export declare function detectRr(): string | null;
|
|
25
|
+
export declare function detectJava(): string | null;
|
|
24
26
|
export declare function detectRuntimes(): Promise<RuntimeInfo[]>;
|
|
25
27
|
export declare function detectRuntimeVersion(kind: string): Promise<string | undefined>;
|
|
26
28
|
export declare function getRuntimeDefinition(kind: string): Omit<RuntimeInfo, "available" | "version"> | undefined;
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import child_process from "node:child_process";
|
|
2
2
|
import util from "node:util";
|
|
3
|
+
import { compareSemver } from "./semver-utils.js";
|
|
3
4
|
const exec = util.promisify(child_process.exec);
|
|
4
5
|
export function detectRr() {
|
|
5
6
|
for (const bin of ["/opt/homebrew/bin/rr", "rr"]) {
|
|
7
|
+
try {
|
|
8
|
+
const out = child_process
|
|
9
|
+
.execSync(`${bin} --version`, { stdio: ["ignore", "pipe", "ignore"] })
|
|
10
|
+
.toString()
|
|
11
|
+
.trim();
|
|
12
|
+
if (/^rr version/i.test(out))
|
|
13
|
+
return bin;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function detectJava() {
|
|
21
|
+
for (const bin of ["java", "/opt/homebrew/opt/openjdk/bin/java"]) {
|
|
6
22
|
try {
|
|
7
23
|
child_process.execSync(`${bin} --version`, { stdio: "ignore" });
|
|
8
24
|
return bin;
|
|
@@ -45,8 +61,9 @@ const RUNTIME_DEFINITIONS = [
|
|
|
45
61
|
defaultGrpcPort: 10002,
|
|
46
62
|
commands: ["rustc --version"],
|
|
47
63
|
toolchain: "rustc + cargo",
|
|
48
|
-
installDeps: "cargo build --
|
|
64
|
+
installDeps: "cargo build --features grpc",
|
|
49
65
|
startCmd: "cargo run",
|
|
66
|
+
grpcStartCmd: "cargo run --features grpc",
|
|
50
67
|
sdkDir: "rust",
|
|
51
68
|
},
|
|
52
69
|
{
|
|
@@ -85,6 +102,7 @@ const RUNTIME_DEFINITIONS = [
|
|
|
85
102
|
{
|
|
86
103
|
kind: "php",
|
|
87
104
|
label: "PHP",
|
|
105
|
+
minVersion: "8.2.0",
|
|
88
106
|
installHint: "Install PHP 8.2+ + RoadRunner: https://php.net/downloads (rr: brew install roadrunner)",
|
|
89
107
|
defaultPort: 9005,
|
|
90
108
|
defaultGrpcPort: 10005,
|
|
@@ -103,13 +121,14 @@ const RUNTIME_DEFINITIONS = [
|
|
|
103
121
|
{
|
|
104
122
|
kind: "ruby",
|
|
105
123
|
label: "Ruby",
|
|
106
|
-
|
|
124
|
+
minVersion: "3.1.0",
|
|
125
|
+
installHint: "Install Ruby 3.1+: https://ruby-lang.org/en/downloads/ (macOS: brew install ruby)",
|
|
107
126
|
defaultPort: 9006,
|
|
108
127
|
defaultGrpcPort: 10006,
|
|
109
128
|
commands: ["ruby --version", "/opt/homebrew/opt/ruby/bin/ruby --version"],
|
|
110
129
|
toolchain: "ruby + bundler",
|
|
111
130
|
installDeps: "bundle install",
|
|
112
|
-
startCmd: "bundle exec
|
|
131
|
+
startCmd: "bundle exec ruby bin/serve.rb",
|
|
113
132
|
sdkDir: "ruby",
|
|
114
133
|
secondaryTool: {
|
|
115
134
|
name: "Bundler",
|
|
@@ -171,10 +190,12 @@ export async function detectRuntimes() {
|
|
|
171
190
|
};
|
|
172
191
|
for (const cmd of def.commands) {
|
|
173
192
|
const output = await tryExec(cmd);
|
|
174
|
-
if (output)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
193
|
+
if (!output)
|
|
194
|
+
continue;
|
|
195
|
+
info.available = true;
|
|
196
|
+
const parsed = parseVersion(output, def.kind);
|
|
197
|
+
if (parsed && (!info.version || compareSemver(parsed, info.version) > 0)) {
|
|
198
|
+
info.version = parsed;
|
|
178
199
|
}
|
|
179
200
|
}
|
|
180
201
|
if (def.secondaryTool) {
|
|
@@ -196,13 +217,16 @@ export async function detectRuntimeVersion(kind) {
|
|
|
196
217
|
const def = RUNTIME_DEFINITIONS.find((r) => r.kind === kind);
|
|
197
218
|
if (!def)
|
|
198
219
|
return undefined;
|
|
220
|
+
let best;
|
|
199
221
|
for (const cmd of def.commands) {
|
|
200
222
|
const output = await tryExec(cmd);
|
|
201
|
-
if (output)
|
|
202
|
-
|
|
203
|
-
|
|
223
|
+
if (!output)
|
|
224
|
+
continue;
|
|
225
|
+
const parsed = parseVersion(output, def.kind);
|
|
226
|
+
if (parsed && (!best || compareSemver(parsed, best) > 0))
|
|
227
|
+
best = parsed;
|
|
204
228
|
}
|
|
205
|
-
return
|
|
229
|
+
return best;
|
|
206
230
|
}
|
|
207
231
|
export function getRuntimeDefinition(kind) {
|
|
208
232
|
return RUNTIME_DEFINITIONS.find((r) => r.kind === kind);
|
|
@@ -36,6 +36,7 @@ export interface ProjectConfig {
|
|
|
36
36
|
}
|
|
37
37
|
export type ProjectRuntimeConfig = ProjectConfig;
|
|
38
38
|
export declare function setupRuntime(runtime: RuntimeInfo, githubRepoLocal: string, projectDir: string, spinner: SpinnerHandler): Promise<RuntimeConfig>;
|
|
39
|
+
export declare function buildRuntimeConfig(runtime: RuntimeInfo, projectDir: string, startCmd?: string): RuntimeConfig;
|
|
39
40
|
export declare function generateGoNodeRegistry(projectDir: string): string;
|
|
40
41
|
export declare function generateRustNodeRegistry(projectDir: string): string;
|
|
41
42
|
export declare function generateJavaNodeRegistry(projectDir: string): string;
|
|
@@ -21,7 +21,7 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
|
|
|
21
21
|
let startCmdOverride;
|
|
22
22
|
switch (runtime.kind) {
|
|
23
23
|
case "python3":
|
|
24
|
-
await setupPython3(blokctlRuntimeDir, spinner);
|
|
24
|
+
startCmdOverride = await setupPython3(blokctlRuntimeDir, spinner);
|
|
25
25
|
break;
|
|
26
26
|
case "go":
|
|
27
27
|
await setupGo(blokctlRuntimeDir, spinner);
|
|
@@ -39,7 +39,7 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
|
|
|
39
39
|
await setupPhp(blokctlRuntimeDir, spinner);
|
|
40
40
|
break;
|
|
41
41
|
case "ruby":
|
|
42
|
-
startCmdOverride = await setupRuby(blokctlRuntimeDir, spinner
|
|
42
|
+
startCmdOverride = await setupRuby(blokctlRuntimeDir, spinner);
|
|
43
43
|
break;
|
|
44
44
|
}
|
|
45
45
|
if (runtime.kind === "go") {
|
|
@@ -55,16 +55,24 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
|
|
|
55
55
|
generateCSharpNodeRegistry(projectDir);
|
|
56
56
|
}
|
|
57
57
|
spinner.message(`${runtime.label} runtime setup complete.`);
|
|
58
|
+
return buildRuntimeConfig(runtime, projectDir, startCmdOverride || runtime.startCmd);
|
|
59
|
+
}
|
|
60
|
+
export function buildRuntimeConfig(runtime, projectDir, startCmd) {
|
|
61
|
+
const blokctlRuntimeDir = path.join(projectDir, ".blok", "runtimes", runtime.kind);
|
|
58
62
|
return {
|
|
59
63
|
port: runtime.defaultPort,
|
|
60
64
|
grpcPort: runtime.defaultGrpcPort,
|
|
61
|
-
startCmd:
|
|
65
|
+
startCmd: startCmd ?? runtime.startCmd,
|
|
62
66
|
grpcStartCmd: runtime.grpcStartCmd,
|
|
63
67
|
cwd: path.relative(projectDir, blokctlRuntimeDir),
|
|
64
68
|
kind: runtime.kind,
|
|
65
69
|
label: runtime.label,
|
|
66
70
|
version: runtime.version,
|
|
67
|
-
requiredVersion: runtime.
|
|
71
|
+
requiredVersion: runtime.minVersion
|
|
72
|
+
? computeDefaultConstraint(runtime.minVersion)
|
|
73
|
+
: runtime.version
|
|
74
|
+
? computeDefaultConstraint(runtime.version)
|
|
75
|
+
: undefined,
|
|
68
76
|
transport: "grpc",
|
|
69
77
|
};
|
|
70
78
|
}
|
|
@@ -79,6 +87,7 @@ async function setupPython3(sdkDir, spinner) {
|
|
|
79
87
|
await exec(`"${venvPip}" install -r "${requirementsFile}"`, { cwd: sdkDir });
|
|
80
88
|
}
|
|
81
89
|
spinner.message("Python3 packages installed.");
|
|
90
|
+
return "python3_runtime/bin/python3 bin/serve.py";
|
|
82
91
|
}
|
|
83
92
|
async function createPythonVenv(sdkDir) {
|
|
84
93
|
await exec("python3 -m venv python3_runtime", { cwd: sdkDir, timeout: 60000 });
|
|
@@ -342,7 +351,7 @@ function collectFilesRecursive(dir, ext) {
|
|
|
342
351
|
}
|
|
343
352
|
async function setupRust(sdkDir, spinner) {
|
|
344
353
|
spinner.message("Building Rust project (this may take a few minutes on first build)...");
|
|
345
|
-
await exec("cargo build --
|
|
354
|
+
await exec("cargo build --features grpc", { cwd: sdkDir, timeout: 600000 });
|
|
346
355
|
spinner.message("Rust project built.");
|
|
347
356
|
}
|
|
348
357
|
async function setupJava(sdkDir, spinner) {
|
|
@@ -373,7 +382,7 @@ async function setupPhp(sdkDir, spinner) {
|
|
|
373
382
|
await exec("composer install --no-dev --optimize-autoloader", { cwd: sdkDir, timeout: 120000 });
|
|
374
383
|
spinner.message("PHP dependencies installed.");
|
|
375
384
|
}
|
|
376
|
-
async function setupRuby(sdkDir, spinner
|
|
385
|
+
async function setupRuby(sdkDir, spinner) {
|
|
377
386
|
const bundleCandidates = ["bundle", "/opt/homebrew/opt/ruby/bin/bundle"];
|
|
378
387
|
let resolvedBundle = "bundle";
|
|
379
388
|
for (const bin of bundleCandidates) {
|
|
@@ -392,7 +401,8 @@ async function setupRuby(sdkDir, spinner, port) {
|
|
|
392
401
|
spinner.message("Installing Ruby dependencies...");
|
|
393
402
|
await exec(`"${resolvedBundle}" install`, { cwd: sdkDir, timeout: 120000 });
|
|
394
403
|
spinner.message("Ruby dependencies installed.");
|
|
395
|
-
|
|
404
|
+
const resolvedRuby = resolvedBundle.includes("/") ? path.join(path.dirname(resolvedBundle), "ruby") : "ruby";
|
|
405
|
+
return `${resolvedBundle} exec ${resolvedRuby} bin/serve.rb`;
|
|
396
406
|
}
|
|
397
407
|
export function writeProjectConfig(projectDir, runtimeConfigs, triggerConfigs, observabilityConfigs) {
|
|
398
408
|
const config = {};
|