From 89ac42feb771c338208c5fca59b855664ab9cd25 Mon Sep 17 00:00:00 2001 From: Evzen Gasta Date: Thu, 2 Jul 2026 12:40:51 +0200 Subject: [PATCH 1/4] feat(openshell-gateway): pin supervisor container image to gateway binary version Write a TOML config file that pins the supervisor_image to the gateway binary's own semver, preventing :latest tag drift. When version detection fails, the gateway starts without --config so behavior is unchanged. Also adds a supervisorImage option to allow explicit image overrides. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Evzen Gasta --- packages/api/src/openshell-gateway-info.ts | 2 + .../openshell-cli/openshell-gateway.spec.ts | 272 ++++++++++++++---- .../plugin/openshell-cli/openshell-gateway.ts | 48 +++- 3 files changed, 269 insertions(+), 53 deletions(-) diff --git a/packages/api/src/openshell-gateway-info.ts b/packages/api/src/openshell-gateway-info.ts index 1e4f48c90..4a2785f49 100644 --- a/packages/api/src/openshell-gateway-info.ts +++ b/packages/api/src/openshell-gateway-info.ts @@ -109,6 +109,8 @@ export interface OpenshellGatewayStartOptions { disableTls?: boolean; /** Skip CLI registration when restarting an already-registered gateway. */ skipRegistration?: boolean; + /** Override the supervisor container image. When unset, the image tag is pinned to the gateway binary version. */ + supervisorImage?: string; } export interface GatewaySandboxes { diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts b/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts index 8b8d5e1ef..45f8ee5d0 100644 --- a/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts @@ -19,20 +19,29 @@ import type { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; +import type { RunResult } from '@openkaiden/api'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import type { CliToolRegistry } from '/@/plugin/cli-tool-registry.js'; import type { OpenshellCli } from '/@/plugin/openshell-cli/openshell-cli.js'; +import type { Proxy } from '/@/plugin/proxy.js'; +import { Exec } from '/@/plugin/util/exec.js'; import type { CliToolInfo } from '/@api/cli-tool-info.js'; import type { GatewayInfo } from '/@api/openshell-gateway-info.js'; import { OpenshellGateway } from './openshell-gateway.js'; vi.mock(import('node:child_process')); +vi.mock(import('node:fs/promises')); +vi.mock(import('node:os')); +vi.mock(import('/@/plugin/util/exec.js')); const { spawn } = await import('node:child_process'); +const { writeFile } = await import('node:fs/promises'); +const { tmpdir } = await import('node:os'); const GATEWAY_BINARY = '/usr/local/bin/openshell-gateway'; +const CLI_BINARY = '/usr/local/bin/openshell'; function createMockChildProcess(): ChildProcess & { _stdout: EventEmitter; _stderr: EventEmitter } { const proc = new EventEmitter() as ChildProcess & { _stdout: EventEmitter; _stderr: EventEmitter }; @@ -44,8 +53,13 @@ function createMockChildProcess(): ChildProcess & { _stdout: EventEmitter; _stde return proc; } +function mockExecResult(stdout = ''): RunResult { + return { command: CLI_BINARY, stdout, stderr: '' }; +} + let gateway: OpenshellGateway; +const exec = new Exec({} as Proxy); const cliToolRegistry = { getCliToolInfos: vi.fn(), } as unknown as CliToolRegistry; @@ -53,16 +67,16 @@ const cliToolRegistry = { const openshellCli = { listGateways: vi.fn(), selectGateway: vi.fn(), - checkEndpointStatus: vi.fn(), - addGateway: vi.fn(), } as unknown as OpenshellCli; beforeEach(() => { vi.resetAllMocks(); vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([ { name: 'openshell-gateway', path: GATEWAY_BINARY }, + { name: 'openshell', path: CLI_BINARY }, ] as unknown as CliToolInfo[]); - gateway = new OpenshellGateway(cliToolRegistry, openshellCli); + vi.mocked(tmpdir).mockReturnValue('/tmp'); + gateway = new OpenshellGateway(exec, cliToolRegistry, openshellCli); }); describe('init', () => { @@ -72,12 +86,12 @@ describe('init', () => { { name: 'local-gw', endpoint: 'https://127.0.0.1:8443', active: true, type: 'local' }, ]; vi.mocked(openshellCli.listGateways).mockResolvedValue(existingGateways); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); await gateway.init(); expect(openshellCli.listGateways).toHaveBeenCalled(); - expect(openshellCli.checkEndpointStatus).toHaveBeenCalledWith('https://127.0.0.1:8443'); + expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, ['status', '--gateway-endpoint', 'https://127.0.0.1:8443']); expect(spawn).not.toHaveBeenCalled(); expect(openshellCli.selectGateway).not.toHaveBeenCalled(); }); @@ -86,7 +100,7 @@ describe('init', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const existingGateways: GatewayInfo[] = [{ name: 'kaiden-alt', endpoint: 'http://127.0.0.1:18080', active: false }]; vi.mocked(openshellCli.listGateways).mockResolvedValue(existingGateways); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); await gateway.init(); @@ -100,7 +114,10 @@ describe('init', () => { vi.mocked(openshellCli.listGateways).mockResolvedValue([]); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValueOnce(false).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockRejectedValueOnce(new Error('connection refused')) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.init(); @@ -115,22 +132,27 @@ describe('init', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); vi.spyOn(console, 'warn').mockImplementation(() => undefined); vi.mocked(openshellCli.listGateways).mockResolvedValue([]); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); await gateway.init(); expect(spawn).not.toHaveBeenCalled(); - expect(openshellCli.addGateway).toHaveBeenCalledWith({ - endpoint: 'http://127.0.0.1:17670', - local: true, - name: 'kaiden-local', - }); + expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, [ + 'gateway', + 'add', + 'http://127.0.0.1:17670', + '--local', + '--name', + 'kaiden-local', + ]); }); test('skips auto-start when discovery fails and binary is not registered', async () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); vi.mocked(openshellCli.listGateways).mockRejectedValue(new Error('CLI not found')); - vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([] as unknown as CliToolInfo[]); + vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([ + { name: 'openshell', path: CLI_BINARY }, + ] as unknown as CliToolInfo[]); await gateway.init(); @@ -143,7 +165,10 @@ describe('init', () => { vi.mocked(openshellCli.listGateways).mockRejectedValue(new Error('no gateway configured')); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValueOnce(false).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockRejectedValueOnce(new Error('connection refused')) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.init(); @@ -161,7 +186,9 @@ describe('init', () => { { name: 'gw-healthy', endpoint: 'http://127.0.0.1:9090', active: true }, ]; vi.mocked(openshellCli.listGateways).mockResolvedValue(gateways); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValueOnce(false).mockResolvedValueOnce(true); + vi.mocked(exec.exec) + .mockRejectedValueOnce(new Error('connection refused')) + .mockResolvedValueOnce(mockExecResult('')); await gateway.init(); @@ -175,10 +202,11 @@ describe('init', () => { vi.mocked(openshellCli.listGateways).mockResolvedValue(gateways); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(false) - .mockResolvedValue(true); + vi.mocked(exec.exec) + .mockRejectedValueOnce(new Error('connection refused')) + .mockRejectedValueOnce(new Error('connection refused')) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.init(); @@ -189,17 +217,17 @@ describe('init', () => { ); }); - test('delegates health check to openshellCli for https endpoints', async () => { + test('does not pass --gateway-insecure for https endpoints during health check', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const gateways: GatewayInfo[] = [ { name: 'tls-gw', endpoint: 'https://127.0.0.1:8443', active: true, type: 'local' }, ]; vi.mocked(openshellCli.listGateways).mockResolvedValue(gateways); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); await gateway.init(); - expect(openshellCli.checkEndpointStatus).toHaveBeenCalledWith('https://127.0.0.1:8443'); + expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, ['status', '--gateway-endpoint', 'https://127.0.0.1:8443']); }); test('skips remote gateways during init and auto-starts local', async () => { @@ -209,11 +237,17 @@ describe('init', () => { vi.mocked(openshellCli.listGateways).mockResolvedValue(gateways); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValueOnce(false).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockRejectedValueOnce(new Error('connection refused')) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.init(); - expect(openshellCli.checkEndpointStatus).not.toHaveBeenCalledWith('https://gw.example.com'); + expect(exec.exec).not.toHaveBeenCalledWith( + CLI_BINARY, + expect.arrayContaining(['--gateway-endpoint', 'https://gw.example.com']), + ); expect(spawn).toHaveBeenCalled(); }); }); @@ -224,23 +258,27 @@ describe('getGatewayBinaryPath', () => { }); test('returns undefined when openshell-gateway is not registered', () => { - vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([] as unknown as CliToolInfo[]); + vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([ + { name: 'openshell', path: CLI_BINARY }, + ] as unknown as CliToolInfo[]); expect(gateway.getGatewayBinaryPath()).toBeUndefined(); }); }); describe('start', () => { - test('spawns the gateway process with default args', async () => { + test('spawns the gateway process with default args and pinned supervisor image', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start(); expect(spawn).toHaveBeenCalledWith( GATEWAY_BINARY, - ['--port', '17670', '--bind-address', '127.0.0.1', '--disable-tls'], + ['--config', '/tmp/kaiden-gateway.toml', '--port', '17670', '--bind-address', '127.0.0.1', '--disable-tls'], expect.objectContaining({ detached: false }), ); }); @@ -249,13 +287,15 @@ describe('start', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start({ port: 9999, bindAddress: '0.0.0.0' }); expect(spawn).toHaveBeenCalledWith( GATEWAY_BINARY, - ['--port', '9999', '--bind-address', '0.0.0.0', '--disable-tls'], + ['--config', '/tmp/kaiden-gateway.toml', '--port', '9999', '--bind-address', '0.0.0.0', '--disable-tls'], expect.objectContaining({ detached: false }), ); }); @@ -264,13 +304,15 @@ describe('start', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start({ disableTls: false }); expect(spawn).toHaveBeenCalledWith( GATEWAY_BINARY, - ['--port', '17670', '--bind-address', '127.0.0.1'], + ['--config', '/tmp/kaiden-gateway.toml', '--port', '17670', '--bind-address', '127.0.0.1'], expect.objectContaining({ detached: false }), ); }); @@ -279,7 +321,9 @@ describe('start', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start(); await gateway.start(); @@ -288,47 +332,63 @@ describe('start', () => { }); test('throws when gateway binary is not registered', async () => { - vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([] as unknown as CliToolInfo[]); + vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([ + { name: 'openshell', path: CLI_BINARY }, + ] as unknown as CliToolInfo[]); await expect(gateway.start()).rejects.toThrow('openshell-gateway binary not registered'); }); - test('performs health check via openshellCli', async () => { + test('performs health check with openshell status', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start(); - expect(openshellCli.checkEndpointStatus).toHaveBeenCalledWith('http://127.0.0.1:17670'); + expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, [ + 'status', + '--gateway-endpoint', + 'http://127.0.0.1:17670', + '--gateway-insecure', + ]); }); - test('registers gateway via openshellCli after health check passes', async () => { + test('registers gateway with CLI after health check passes', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start(); - expect(openshellCli.addGateway).toHaveBeenCalledWith({ - endpoint: 'http://127.0.0.1:17670', - local: true, - name: 'kaiden-local', - }); + expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, [ + 'gateway', + 'add', + 'http://127.0.0.1:17670', + '--local', + '--name', + 'kaiden-local', + ]); }); test('skips registerWithCli when skipRegistration is true', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start({ skipRegistration: true }); expect(spawn).toHaveBeenCalled(); - expect(openshellCli.addGateway).not.toHaveBeenCalled(); + expect(exec.exec).not.toHaveBeenCalledWith(CLI_BINARY, expect.arrayContaining(['gateway', 'add'])); }); test('stops the spawned process when waitForReady fails', async () => { @@ -337,7 +397,9 @@ describe('start', () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(false); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockRejectedValue(new Error('connection refused')); let caughtError: unknown; const startPromise = gateway.start().catch((err: unknown) => { @@ -364,7 +426,9 @@ describe('stop', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start(); @@ -389,7 +453,9 @@ describe('isRunning', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start(); @@ -402,7 +468,9 @@ describe('dispose', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); await gateway.start(); @@ -411,3 +479,105 @@ describe('dispose', () => { expect(proc.kill).toHaveBeenCalledWith('SIGTERM'); }); }); + +describe('supervisor image pinning', () => { + test('writes config file pinning supervisor image to gateway version', async () => { + vi.spyOn(console, 'log').mockImplementation(() => undefined); + const proc = createMockChildProcess(); + vi.mocked(spawn).mockReturnValue(proc); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); + + await gateway.start(); + + expect(exec.exec).toHaveBeenCalledWith(GATEWAY_BINARY, ['--version']); + expect(writeFile).toHaveBeenCalledWith( + '/tmp/kaiden-gateway.toml', + expect.stringContaining('supervisor_image = "ghcr.io/nvidia/openshell/supervisor:0.0.69"'), + 'utf-8', + ); + }); + + test('config only includes podman driver section', async () => { + vi.spyOn(console, 'log').mockImplementation(() => undefined); + const proc = createMockChildProcess(); + vi.mocked(spawn).mockReturnValue(proc); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); + + await gateway.start(); + + const configContent = vi.mocked(writeFile).mock.calls[0]?.[1] as string; + expect(configContent).toContain('[openshell.drivers.podman]'); + expect(configContent).not.toContain('[openshell.drivers.docker]'); + }); + + test('passes --config flag to spawned gateway process', async () => { + vi.spyOn(console, 'log').mockImplementation(() => undefined); + const proc = createMockChildProcess(); + vi.mocked(spawn).mockReturnValue(proc); + vi.mocked(exec.exec) + .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) + .mockResolvedValue(mockExecResult('')); + + await gateway.start(); + + expect(spawn).toHaveBeenCalledWith( + GATEWAY_BINARY, + expect.arrayContaining(['--config', '/tmp/kaiden-gateway.toml']), + expect.objectContaining({ detached: false }), + ); + }); + + test('uses custom supervisorImage without version detection', async () => { + vi.spyOn(console, 'log').mockImplementation(() => undefined); + const proc = createMockChildProcess(); + vi.mocked(spawn).mockReturnValue(proc); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); + + await gateway.start({ supervisorImage: 'custom-registry.io/supervisor:1.2.3' }); + + expect(exec.exec).not.toHaveBeenCalledWith(GATEWAY_BINARY, ['--version']); + expect(writeFile).toHaveBeenCalledWith( + '/tmp/kaiden-gateway.toml', + expect.stringContaining('supervisor_image = "custom-registry.io/supervisor:1.2.3"'), + 'utf-8', + ); + }); + + test('starts without --config when version detection fails', async () => { + vi.spyOn(console, 'log').mockImplementation(() => undefined); + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const proc = createMockChildProcess(); + vi.mocked(spawn).mockReturnValue(proc); + vi.mocked(exec.exec).mockRejectedValueOnce(new Error('binary not found')).mockResolvedValue(mockExecResult('')); + + await gateway.start(); + + expect(writeFile).not.toHaveBeenCalled(); + expect(spawn).toHaveBeenCalledWith( + GATEWAY_BINARY, + ['--port', '17670', '--bind-address', '127.0.0.1', '--disable-tls'], + expect.objectContaining({ detached: false }), + ); + }); + + test('starts without --config when version output is unparseable', async () => { + vi.spyOn(console, 'log').mockImplementation(() => undefined); + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const proc = createMockChildProcess(); + vi.mocked(spawn).mockReturnValue(proc); + vi.mocked(exec.exec).mockResolvedValueOnce(mockExecResult('unknown-format')).mockResolvedValue(mockExecResult('')); + + await gateway.start(); + + expect(writeFile).not.toHaveBeenCalled(); + expect(spawn).toHaveBeenCalledWith( + GATEWAY_BINARY, + ['--port', '17670', '--bind-address', '127.0.0.1', '--disable-tls'], + expect.objectContaining({ detached: false }), + ); + }); +}); diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.ts b/packages/main/src/plugin/openshell-cli/openshell-gateway.ts index e83e82928..2848d63b9 100644 --- a/packages/main/src/plugin/openshell-cli/openshell-gateway.ts +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.ts @@ -18,6 +18,9 @@ import type { ChildProcess } from 'node:child_process'; import { spawn } from 'node:child_process'; +import { writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import type { Disposable } from '@openkaiden/api'; import { inject, injectable, preDestroy } from 'inversify'; @@ -31,6 +34,7 @@ const DEFAULT_BIND_ADDRESS = '127.0.0.1'; const HEALTH_CHECK_INTERVAL_MS = 1000; const MAX_HEALTH_CHECK_ATTEMPTS = 30; const STOP_TIMEOUT_MS = 5000; +const SUPERVISOR_IMAGE_BASE = 'ghcr.io/nvidia/openshell/supervisor'; /** * Manages the `openshell-gateway` server binary lifecycle. @@ -130,7 +134,8 @@ export class OpenshellGateway implements Disposable { this.#bindAddress = options.bindAddress; } - const args = this.buildArgs(options?.disableTls ?? true); + const configPath = await this.writeSupervisorConfig(binaryPath, options?.supervisorImage); + const args = this.buildArgs(options?.disableTls ?? true, configPath); console.log(`[openshell-gateway] starting: ${binaryPath} ${args.join(' ')}`); this.#gatewayProcess = spawn(binaryPath, args, { @@ -207,8 +212,11 @@ export class OpenshellGateway implements Disposable { this.stop().catch((err: unknown) => console.error('[openshell-gateway] failed to stop: ', err)); } - private buildArgs(disableTls: boolean): string[] { + private buildArgs(disableTls: boolean, configPath?: string): string[] { const args: string[] = []; + if (configPath) { + args.push('--config', configPath); + } args.push('--port', String(this.#port)); args.push('--bind-address', this.#bindAddress); if (disableTls) { @@ -217,6 +225,42 @@ export class OpenshellGateway implements Disposable { return args; } + private async getGatewayVersion(binaryPath: string): Promise { + const result = await this.exec.exec(binaryPath, ['--version']); + const output = result.stdout.trim(); + const token = output.split(' ').pop() ?? ''; + const parts = token.split('.'); + if (parts.length !== 3 || parts.some(p => p.length === 0 || !Number.isFinite(Number(p)))) { + throw new Error(`Unable to parse version from: ${output}`); + } + return token; + } + + /** + * Writes a TOML config that pins the supervisor container image to the + * gateway binary's own version, preventing the `:latest` tag drift + * described in issue #2315. + */ + private async writeSupervisorConfig(binaryPath: string, supervisorImage?: string): Promise { + try { + let image = supervisorImage; + if (!image) { + const version = await this.getGatewayVersion(binaryPath); + image = `${SUPERVISOR_IMAGE_BASE}:${version}`; + } + + const configPath = join(tmpdir(), 'kaiden-gateway.toml'); + const content = ['[openshell.drivers.podman]', `supervisor_image = "${image}"`, ''].join('\n'); + await writeFile(configPath, content, 'utf-8'); + console.log(`[openshell-gateway] supervisor image pinned to ${image}`); + return configPath; + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + console.warn(`[openshell-gateway] unable to pin supervisor image: ${message}`); + return undefined; + } + } + private async waitForReady(): Promise { const endpoint = `http://${this.#bindAddress}:${this.#port}`; console.log(`[openshell-gateway] waiting for server at ${endpoint}`); From dd9e06958917acb683c518f68b04bbbcaf66ebc7 Mon Sep 17 00:00:00 2001 From: Evzen Gasta Date: Thu, 2 Jul 2026 15:31:05 +0200 Subject: [PATCH 2/4] refactor: improve supervisor pinning tests and version parsing - Update start tests to mock version detection and assert --config flag - Add comprehensive supervisor image pinning test suite (6 tests) - Remove JSDoc comment, simplify writeSupervisorConfig formatting Co-Authored-By: Claude Opus 4.6 Signed-off-by: Evzen Gasta --- .../openshell-cli/openshell-gateway.spec.ts | 32 +++++++++---------- .../plugin/openshell-cli/openshell-gateway.ts | 5 --- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts b/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts index 45f8ee5d0..cf092f129 100644 --- a/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts @@ -18,6 +18,8 @@ import type { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; +import { writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; import type { RunResult } from '@openkaiden/api'; import { beforeEach, describe, expect, test, vi } from 'vitest'; @@ -37,8 +39,6 @@ vi.mock(import('node:os')); vi.mock(import('/@/plugin/util/exec.js')); const { spawn } = await import('node:child_process'); -const { writeFile } = await import('node:fs/promises'); -const { tmpdir } = await import('node:os'); const GATEWAY_BINARY = '/usr/local/bin/openshell-gateway'; const CLI_BINARY = '/usr/local/bin/openshell'; @@ -266,13 +266,13 @@ describe('getGatewayBinaryPath', () => { }); describe('start', () => { - test('spawns the gateway process with default args and pinned supervisor image', async () => { + test('spawns the gateway process with default args', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); vi.mocked(exec.exec) .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + .mockResolvedValue(mockExecResult('connected')); await gateway.start(); @@ -289,7 +289,7 @@ describe('start', () => { vi.mocked(spawn).mockReturnValue(proc); vi.mocked(exec.exec) .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + .mockResolvedValue(mockExecResult('connected')); await gateway.start({ port: 9999, bindAddress: '0.0.0.0' }); @@ -306,7 +306,7 @@ describe('start', () => { vi.mocked(spawn).mockReturnValue(proc); vi.mocked(exec.exec) .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + .mockResolvedValue(mockExecResult('connected')); await gateway.start({ disableTls: false }); @@ -323,7 +323,7 @@ describe('start', () => { vi.mocked(spawn).mockReturnValue(proc); vi.mocked(exec.exec) .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + .mockResolvedValue(mockExecResult('connected')); await gateway.start(); await gateway.start(); @@ -397,9 +397,7 @@ describe('start', () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockRejectedValue(new Error('connection refused')); + vi.mocked(exec.exec).mockRejectedValue(new Error('connection refused')); let caughtError: unknown; const startPromise = gateway.start().catch((err: unknown) => { @@ -494,7 +492,7 @@ describe('supervisor image pinning', () => { expect(exec.exec).toHaveBeenCalledWith(GATEWAY_BINARY, ['--version']); expect(writeFile).toHaveBeenCalledWith( '/tmp/kaiden-gateway.toml', - expect.stringContaining('supervisor_image = "ghcr.io/nvidia/openshell/supervisor:0.0.69"'), + '[openshell.drivers.podman]\nsupervisor_image = "ghcr.io/nvidia/openshell/supervisor:0.0.69"\n', 'utf-8', ); }); @@ -509,9 +507,9 @@ describe('supervisor image pinning', () => { await gateway.start(); - const configContent = vi.mocked(writeFile).mock.calls[0]?.[1] as string; - expect(configContent).toContain('[openshell.drivers.podman]'); - expect(configContent).not.toContain('[openshell.drivers.docker]'); + const writtenContent = vi.mocked(writeFile).mock.calls[0]?.[1] as string; + expect(writtenContent).toContain('[openshell.drivers.podman]'); + expect(writtenContent).not.toContain('[openshell.drivers.docker]'); }); test('passes --config flag to spawned gateway process', async () => { @@ -537,12 +535,12 @@ describe('supervisor image pinning', () => { vi.mocked(spawn).mockReturnValue(proc); vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); - await gateway.start({ supervisorImage: 'custom-registry.io/supervisor:1.2.3' }); + await gateway.start({ supervisorImage: 'my-registry.io/supervisor:custom' }); expect(exec.exec).not.toHaveBeenCalledWith(GATEWAY_BINARY, ['--version']); expect(writeFile).toHaveBeenCalledWith( '/tmp/kaiden-gateway.toml', - expect.stringContaining('supervisor_image = "custom-registry.io/supervisor:1.2.3"'), + '[openshell.drivers.podman]\nsupervisor_image = "my-registry.io/supervisor:custom"\n', 'utf-8', ); }); @@ -552,7 +550,7 @@ describe('supervisor image pinning', () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec).mockRejectedValueOnce(new Error('binary not found')).mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockRejectedValueOnce(new Error('command not found')).mockResolvedValue(mockExecResult('')); await gateway.start(); diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.ts b/packages/main/src/plugin/openshell-cli/openshell-gateway.ts index 2848d63b9..b04e5d658 100644 --- a/packages/main/src/plugin/openshell-cli/openshell-gateway.ts +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.ts @@ -236,11 +236,6 @@ export class OpenshellGateway implements Disposable { return token; } - /** - * Writes a TOML config that pins the supervisor container image to the - * gateway binary's own version, preventing the `:latest` tag drift - * described in issue #2315. - */ private async writeSupervisorConfig(binaryPath: string, supervisorImage?: string): Promise { try { let image = supervisorImage; From 9b870437b4d47dcfe5b0ee64db1a92508bc5b88c Mon Sep 17 00:00:00 2001 From: Evzen Gasta Date: Fri, 3 Jul 2026 20:51:43 +0200 Subject: [PATCH 3/4] feat(openshell-gateway): use mustache template for gateway config generation Replace hardcoded TOML string with a .toml.template file rendered via Mustache. Config is now written to the Kaiden data directory instead of /tmp, and includes bind mount settings alongside supervisor image pinning. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Evzen Gasta --- package.json | 1 + .../openshell-cli/openshell-gateway.spec.ts | 291 +++++++++--------- .../openshell-gateway.toml.template | 8 + .../plugin/openshell-cli/openshell-gateway.ts | 44 ++- pnpm-lock.yaml | 8 + 5 files changed, 192 insertions(+), 160 deletions(-) create mode 100644 packages/main/src/plugin/openshell-cli/openshell-gateway.toml.template diff --git a/package.json b/package.json index 6544ea685..944331c6e 100644 --- a/package.json +++ b/package.json @@ -162,6 +162,7 @@ "@types/mime-types": "^3.0.1", "@types/minimist": "^1.2.5", "@types/ms": "^2.1.0", + "@types/mustache": "^4.2.6", "@types/node": "^24", "@types/tar": "^6.1.13", "@types/tar-fs": "^2.0.4", diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts b/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts index cf092f129..62db78706 100644 --- a/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts @@ -18,13 +18,14 @@ import type { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; -import { writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; import type { RunResult } from '@openkaiden/api'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import type { CliToolRegistry } from '/@/plugin/cli-tool-registry.js'; +import type { Directories } from '/@/plugin/directories.js'; import type { OpenshellCli } from '/@/plugin/openshell-cli/openshell-cli.js'; import type { Proxy } from '/@/plugin/proxy.js'; import { Exec } from '/@/plugin/util/exec.js'; @@ -35,13 +36,14 @@ import { OpenshellGateway } from './openshell-gateway.js'; vi.mock(import('node:child_process')); vi.mock(import('node:fs/promises')); -vi.mock(import('node:os')); vi.mock(import('/@/plugin/util/exec.js')); const { spawn } = await import('node:child_process'); const GATEWAY_BINARY = '/usr/local/bin/openshell-gateway'; -const CLI_BINARY = '/usr/local/bin/openshell'; +const KAIDEN_DATA_DIRECTORY = '/home/user/.local/share/kaiden'; +const GATEWAY_STORAGE_DIRECTORY = join(KAIDEN_DATA_DIRECTORY, 'openshell-gateway'); +const GATEWAY_CONFIG_PATH = join(GATEWAY_STORAGE_DIRECTORY, 'gateway.toml'); function createMockChildProcess(): ChildProcess & { _stdout: EventEmitter; _stderr: EventEmitter } { const proc = new EventEmitter() as ChildProcess & { _stdout: EventEmitter; _stderr: EventEmitter }; @@ -54,7 +56,7 @@ function createMockChildProcess(): ChildProcess & { _stdout: EventEmitter; _stde } function mockExecResult(stdout = ''): RunResult { - return { command: CLI_BINARY, stdout, stderr: '' }; + return { command: GATEWAY_BINARY, stdout, stderr: '' }; } let gateway: OpenshellGateway; @@ -67,16 +69,21 @@ const cliToolRegistry = { const openshellCli = { listGateways: vi.fn(), selectGateway: vi.fn(), + checkEndpointStatus: vi.fn(), + addGateway: vi.fn(), } as unknown as OpenshellCli; +const directories = { + getDataDirectory: vi.fn(), +} as unknown as Directories; + beforeEach(() => { vi.resetAllMocks(); vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([ { name: 'openshell-gateway', path: GATEWAY_BINARY }, - { name: 'openshell', path: CLI_BINARY }, ] as unknown as CliToolInfo[]); - vi.mocked(tmpdir).mockReturnValue('/tmp'); - gateway = new OpenshellGateway(exec, cliToolRegistry, openshellCli); + vi.mocked(directories.getDataDirectory).mockReturnValue(KAIDEN_DATA_DIRECTORY); + gateway = new OpenshellGateway(exec, cliToolRegistry, openshellCli, directories); }); describe('init', () => { @@ -86,12 +93,12 @@ describe('init', () => { { name: 'local-gw', endpoint: 'https://127.0.0.1:8443', active: true, type: 'local' }, ]; vi.mocked(openshellCli.listGateways).mockResolvedValue(existingGateways); - vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.init(); expect(openshellCli.listGateways).toHaveBeenCalled(); - expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, ['status', '--gateway-endpoint', 'https://127.0.0.1:8443']); + expect(openshellCli.checkEndpointStatus).toHaveBeenCalledWith('https://127.0.0.1:8443'); expect(spawn).not.toHaveBeenCalled(); expect(openshellCli.selectGateway).not.toHaveBeenCalled(); }); @@ -100,7 +107,7 @@ describe('init', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const existingGateways: GatewayInfo[] = [{ name: 'kaiden-alt', endpoint: 'http://127.0.0.1:18080', active: false }]; vi.mocked(openshellCli.listGateways).mockResolvedValue(existingGateways); - vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.init(); @@ -114,10 +121,8 @@ describe('init', () => { vi.mocked(openshellCli.listGateways).mockResolvedValue([]); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockRejectedValueOnce(new Error('connection refused')) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValueOnce(false).mockResolvedValue(true); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); await gateway.init(); @@ -132,27 +137,22 @@ describe('init', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); vi.spyOn(console, 'warn').mockImplementation(() => undefined); vi.mocked(openshellCli.listGateways).mockResolvedValue([]); - vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.init(); expect(spawn).not.toHaveBeenCalled(); - expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, [ - 'gateway', - 'add', - 'http://127.0.0.1:17670', - '--local', - '--name', - 'kaiden-local', - ]); + expect(openshellCli.addGateway).toHaveBeenCalledWith({ + endpoint: 'http://127.0.0.1:17670', + local: true, + name: 'kaiden-local', + }); }); test('skips auto-start when discovery fails and binary is not registered', async () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); vi.mocked(openshellCli.listGateways).mockRejectedValue(new Error('CLI not found')); - vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([ - { name: 'openshell', path: CLI_BINARY }, - ] as unknown as CliToolInfo[]); + vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([] as unknown as CliToolInfo[]); await gateway.init(); @@ -165,10 +165,8 @@ describe('init', () => { vi.mocked(openshellCli.listGateways).mockRejectedValue(new Error('no gateway configured')); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockRejectedValueOnce(new Error('connection refused')) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValueOnce(false).mockResolvedValue(true); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); await gateway.init(); @@ -186,9 +184,7 @@ describe('init', () => { { name: 'gw-healthy', endpoint: 'http://127.0.0.1:9090', active: true }, ]; vi.mocked(openshellCli.listGateways).mockResolvedValue(gateways); - vi.mocked(exec.exec) - .mockRejectedValueOnce(new Error('connection refused')) - .mockResolvedValueOnce(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValueOnce(false).mockResolvedValueOnce(true); await gateway.init(); @@ -202,11 +198,11 @@ describe('init', () => { vi.mocked(openshellCli.listGateways).mockResolvedValue(gateways); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockRejectedValueOnce(new Error('connection refused')) - .mockRejectedValueOnce(new Error('connection refused')) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false) + .mockResolvedValue(true); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); await gateway.init(); @@ -217,17 +213,17 @@ describe('init', () => { ); }); - test('does not pass --gateway-insecure for https endpoints during health check', async () => { + test('delegates health check to openshellCli for https endpoints', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const gateways: GatewayInfo[] = [ { name: 'tls-gw', endpoint: 'https://127.0.0.1:8443', active: true, type: 'local' }, ]; vi.mocked(openshellCli.listGateways).mockResolvedValue(gateways); - vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.init(); - expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, ['status', '--gateway-endpoint', 'https://127.0.0.1:8443']); + expect(openshellCli.checkEndpointStatus).toHaveBeenCalledWith('https://127.0.0.1:8443'); }); test('skips remote gateways during init and auto-starts local', async () => { @@ -237,17 +233,12 @@ describe('init', () => { vi.mocked(openshellCli.listGateways).mockResolvedValue(gateways); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockRejectedValueOnce(new Error('connection refused')) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValueOnce(false).mockResolvedValue(true); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); await gateway.init(); - expect(exec.exec).not.toHaveBeenCalledWith( - CLI_BINARY, - expect.arrayContaining(['--gateway-endpoint', 'https://gw.example.com']), - ); + expect(openshellCli.checkEndpointStatus).not.toHaveBeenCalledWith('https://gw.example.com'); expect(spawn).toHaveBeenCalled(); }); }); @@ -258,9 +249,7 @@ describe('getGatewayBinaryPath', () => { }); test('returns undefined when openshell-gateway is not registered', () => { - vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([ - { name: 'openshell', path: CLI_BINARY }, - ] as unknown as CliToolInfo[]); + vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([] as unknown as CliToolInfo[]); expect(gateway.getGatewayBinaryPath()).toBeUndefined(); }); }); @@ -270,15 +259,14 @@ describe('start', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('connected')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start(); expect(spawn).toHaveBeenCalledWith( GATEWAY_BINARY, - ['--config', '/tmp/kaiden-gateway.toml', '--port', '17670', '--bind-address', '127.0.0.1', '--disable-tls'], + ['--config', GATEWAY_CONFIG_PATH, '--port', '17670', '--bind-address', '127.0.0.1', '--disable-tls'], expect.objectContaining({ detached: false }), ); }); @@ -287,15 +275,14 @@ describe('start', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('connected')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start({ port: 9999, bindAddress: '0.0.0.0' }); expect(spawn).toHaveBeenCalledWith( GATEWAY_BINARY, - ['--config', '/tmp/kaiden-gateway.toml', '--port', '9999', '--bind-address', '0.0.0.0', '--disable-tls'], + ['--config', GATEWAY_CONFIG_PATH, '--port', '9999', '--bind-address', '0.0.0.0', '--disable-tls'], expect.objectContaining({ detached: false }), ); }); @@ -304,15 +291,14 @@ describe('start', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('connected')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start({ disableTls: false }); expect(spawn).toHaveBeenCalledWith( GATEWAY_BINARY, - ['--config', '/tmp/kaiden-gateway.toml', '--port', '17670', '--bind-address', '127.0.0.1'], + ['--config', GATEWAY_CONFIG_PATH, '--port', '17670', '--bind-address', '127.0.0.1'], expect.objectContaining({ detached: false }), ); }); @@ -321,9 +307,8 @@ describe('start', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('connected')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start(); await gateway.start(); @@ -332,63 +317,50 @@ describe('start', () => { }); test('throws when gateway binary is not registered', async () => { - vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([ - { name: 'openshell', path: CLI_BINARY }, - ] as unknown as CliToolInfo[]); + vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([] as unknown as CliToolInfo[]); await expect(gateway.start()).rejects.toThrow('openshell-gateway binary not registered'); }); - test('performs health check with openshell status', async () => { + test('performs health check via openshellCli', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start(); - expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, [ - 'status', - '--gateway-endpoint', - 'http://127.0.0.1:17670', - '--gateway-insecure', - ]); + expect(openshellCli.checkEndpointStatus).toHaveBeenCalledWith('http://127.0.0.1:17670'); }); - test('registers gateway with CLI after health check passes', async () => { + test('registers gateway via openshellCli after health check passes', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start(); - expect(exec.exec).toHaveBeenCalledWith(CLI_BINARY, [ - 'gateway', - 'add', - 'http://127.0.0.1:17670', - '--local', - '--name', - 'kaiden-local', - ]); + expect(openshellCli.addGateway).toHaveBeenCalledWith({ + endpoint: 'http://127.0.0.1:17670', + local: true, + name: 'kaiden-local', + }); }); test('skips registerWithCli when skipRegistration is true', async () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start({ skipRegistration: true }); expect(spawn).toHaveBeenCalled(); - expect(exec.exec).not.toHaveBeenCalledWith(CLI_BINARY, expect.arrayContaining(['gateway', 'add'])); + expect(openshellCli.addGateway).not.toHaveBeenCalled(); }); test('stops the spawned process when waitForReady fails', async () => { @@ -397,7 +369,8 @@ describe('start', () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec).mockRejectedValue(new Error('connection refused')); + vi.mocked(exec.exec).mockRejectedValue(new Error('command not found')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(false); let caughtError: unknown; const startPromise = gateway.start().catch((err: unknown) => { @@ -424,9 +397,8 @@ describe('stop', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start(); @@ -451,9 +423,8 @@ describe('isRunning', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start(); @@ -466,9 +437,8 @@ describe('dispose', () => { vi.spyOn(console, 'log').mockImplementation(() => undefined); const proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); await gateway.start(); @@ -478,32 +448,31 @@ describe('dispose', () => { }); }); -describe('supervisor image pinning', () => { - test('writes config file pinning supervisor image to gateway version', async () => { +describe('gateway config generation', () => { + let proc: ReturnType; + + beforeEach(() => { vi.spyOn(console, 'log').mockImplementation(() => undefined); - const proc = createMockChildProcess(); + proc = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); + }); + + test('writes gateway config under the kaiden data directory with bind mounts enabled for podman', async () => { + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); await gateway.start(); - expect(exec.exec).toHaveBeenCalledWith(GATEWAY_BINARY, ['--version']); + expect(mkdir).toHaveBeenCalledWith(GATEWAY_STORAGE_DIRECTORY, { recursive: true }); expect(writeFile).toHaveBeenCalledWith( - '/tmp/kaiden-gateway.toml', - '[openshell.drivers.podman]\nsupervisor_image = "ghcr.io/nvidia/openshell/supervisor:0.0.69"\n', + GATEWAY_CONFIG_PATH, + expect.stringContaining('[openshell.drivers.podman]\nenable_bind_mounts = true'), 'utf-8', ); }); test('config only includes podman driver section', async () => { - vi.spyOn(console, 'log').mockImplementation(() => undefined); - const proc = createMockChildProcess(); - vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); await gateway.start(); @@ -512,66 +481,90 @@ describe('supervisor image pinning', () => { expect(writtenContent).not.toContain('[openshell.drivers.docker]'); }); + test('pins supervisor image to detected gateway version', async () => { + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + + await gateway.start(); + + expect(exec.exec).toHaveBeenCalledWith(GATEWAY_BINARY, ['--version']); + expect(writeFile).toHaveBeenCalledWith( + GATEWAY_CONFIG_PATH, + expect.stringContaining('supervisor_image = "ghcr.io/nvidia/openshell/supervisor:0.0.69"'), + 'utf-8', + ); + }); + test('passes --config flag to spawned gateway process', async () => { - vi.spyOn(console, 'log').mockImplementation(() => undefined); - const proc = createMockChildProcess(); - vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec) - .mockResolvedValueOnce(mockExecResult('openshell-gateway 0.0.69')) - .mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); await gateway.start(); expect(spawn).toHaveBeenCalledWith( GATEWAY_BINARY, - expect.arrayContaining(['--config', '/tmp/kaiden-gateway.toml']), + expect.arrayContaining(['--config', GATEWAY_CONFIG_PATH]), expect.objectContaining({ detached: false }), ); }); test('uses custom supervisorImage without version detection', async () => { - vi.spyOn(console, 'log').mockImplementation(() => undefined); - const proc = createMockChildProcess(); - vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec).mockResolvedValue(mockExecResult('')); - await gateway.start({ supervisorImage: 'my-registry.io/supervisor:custom' }); expect(exec.exec).not.toHaveBeenCalledWith(GATEWAY_BINARY, ['--version']); expect(writeFile).toHaveBeenCalledWith( - '/tmp/kaiden-gateway.toml', - '[openshell.drivers.podman]\nsupervisor_image = "my-registry.io/supervisor:custom"\n', + GATEWAY_CONFIG_PATH, + expect.stringContaining('supervisor_image = "my-registry.io/supervisor:custom"'), 'utf-8', ); }); - test('starts without --config when version detection fails', async () => { - vi.spyOn(console, 'log').mockImplementation(() => undefined); + test('still generates config with bind mounts when version detection fails', async () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); - const proc = createMockChildProcess(); - vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec).mockRejectedValueOnce(new Error('command not found')).mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockRejectedValue(new Error('command not found')); await gateway.start(); - expect(writeFile).not.toHaveBeenCalled(); + expect(writeFile).toHaveBeenCalledWith( + GATEWAY_CONFIG_PATH, + expect.stringContaining('enable_bind_mounts = true'), + 'utf-8', + ); + expect(writeFile).toHaveBeenCalledWith( + GATEWAY_CONFIG_PATH, + expect.not.stringContaining('supervisor_image'), + 'utf-8', + ); expect(spawn).toHaveBeenCalledWith( GATEWAY_BINARY, - ['--port', '17670', '--bind-address', '127.0.0.1', '--disable-tls'], + expect.arrayContaining(['--config', GATEWAY_CONFIG_PATH]), expect.objectContaining({ detached: false }), ); }); - test('starts without --config when version output is unparseable', async () => { - vi.spyOn(console, 'log').mockImplementation(() => undefined); + test('still generates config with bind mounts when version output is unparseable', async () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); - const proc = createMockChildProcess(); - vi.mocked(spawn).mockReturnValue(proc); - vi.mocked(exec.exec).mockResolvedValueOnce(mockExecResult('unknown-format')).mockResolvedValue(mockExecResult('')); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('unknown-format')); + + await gateway.start(); + + expect(writeFile).toHaveBeenCalledWith( + GATEWAY_CONFIG_PATH, + expect.stringContaining('enable_bind_mounts = true'), + 'utf-8', + ); + expect(writeFile).toHaveBeenCalledWith( + GATEWAY_CONFIG_PATH, + expect.not.stringContaining('supervisor_image'), + 'utf-8', + ); + }); + + test('starts without --config when writeFile fails', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); + vi.mocked(writeFile).mockRejectedValueOnce(new Error('permission denied')); await gateway.start(); - expect(writeFile).not.toHaveBeenCalled(); expect(spawn).toHaveBeenCalledWith( GATEWAY_BINARY, ['--port', '17670', '--bind-address', '127.0.0.1', '--disable-tls'], diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.toml.template b/packages/main/src/plugin/openshell-cli/openshell-gateway.toml.template new file mode 100644 index 000000000..941738e8b --- /dev/null +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.toml.template @@ -0,0 +1,8 @@ +[openshell] +version = 1 + +[openshell.drivers.podman] +enable_bind_mounts = {{enableBindMounts}} +{{#supervisorImage}} +supervisor_image = "{{{supervisorImage}}}" +{{/supervisorImage}} diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.ts b/packages/main/src/plugin/openshell-cli/openshell-gateway.ts index b04e5d658..ab58691c2 100644 --- a/packages/main/src/plugin/openshell-cli/openshell-gateway.ts +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.ts @@ -18,17 +18,21 @@ import type { ChildProcess } from 'node:child_process'; import { spawn } from 'node:child_process'; -import { writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; +import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import type { Disposable } from '@openkaiden/api'; import { inject, injectable, preDestroy } from 'inversify'; +import Mustache from 'mustache'; import { CliToolRegistry } from '/@/plugin/cli-tool-registry.js'; +import { Directories } from '/@/plugin/directories.js'; import { OpenshellCli } from '/@/plugin/openshell-cli/openshell-cli.js'; +import { Exec } from '/@/plugin/util/exec.js'; import type { OpenshellGatewayStartOptions } from '/@api/openshell-gateway-info.js'; +import gatewayConfigTemplate from './openshell-gateway.toml.template?raw'; + const DEFAULT_PORT = 17670; const DEFAULT_BIND_ADDRESS = '127.0.0.1'; const HEALTH_CHECK_INTERVAL_MS = 1000; @@ -51,10 +55,14 @@ export class OpenshellGateway implements Disposable { #bindAddress: string = DEFAULT_BIND_ADDRESS; constructor( + @inject(Exec) + private readonly exec: Exec, @inject(CliToolRegistry) private readonly cliToolRegistry: CliToolRegistry, @inject(OpenshellCli) private readonly openshellCli: OpenshellCli, + @inject(Directories) + private readonly directories: Directories, ) {} async init(): Promise { @@ -134,7 +142,7 @@ export class OpenshellGateway implements Disposable { this.#bindAddress = options.bindAddress; } - const configPath = await this.writeSupervisorConfig(binaryPath, options?.supervisorImage); + const configPath = await this.createGatewayConfig(binaryPath, options?.supervisorImage); const args = this.buildArgs(options?.disableTls ?? true, configPath); console.log(`[openshell-gateway] starting: ${binaryPath} ${args.join(' ')}`); @@ -236,22 +244,36 @@ export class OpenshellGateway implements Disposable { return token; } - private async writeSupervisorConfig(binaryPath: string, supervisorImage?: string): Promise { + private async createGatewayConfig(binaryPath: string, supervisorImage?: string): Promise { try { let image = supervisorImage; if (!image) { - const version = await this.getGatewayVersion(binaryPath); - image = `${SUPERVISOR_IMAGE_BASE}:${version}`; + try { + const version = await this.getGatewayVersion(binaryPath); + image = `${SUPERVISOR_IMAGE_BASE}:${version}`; + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + console.warn(`[openshell-gateway] unable to detect version for supervisor pinning: ${message}`); + } } - const configPath = join(tmpdir(), 'kaiden-gateway.toml'); - const content = ['[openshell.drivers.podman]', `supervisor_image = "${image}"`, ''].join('\n'); - await writeFile(configPath, content, 'utf-8'); - console.log(`[openshell-gateway] supervisor image pinned to ${image}`); + const storageDirectory = join(this.directories.getDataDirectory(), 'openshell-gateway'); + const configPath = join(storageDirectory, 'gateway.toml'); + const config = Mustache.render(gatewayConfigTemplate, { + enableBindMounts: true, + supervisorImage: image, + }); + + await mkdir(storageDirectory, { recursive: true }); + await writeFile(configPath, config, 'utf-8'); + if (image) { + console.log(`[openshell-gateway] supervisor image pinned to ${image}`); + } + console.log(`[openshell-gateway] generated local gateway config at ${configPath}`); return configPath; } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); - console.warn(`[openshell-gateway] unable to pin supervisor image: ${message}`); + console.warn(`[openshell-gateway] failed to generate gateway config: ${message}`); return undefined; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d93583eb3..ab792f813 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,6 +270,9 @@ importers: '@types/ms': specifier: ^2.1.0 version: 2.1.0 + '@types/mustache': + specifier: ^4.2.6 + version: 4.2.6 '@types/node': specifier: ^24 version: 24.1.0 @@ -2588,6 +2591,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/mustache@4.2.6': + resolution: {integrity: sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw==} + '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} @@ -9728,6 +9734,8 @@ snapshots: '@types/ms@2.1.0': {} + '@types/mustache@4.2.6': {} + '@types/node-fetch@2.6.13': dependencies: '@types/node': 24.11.0 From 6dc103d70bda53fd1f27f6bc43ae0edbb96b158f Mon Sep 17 00:00:00 2001 From: Evzen Gasta Date: Sat, 4 Jul 2026 08:16:57 +0200 Subject: [PATCH 4/4] chore: removed enable_bind_mounts Signed-off-by: Evzen Gasta --- .../openshell-cli/openshell-gateway.spec.ts | 18 ++++-------------- .../openshell-gateway.toml.template | 1 - .../plugin/openshell-cli/openshell-gateway.ts | 1 - 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts b/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts index 62db78706..21b346098 100644 --- a/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.spec.ts @@ -458,7 +458,7 @@ describe('gateway config generation', () => { vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true); }); - test('writes gateway config under the kaiden data directory with bind mounts enabled for podman', async () => { + test('writes gateway config under the kaiden data directory', async () => { vi.mocked(exec.exec).mockResolvedValue(mockExecResult('openshell-gateway 0.0.69')); await gateway.start(); @@ -466,7 +466,7 @@ describe('gateway config generation', () => { expect(mkdir).toHaveBeenCalledWith(GATEWAY_STORAGE_DIRECTORY, { recursive: true }); expect(writeFile).toHaveBeenCalledWith( GATEWAY_CONFIG_PATH, - expect.stringContaining('[openshell.drivers.podman]\nenable_bind_mounts = true'), + expect.stringContaining('[openshell.drivers.podman]'), 'utf-8', ); }); @@ -517,17 +517,12 @@ describe('gateway config generation', () => { ); }); - test('still generates config with bind mounts when version detection fails', async () => { + test('still generates config when version detection fails', async () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); vi.mocked(exec.exec).mockRejectedValue(new Error('command not found')); await gateway.start(); - expect(writeFile).toHaveBeenCalledWith( - GATEWAY_CONFIG_PATH, - expect.stringContaining('enable_bind_mounts = true'), - 'utf-8', - ); expect(writeFile).toHaveBeenCalledWith( GATEWAY_CONFIG_PATH, expect.not.stringContaining('supervisor_image'), @@ -540,17 +535,12 @@ describe('gateway config generation', () => { ); }); - test('still generates config with bind mounts when version output is unparseable', async () => { + test('still generates config when version output is unparseable', async () => { vi.spyOn(console, 'warn').mockImplementation(() => undefined); vi.mocked(exec.exec).mockResolvedValue(mockExecResult('unknown-format')); await gateway.start(); - expect(writeFile).toHaveBeenCalledWith( - GATEWAY_CONFIG_PATH, - expect.stringContaining('enable_bind_mounts = true'), - 'utf-8', - ); expect(writeFile).toHaveBeenCalledWith( GATEWAY_CONFIG_PATH, expect.not.stringContaining('supervisor_image'), diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.toml.template b/packages/main/src/plugin/openshell-cli/openshell-gateway.toml.template index 941738e8b..b3a5ff002 100644 --- a/packages/main/src/plugin/openshell-cli/openshell-gateway.toml.template +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.toml.template @@ -2,7 +2,6 @@ version = 1 [openshell.drivers.podman] -enable_bind_mounts = {{enableBindMounts}} {{#supervisorImage}} supervisor_image = "{{{supervisorImage}}}" {{/supervisorImage}} diff --git a/packages/main/src/plugin/openshell-cli/openshell-gateway.ts b/packages/main/src/plugin/openshell-cli/openshell-gateway.ts index ab58691c2..4f93f3d07 100644 --- a/packages/main/src/plugin/openshell-cli/openshell-gateway.ts +++ b/packages/main/src/plugin/openshell-cli/openshell-gateway.ts @@ -260,7 +260,6 @@ export class OpenshellGateway implements Disposable { const storageDirectory = join(this.directories.getDataDirectory(), 'openshell-gateway'); const configPath = join(storageDirectory, 'gateway.toml'); const config = Mustache.render(gatewayConfigTemplate, { - enableBindMounts: true, supervisorImage: image, });