Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ 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';
import type { Exec } from '/@/plugin/util/exec.js';
import type { CliToolInfo } from '/@api/cli-tool-info.js';
import type { GatewayInfo } from '/@api/openshell-gateway-info.js';

Expand Down Expand Up @@ -61,7 +60,6 @@ function mockExecResult(stdout = ''): RunResult {

let gateway: OpenshellGateway;

const exec = new Exec({} as Proxy);
const cliToolRegistry = {
getCliToolInfos: vi.fn(),
} as unknown as CliToolRegistry;
Expand All @@ -74,16 +72,21 @@ const openshellCli = {
} as unknown as OpenshellCli;

const directories = {
getDataDirectory: vi.fn(),
getDataDirectory: vi.fn().mockReturnValue(KAIDEN_DATA_DIRECTORY),
} as unknown as Directories;

const exec = {
exec: vi.fn(),
} as unknown as Exec;

beforeEach(() => {
vi.resetAllMocks();
vi.mocked(directories.getDataDirectory).mockReturnValue(KAIDEN_DATA_DIRECTORY);
vi.mocked(cliToolRegistry.getCliToolInfos).mockReturnValue([
{ name: 'openshell-gateway', path: GATEWAY_BINARY },
] as unknown as CliToolInfo[]);
vi.mocked(directories.getDataDirectory).mockReturnValue(KAIDEN_DATA_DIRECTORY);
gateway = new OpenshellGateway(exec, cliToolRegistry, openshellCli, directories);
vi.mocked(exec.exec).mockResolvedValue({ command: '', stdout: '', stderr: '' });
gateway = new OpenshellGateway(cliToolRegistry, openshellCli, directories, exec);
});

describe('init', () => {
Expand Down Expand Up @@ -255,7 +258,7 @@ describe('getGatewayBinaryPath', () => {
});

describe('start', () => {
test('spawns the gateway process with default args', async () => {
test('spawns the gateway process with default args including config', async () => {
vi.spyOn(console, 'log').mockImplementation(() => undefined);
const proc = createMockChildProcess();
vi.mocked(spawn).mockReturnValue(proc);
Expand Down Expand Up @@ -363,6 +366,63 @@ describe('start', () => {
expect(openshellCli.addGateway).not.toHaveBeenCalled();
});

test('generates certs by calling the gateway binary with generate-certs', async () => {
vi.spyOn(console, 'log').mockImplementation(() => undefined);
const proc = createMockChildProcess();
vi.mocked(spawn).mockReturnValue(proc);
vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true);

await gateway.start();

expect(mkdir).toHaveBeenCalledWith(GATEWAY_STORAGE_DIRECTORY, { recursive: true });
expect(exec.exec).toHaveBeenCalledWith(GATEWAY_BINARY, [
'generate-certs',
'--server-san',
'127.0.0.1',
'--server-san',
'localhost',
'--server-san',
'host.openshell.internal',
'--output-dir',
GATEWAY_STORAGE_DIRECTORY,
]);
});

test('writes gateway.toml config with JWT paths', async () => {
vi.spyOn(console, 'log').mockImplementation(() => undefined);
const proc = createMockChildProcess();
vi.mocked(spawn).mockReturnValue(proc);
vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true);

await gateway.start();

expect(writeFile).toHaveBeenCalledWith(
GATEWAY_CONFIG_PATH,
expect.stringContaining('[openshell.gateway.gateway_jwt]'),
'utf-8',
);
expect(writeFile).toHaveBeenCalledWith(GATEWAY_CONFIG_PATH, expect.stringContaining('signing_key_path'), 'utf-8');
expect(writeFile).toHaveBeenCalledWith(GATEWAY_CONFIG_PATH, expect.stringContaining('public_key_path'), 'utf-8');
expect(writeFile).toHaveBeenCalledWith(GATEWAY_CONFIG_PATH, expect.stringContaining('kid_path'), 'utf-8');
});

test('starts gateway without --config when cert generation fails', async () => {
vi.spyOn(console, 'log').mockImplementation(() => undefined);
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const proc = createMockChildProcess();
vi.mocked(spawn).mockReturnValue(proc);
vi.mocked(openshellCli.checkEndpointStatus).mockResolvedValue(true);
vi.mocked(exec.exec).mockRejectedValue(new Error('generate-certs failed'));

await gateway.start();

expect(spawn).toHaveBeenCalledWith(
GATEWAY_BINARY,
['--port', '17670', '--bind-address', '127.0.0.1', '--disable-tls'],
expect.objectContaining({ detached: false }),
);
});

test('stops the spawned process when waitForReady fails', async () => {
vi.useFakeTimers();
vi.spyOn(console, 'log').mockImplementation(() => undefined);
Expand Down Expand Up @@ -519,7 +579,7 @@ describe('gateway config generation', () => {

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'));
vi.mocked(exec.exec).mockRejectedValueOnce(new Error('command not found'));

await gateway.start();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ version = 1
{{#supervisorImage}}
supervisor_image = "{{{supervisorImage}}}"
{{/supervisorImage}}

[openshell.gateway.auth]
allow_unauthenticated_users = true

[openshell.gateway.gateway_jwt]
signing_key_path = "{{{gatewayDir}}}/jwt/signing.pem"
public_key_path = "{{{gatewayDir}}}/jwt/public.pem"
kid_path = "{{{gatewayDir}}}/jwt/kid"
ttl_secs = 3600
21 changes: 19 additions & 2 deletions packages/main/src/plugin/openshell-cli/openshell-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +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,
@inject(Exec)
private readonly exec: Exec,
) {}

async init(): Promise<void> {
Expand Down Expand Up @@ -220,6 +220,20 @@ export class OpenshellGateway implements Disposable {
this.stop().catch((err: unknown) => console.error('[openshell-gateway] failed to stop: ', err));
}

private async generateCerts(binaryPath: string, gatewayDir: string): Promise<void> {
await this.exec.exec(binaryPath, [
'generate-certs',
'--server-san',
'127.0.0.1',
'--server-san',
'localhost',
'--server-san',
'host.openshell.internal',
'--output-dir',
gatewayDir,
]);
}

private buildArgs(disableTls: boolean, configPath?: string): string[] {
const args: string[] = [];
if (configPath) {
Expand Down Expand Up @@ -259,8 +273,11 @@ export class OpenshellGateway implements Disposable {

const storageDirectory = join(this.directories.getDataDirectory(), 'openshell-gateway');
const configPath = join(storageDirectory, 'gateway.toml');
await this.generateCerts(binaryPath, storageDirectory);
const config = Mustache.render(gatewayConfigTemplate, {
supervisorImage: image,
gatewayDir: storageDirectory,
q: '"',
});

await mkdir(storageDirectory, { recursive: true });
Expand Down
Loading