Skip to content

Commit 2466ae7

Browse files
committed
fix: Added separate plugins for each stage of the full test
1 parent c062f4f commit 2466ae7

4 files changed

Lines changed: 372 additions & 340 deletions

File tree

src/plugin-process.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import Ajv from 'ajv';
2+
import {
3+
ApplyRequestData, ImportRequestData, ImportResponseData,
4+
InitializeResponseData,
5+
IpcMessageSchema,
6+
IpcMessageV2,
7+
MessageCmd, PlanRequestData, PlanResponseData,
8+
SpawnStatus,
9+
SudoRequestData,
10+
SudoRequestDataSchema, ValidateRequestData, ValidateResponseData
11+
} from 'codify-schemas';
12+
import { nanoid } from 'nanoid';
13+
import { ChildProcess, SpawnOptions, fork, spawn } from 'node:child_process';
14+
import path from 'node:path';
15+
16+
import { CodifyTestUtils } from './test-utils.js';
17+
18+
const ajv = new Ajv.default({
19+
strict: true
20+
});
21+
22+
const ipcMessageValidator = ajv.compile(IpcMessageSchema);
23+
const sudoRequestValidator = ajv.compile(SudoRequestDataSchema);
24+
25+
export class PluginProcess {
26+
childProcess: ChildProcess
27+
28+
/**
29+
* PluginTester is a helper class to integration test plugins. It launches plugins via fork() just like CodifyCLI does.
30+
*
31+
* @param pluginPath A fully qualified path
32+
*/
33+
constructor(pluginPath: string) {
34+
if (!path.isAbsolute(pluginPath)) {
35+
throw new Error('A fully qualified path must be supplied to PluginTester');
36+
}
37+
38+
this.childProcess = fork(
39+
pluginPath,
40+
[],
41+
{
42+
// Use default true to test plugins in secure mode (un-able to request sudo directly)
43+
// detached: true,
44+
env: { ...process.env },
45+
execArgv: ['--import', 'tsx/esm'],
46+
},
47+
)
48+
49+
this.handleSudoRequests(this.childProcess);
50+
}
51+
52+
async initialize(): Promise<InitializeResponseData> {
53+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
54+
cmd: 'initialize',
55+
data: {},
56+
requestId: nanoid(6),
57+
});
58+
}
59+
60+
async validate(data: ValidateRequestData): Promise<ValidateResponseData> {
61+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
62+
cmd: 'validate',
63+
data,
64+
requestId: nanoid(6),
65+
});
66+
}
67+
68+
async plan(data: PlanRequestData): Promise<PlanResponseData> {
69+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
70+
cmd: 'plan',
71+
data,
72+
requestId: nanoid(6),
73+
});
74+
}
75+
76+
async apply(data: ApplyRequestData): Promise<void> {
77+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
78+
cmd: 'apply',
79+
data,
80+
requestId: nanoid(6),
81+
});
82+
}
83+
84+
async import(data: ImportRequestData): Promise<ImportResponseData> {
85+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
86+
cmd: 'import',
87+
data,
88+
requestId: nanoid(6),
89+
});
90+
}
91+
92+
kill() {
93+
this.childProcess.kill();
94+
}
95+
96+
private handleSudoRequests(process: ChildProcess) {
97+
// Listen for incoming sudo incoming sudo requests
98+
process.on('message', async (message) => {
99+
if (!ipcMessageValidator(message)) {
100+
throw new Error(`Invalid message from plugin. ${JSON.stringify(message, null, 2)}`);
101+
}
102+
103+
if (message.cmd === MessageCmd.SUDO_REQUEST) {
104+
const { data, requestId } = message;
105+
if (!sudoRequestValidator(data)) {
106+
throw new Error(`Invalid sudo request from plugin. ${JSON.stringify(sudoRequestValidator.errors, null, 2)}`);
107+
}
108+
109+
const { command, options } = data as unknown as SudoRequestData;
110+
111+
console.log(`Running command with sudo: 'sudo ${command}'`)
112+
const result = await sudoSpawn(command, options);
113+
114+
process.send(<IpcMessageV2>{
115+
cmd: MessageCmd.SUDO_REQUEST + '_Response',
116+
data: result,
117+
requestId,
118+
})
119+
}
120+
})
121+
}
122+
123+
}
124+
125+
type CodifySpawnOptions = {
126+
cwd?: string;
127+
throws?: boolean,
128+
} & Omit<SpawnOptions, 'detached' | 'shell' | 'stdio'>
129+
130+
/**
131+
*
132+
* @param cmd Command to run. Ex: `rm -rf`
133+
* @param opts Options for spawn
134+
*
135+
* @see promiseSpawn
136+
* @see spawn
137+
*
138+
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
139+
*/
140+
async function sudoSpawn(
141+
cmd: string,
142+
opts: CodifySpawnOptions,
143+
): Promise<{ data: string, status: SpawnStatus }> {
144+
return new Promise((resolve) => {
145+
const output: string[] = [];
146+
147+
const _cmd = `sudo ${cmd}`;
148+
149+
// Source start up shells to emulate a users environment vs. a non-interactive non-login shell script
150+
// Ignore all stdin
151+
const _process = spawn(`source ~/.zshrc; ${_cmd}`, [], {
152+
...opts,
153+
shell: 'zsh',
154+
stdio: ['ignore', 'pipe', 'pipe'],
155+
});
156+
157+
const { stderr, stdout } = _process
158+
stdout.setEncoding('utf8');
159+
stderr.setEncoding('utf8');
160+
161+
stdout.on('data', (data) => {
162+
output.push(data.toString());
163+
})
164+
165+
stderr.on('data', (data) => {
166+
output.push(data.toString());
167+
})
168+
169+
stdout.pipe(process.stdout);
170+
stderr.pipe(process.stderr);
171+
172+
_process.on('close', (code) => {
173+
resolve({
174+
data: output.join(''),
175+
status: code === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
176+
})
177+
})
178+
})
179+
}

0 commit comments

Comments
 (0)