Skip to content

Commit a7102a4

Browse files
authored
Pin OpenShell gateway image to installed release (#559)
* pin gateway image to openshell release * harden gateway image version parsing * preserve runner env when pinning gateway image * restore PATH in runner env test * pin openshell gateway pod image tag
1 parent be7ec09 commit a7102a4

5 files changed

Lines changed: 97 additions & 7 deletions

File tree

bin/lib/onboard.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ function shellQuote(value) {
8989
return `'${String(value).replace(/'/g, `'\\''`)}'`;
9090
}
9191

92+
function getInstalledOpenshellVersion(versionOutput = null) {
93+
const output = String(versionOutput ?? runCapture("openshell -V", { ignoreError: true })).trim();
94+
const match = output.match(/openshell\s+([0-9]+\.[0-9]+\.[0-9]+)/i);
95+
if (!match) return null;
96+
return match[1];
97+
}
98+
99+
function getStableGatewayImageRef(versionOutput = null) {
100+
const version = getInstalledOpenshellVersion(versionOutput);
101+
if (!version) return null;
102+
return `ghcr.io/nvidia/openshell/cluster:${version}`;
103+
}
104+
92105
function pythonLiteralJson(value) {
93106
return JSON.stringify(JSON.stringify(value));
94107
}
@@ -373,8 +386,21 @@ async function startGateway(gpu) {
373386
// sandbox itself does not need direct GPU access. Passing --gpu causes
374387
// FailedPrecondition errors when the gateway's k3s device plugin cannot
375388
// allocate GPUs. See: https://build.nvidia.com/spark/nemoclaw/instructions
376-
377-
run(`openshell gateway start ${gwArgs.join(" ")}`, { ignoreError: false });
389+
const gatewayEnv = {};
390+
const openshellVersion = getInstalledOpenshellVersion();
391+
const stableGatewayImage = openshellVersion
392+
? `ghcr.io/nvidia/openshell/cluster:${openshellVersion}`
393+
: null;
394+
if (stableGatewayImage && openshellVersion) {
395+
gatewayEnv.OPENSHELL_CLUSTER_IMAGE = stableGatewayImage;
396+
gatewayEnv.IMAGE_TAG = openshellVersion;
397+
console.log(` Using pinned OpenShell gateway image: ${stableGatewayImage}`);
398+
}
399+
400+
run(`openshell gateway start ${gwArgs.join(" ")}`, {
401+
ignoreError: false,
402+
env: gatewayEnv,
403+
});
378404

379405
// Verify health
380406
for (let i = 0; i < 5; i++) {
@@ -964,4 +990,12 @@ async function onboard(opts = {}) {
964990
printDashboard(sandboxName, model, provider);
965991
}
966992

967-
module.exports = { buildSandboxConfigSyncScript, hasStaleGateway, isSandboxReady, onboard, setupNim };
993+
module.exports = {
994+
buildSandboxConfigSyncScript,
995+
getInstalledOpenshellVersion,
996+
getStableGatewayImageRef,
997+
hasStaleGateway,
998+
isSandboxReady,
999+
onboard,
1000+
setupNim,
1001+
};

bin/lib/runner.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ if (dockerHost) {
1616
function run(cmd, opts = {}) {
1717
const stdio = opts.stdio ?? ["ignore", "inherit", "inherit"];
1818
const result = spawnSync("bash", ["-c", cmd], {
19+
...opts,
1920
stdio,
2021
cwd: ROOT,
2122
env: { ...process.env, ...opts.env },
22-
...opts,
2323
});
2424
if (result.status !== 0 && !opts.ignoreError) {
2525
console.error(` Command failed (exit ${result.status}): ${cmd.slice(0, 80)}`);
@@ -31,10 +31,10 @@ function run(cmd, opts = {}) {
3131
function runInteractive(cmd, opts = {}) {
3232
const stdio = opts.stdio ?? "inherit";
3333
const result = spawnSync("bash", ["-c", cmd], {
34+
...opts,
3435
stdio,
3536
cwd: ROOT,
3637
env: { ...process.env, ...opts.env },
37-
...opts,
3838
});
3939
if (result.status !== 0 && !opts.ignoreError) {
4040
console.error(` Command failed (exit ${result.status}): ${cmd.slice(0, 80)}`);
@@ -46,11 +46,11 @@ function runInteractive(cmd, opts = {}) {
4646
function runCapture(cmd, opts = {}) {
4747
try {
4848
return execSync(cmd, {
49+
...opts,
4950
encoding: "utf-8",
5051
cwd: ROOT,
5152
env: { ...process.env, ...opts.env },
5253
stdio: ["pipe", "pipe", "pipe"],
53-
...opts,
5454
}).trim();
5555
} catch (err) {
5656
if (opts.ignoreError) return "";

scripts/setup.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ fi
9292
SANDBOX_NAME="${1:-nemoclaw}"
9393
info "Using sandbox name: ${SANDBOX_NAME}"
9494

95+
OPEN_SHELL_VERSION_RAW="$(openshell -V 2>/dev/null || true)"
96+
OPEN_SHELL_VERSION_LOWER="${OPEN_SHELL_VERSION_RAW,,}"
97+
if [[ "$OPEN_SHELL_VERSION_LOWER" =~ openshell[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+) ]]; then
98+
export IMAGE_TAG="${BASH_REMATCH[1]}"
99+
export OPENSHELL_CLUSTER_IMAGE="ghcr.io/nvidia/openshell/cluster:${BASH_REMATCH[1]}"
100+
info "Using pinned OpenShell gateway image: ${OPENSHELL_CLUSTER_IMAGE}"
101+
elif [[ -n "$OPEN_SHELL_VERSION_RAW" ]]; then
102+
warn "Could not parse openshell version from 'openshell -V': ${OPEN_SHELL_VERSION_RAW}"
103+
warn "Skipping OpenShell gateway image pinning."
104+
fi
105+
95106
# 1. Gateway — always start fresh to avoid stale state
96107
info "Starting OpenShell gateway..."
97108
openshell gateway destroy -g nemoclaw > /dev/null 2>&1 || true

test/onboard.test.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
const { describe, it } = require("node:test");
55
const assert = require("node:assert/strict");
66

7-
const { buildSandboxConfigSyncScript } = require("../bin/lib/onboard");
7+
const {
8+
buildSandboxConfigSyncScript,
9+
getInstalledOpenshellVersion,
10+
getStableGatewayImageRef,
11+
} = require("../bin/lib/onboard");
812

913
describe("onboard helpers", () => {
1014
it("builds a sandbox sync script that writes config and updates the selected model", () => {
@@ -28,4 +32,16 @@ describe("onboard helpers", () => {
2832
assert.match(script, /inference\/nemotron-3-nano:30b/);
2933
assert.match(script, /^exit$/m);
3034
});
35+
36+
it("pins the gateway image to the installed OpenShell release version", () => {
37+
assert.equal(getInstalledOpenshellVersion("openshell 0.0.12"), "0.0.12");
38+
assert.equal(getInstalledOpenshellVersion("openshell 0.0.13-dev.8+gbbcaed2ea"), "0.0.13");
39+
assert.equal(getInstalledOpenshellVersion("bogus"), null);
40+
assert.equal(
41+
getStableGatewayImageRef("openshell 0.0.12"),
42+
"ghcr.io/nvidia/openshell/cluster:0.0.12"
43+
);
44+
assert.equal(getStableGatewayImageRef("openshell 0.0.13-dev.8+gbbcaed2ea"), "ghcr.io/nvidia/openshell/cluster:0.0.13");
45+
assert.equal(getStableGatewayImageRef("bogus"), null);
46+
});
3147
});

test/runner.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,33 @@ describe("runner helpers", () => {
5252
assert.deepEqual(calls[0][2].stdio, ["ignore", "inherit", "inherit"]);
5353
assert.equal(calls[1][2].stdio, "inherit");
5454
});
55+
56+
it("preserves process env when opts.env is provided", () => {
57+
const calls = [];
58+
const originalSpawnSync = childProcess.spawnSync;
59+
const originalPath = process.env.PATH;
60+
childProcess.spawnSync = (...args) => {
61+
calls.push(args);
62+
return { status: 0 };
63+
};
64+
65+
try {
66+
delete require.cache[require.resolve(runnerPath)];
67+
const { run } = require(runnerPath);
68+
process.env.PATH = "/usr/local/bin:/usr/bin";
69+
run("echo test", { env: { OPENSHELL_CLUSTER_IMAGE: "ghcr.io/nvidia/openshell/cluster:0.0.12" } });
70+
} finally {
71+
if (originalPath === undefined) {
72+
delete process.env.PATH;
73+
} else {
74+
process.env.PATH = originalPath;
75+
}
76+
childProcess.spawnSync = originalSpawnSync;
77+
delete require.cache[require.resolve(runnerPath)];
78+
}
79+
80+
assert.equal(calls.length, 1);
81+
assert.equal(calls[0][2].env.OPENSHELL_CLUSTER_IMAGE, "ghcr.io/nvidia/openshell/cluster:0.0.12");
82+
assert.equal(calls[0][2].env.PATH, "/usr/local/bin:/usr/bin");
83+
});
5584
});

0 commit comments

Comments
 (0)