codify-plugin-test 0.0.33 → 0.0.35
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/plugin-process.d.ts +13 -0
- package/dist/plugin-process.js +115 -0
- package/dist/plugin-process.js.map +1 -0
- package/dist/plugin-tester.d.ts +4 -14
- package/dist/plugin-tester.js +103 -185
- package/dist/plugin-tester.js.map +1 -1
- package/package.json +3 -2
- package/src/plugin-process.ts +183 -0
- package/src/plugin-tester.ts +113 -263
- package/test/plugin-tester.test.ts +71 -67
- package/test/test-plugin.ts +10 -10
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
stdio: 'pipe',
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
this.childProcess.stderr?.pipe(process.stderr);
|
|
51
|
+
this.childProcess.stdout?.pipe(process.stdout);
|
|
52
|
+
|
|
53
|
+
this.handleSudoRequests(this.childProcess);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async initialize(): Promise<InitializeResponseData> {
|
|
57
|
+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
58
|
+
cmd: 'initialize',
|
|
59
|
+
data: {},
|
|
60
|
+
requestId: nanoid(6),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async validate(data: ValidateRequestData): Promise<ValidateResponseData> {
|
|
65
|
+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
66
|
+
cmd: 'validate',
|
|
67
|
+
data,
|
|
68
|
+
requestId: nanoid(6),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async plan(data: PlanRequestData): Promise<PlanResponseData> {
|
|
73
|
+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
74
|
+
cmd: 'plan',
|
|
75
|
+
data,
|
|
76
|
+
requestId: nanoid(6),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async apply(data: ApplyRequestData): Promise<void> {
|
|
81
|
+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
82
|
+
cmd: 'apply',
|
|
83
|
+
data,
|
|
84
|
+
requestId: nanoid(6),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async import(data: ImportRequestData): Promise<ImportResponseData> {
|
|
89
|
+
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
90
|
+
cmd: 'import',
|
|
91
|
+
data,
|
|
92
|
+
requestId: nanoid(6),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
kill() {
|
|
97
|
+
this.childProcess.kill();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private handleSudoRequests(process: ChildProcess) {
|
|
101
|
+
// Listen for incoming sudo incoming sudo requests
|
|
102
|
+
process.on('message', async (message) => {
|
|
103
|
+
if (!ipcMessageValidator(message)) {
|
|
104
|
+
throw new Error(`Invalid message from plugin. ${JSON.stringify(message, null, 2)}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (message.cmd === MessageCmd.SUDO_REQUEST) {
|
|
108
|
+
const { data, requestId } = message;
|
|
109
|
+
if (!sudoRequestValidator(data)) {
|
|
110
|
+
throw new Error(`Invalid sudo request from plugin. ${JSON.stringify(sudoRequestValidator.errors, null, 2)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { command, options } = data as unknown as SudoRequestData;
|
|
114
|
+
|
|
115
|
+
console.log(`Running command with sudo: 'sudo ${command}'`)
|
|
116
|
+
const result = await sudoSpawn(command, options);
|
|
117
|
+
|
|
118
|
+
process.send(<IpcMessageV2>{
|
|
119
|
+
cmd: MessageCmd.SUDO_REQUEST + '_Response',
|
|
120
|
+
data: result,
|
|
121
|
+
requestId,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type CodifySpawnOptions = {
|
|
130
|
+
cwd?: string;
|
|
131
|
+
throws?: boolean,
|
|
132
|
+
} & Omit<SpawnOptions, 'detached' | 'shell' | 'stdio'>
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
*
|
|
136
|
+
* @param cmd Command to run. Ex: `rm -rf`
|
|
137
|
+
* @param opts Options for spawn
|
|
138
|
+
*
|
|
139
|
+
* @see promiseSpawn
|
|
140
|
+
* @see spawn
|
|
141
|
+
*
|
|
142
|
+
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
|
|
143
|
+
*/
|
|
144
|
+
async function sudoSpawn(
|
|
145
|
+
cmd: string,
|
|
146
|
+
opts: CodifySpawnOptions,
|
|
147
|
+
): Promise<{ data: string, status: SpawnStatus }> {
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
const output: string[] = [];
|
|
150
|
+
|
|
151
|
+
const _cmd = `sudo ${cmd}`;
|
|
152
|
+
|
|
153
|
+
// Source start up shells to emulate a users environment vs. a non-interactive non-login shell script
|
|
154
|
+
// Ignore all stdin
|
|
155
|
+
const _process = spawn(`source ~/.zshrc; ${_cmd}`, [], {
|
|
156
|
+
...opts,
|
|
157
|
+
shell: 'zsh',
|
|
158
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const { stderr, stdout } = _process
|
|
162
|
+
stdout.setEncoding('utf8');
|
|
163
|
+
stderr.setEncoding('utf8');
|
|
164
|
+
|
|
165
|
+
stdout.on('data', (data) => {
|
|
166
|
+
output.push(data.toString());
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
stderr.on('data', (data) => {
|
|
170
|
+
output.push(data.toString());
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
stdout.pipe(process.stdout);
|
|
174
|
+
stderr.pipe(process.stderr);
|
|
175
|
+
|
|
176
|
+
_process.on('close', (code) => {
|
|
177
|
+
resolve({
|
|
178
|
+
data: output.join(''),
|
|
179
|
+
status: code === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
}
|
package/src/plugin-tester.ts
CHANGED
|
@@ -1,64 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import chalk from 'chalk';
|
|
2
2
|
import {
|
|
3
|
-
ApplyRequestData,
|
|
4
|
-
ImportRequestData,
|
|
5
3
|
ImportResponseData,
|
|
6
|
-
InitializeResponseData,
|
|
7
|
-
IpcMessageSchema,
|
|
8
|
-
IpcMessageV2,
|
|
9
|
-
MessageCmd,
|
|
10
|
-
PlanRequestData,
|
|
11
4
|
PlanResponseData,
|
|
12
5
|
ResourceConfig,
|
|
13
6
|
ResourceOperation,
|
|
14
|
-
SpawnStatus,
|
|
15
|
-
SudoRequestData,
|
|
16
|
-
SudoRequestDataSchema,
|
|
17
|
-
ValidateRequestData,
|
|
18
|
-
ValidateResponseData
|
|
19
7
|
} from 'codify-schemas';
|
|
20
8
|
import unionBy from 'lodash.unionby';
|
|
21
|
-
import { nanoid } from 'nanoid';
|
|
22
|
-
import { ChildProcess, SpawnOptions, fork, spawn } from 'node:child_process';
|
|
23
|
-
import path from 'node:path';
|
|
24
|
-
|
|
25
|
-
import { CodifyTestUtils } from './test-utils.js';
|
|
26
|
-
|
|
27
|
-
const ajv = new Ajv.default({
|
|
28
|
-
strict: true
|
|
29
|
-
});
|
|
30
|
-
const ipcMessageValidator = ajv.compile(IpcMessageSchema);
|
|
31
|
-
const sudoRequestValidator = ajv.compile(SudoRequestDataSchema);
|
|
32
9
|
|
|
10
|
+
import { PluginProcess } from './plugin-process.js';
|
|
33
11
|
|
|
34
12
|
export class PluginTester {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* PluginTester is a helper class to integration test plugins. It launches plugins via fork() just like CodifyCLI does.
|
|
39
|
-
*
|
|
40
|
-
* @param pluginPath A fully qualified path
|
|
41
|
-
*/
|
|
42
|
-
constructor(pluginPath: string) {
|
|
43
|
-
if (!path.isAbsolute(pluginPath)) {
|
|
44
|
-
throw new Error('A fully qualified path must be supplied to PluginTester');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
this.childProcess = fork(
|
|
48
|
-
pluginPath,
|
|
49
|
-
[],
|
|
50
|
-
{
|
|
51
|
-
// Use default true to test plugins in secure mode (un-able to request sudo directly)
|
|
52
|
-
// detached: true,
|
|
53
|
-
env: { ...process.env },
|
|
54
|
-
execArgv: ['--import', 'tsx/esm', '--inspect=9221'],
|
|
55
|
-
},
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
this.handleSudoRequests(this.childProcess);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async fullTest(
|
|
13
|
+
static async fullTest(
|
|
14
|
+
pluginPath: string,
|
|
62
15
|
configs: ResourceConfig[],
|
|
63
16
|
options?: {
|
|
64
17
|
skipUninstall?: boolean,
|
|
@@ -71,91 +24,110 @@ export class PluginTester {
|
|
|
71
24
|
validateModify?: (plans: PlanResponseData[]) => Promise<void> | void,
|
|
72
25
|
}
|
|
73
26
|
}): Promise<void> {
|
|
27
|
+
const ids = configs.map((c) => c.name ? `${c.type}.${c.name}` : c.type).join(', ')
|
|
28
|
+
console.info(chalk.cyan(`Starting full test of [ ${ids} ]...`))
|
|
29
|
+
|
|
74
30
|
const {
|
|
75
31
|
skipUninstall = false,
|
|
76
32
|
} = options ?? {}
|
|
77
33
|
|
|
78
|
-
const ids = configs.map((c) => c.name ? `${c.type}.${c.name}` : c.type).join(',')
|
|
79
34
|
|
|
80
|
-
|
|
81
|
-
|
|
35
|
+
const plugin = new PluginProcess(pluginPath);
|
|
36
|
+
try {
|
|
37
|
+
console.info(chalk.cyan('Testing initialization...'))
|
|
38
|
+
const initializeResult = await plugin.initialize();
|
|
82
39
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
40
|
+
const unsupportedConfigs = configs.filter((c) =>
|
|
41
|
+
!initializeResult.resourceDefinitions.some((rd) => rd.type === c.type)
|
|
42
|
+
)
|
|
43
|
+
if (unsupportedConfigs.length > 0) {
|
|
44
|
+
throw new Error(`The plugin does not support the following configs supplied:\n ${JSON.stringify(unsupportedConfigs, null, 2)}\n Initialize result: ${JSON.stringify(initializeResult)}`)
|
|
45
|
+
}
|
|
89
46
|
|
|
90
|
-
|
|
91
|
-
|
|
47
|
+
console.info(chalk.cyan('Testing validate...'))
|
|
48
|
+
const validate = await plugin.validate({ configs });
|
|
92
49
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
50
|
+
const invalidConfigs = validate.resourceValidations.filter((v) => !v.isValid)
|
|
51
|
+
if (invalidConfigs.length > 0) {
|
|
52
|
+
throw new Error(`The following configs did not validate:\n ${JSON.stringify(invalidConfigs, null, 2)}`)
|
|
53
|
+
}
|
|
97
54
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
55
|
+
console.info(chalk.cyan('Testing plan...'))
|
|
56
|
+
const plans = [];
|
|
57
|
+
for (const config of configs) {
|
|
58
|
+
plans.push(await plugin.plan({
|
|
59
|
+
desired: config,
|
|
60
|
+
isStateful: false,
|
|
61
|
+
state: undefined,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
107
64
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
65
|
+
if (options?.validatePlan) {
|
|
66
|
+
await options.validatePlan(plans);
|
|
67
|
+
}
|
|
111
68
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
69
|
+
console.info(chalk.cyan('Testing apply...'))
|
|
70
|
+
for (const plan of plans) {
|
|
71
|
+
await plugin.apply({
|
|
72
|
+
planId: plan.planId
|
|
73
|
+
});
|
|
74
|
+
}
|
|
118
75
|
|
|
119
|
-
|
|
120
|
-
|
|
76
|
+
if (options?.validateApply) {
|
|
77
|
+
await options.validateApply(plans);
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
plugin.kill();
|
|
121
81
|
}
|
|
122
82
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
importResults
|
|
128
|
-
|
|
83
|
+
const importPlugin = new PluginProcess(pluginPath);
|
|
84
|
+
try {
|
|
85
|
+
console.info(chalk.cyan('Testing import...'))
|
|
86
|
+
|
|
87
|
+
const importResults = [];
|
|
88
|
+
for (const config of configs) {
|
|
89
|
+
const importResult = await importPlugin.import({ config })
|
|
90
|
+
importResults.push(importResult);
|
|
91
|
+
}
|
|
129
92
|
|
|
130
|
-
|
|
131
|
-
|
|
93
|
+
if (options?.validateImport) {
|
|
94
|
+
await options.validateImport(importResults.map((r) => r.result[0]));
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
importPlugin.kill();
|
|
132
98
|
}
|
|
133
99
|
|
|
134
100
|
if (options?.testModify) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
101
|
+
const modifyPlugin = new PluginProcess(pluginPath);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
console.info(chalk.cyan('Testing modify...'))
|
|
105
|
+
|
|
106
|
+
const modifyPlans = [];
|
|
107
|
+
for (const config of options.testModify.modifiedConfigs) {
|
|
108
|
+
modifyPlans.push(await modifyPlugin.plan({
|
|
109
|
+
desired: config,
|
|
110
|
+
isStateful: false,
|
|
111
|
+
state: undefined,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
145
114
|
|
|
146
|
-
|
|
147
|
-
|
|
115
|
+
if (modifyPlans.some((p) => p.operation !== ResourceOperation.MODIFY)) {
|
|
116
|
+
throw new Error(`Error while testing modify. Non-modify results were found in the plan:
|
|
148
117
|
${JSON.stringify(modifyPlans, null, 2)}`)
|
|
149
|
-
|
|
118
|
+
}
|
|
150
119
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
120
|
+
for (const plan of modifyPlans) {
|
|
121
|
+
await modifyPlugin.apply({
|
|
122
|
+
planId: plan.planId
|
|
123
|
+
});
|
|
124
|
+
}
|
|
156
125
|
|
|
157
|
-
|
|
158
|
-
|
|
126
|
+
if (options.testModify.validateModify) {
|
|
127
|
+
await options.testModify.validateModify(modifyPlans);
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
modifyPlugin.kill();
|
|
159
131
|
}
|
|
160
132
|
}
|
|
161
133
|
|
|
@@ -167,112 +139,46 @@ ${JSON.stringify(modifyPlans, null, 2)}`)
|
|
|
167
139
|
const id = (config: ResourceConfig) => config.type + (config.name ? `.${config.name}` : '')
|
|
168
140
|
|
|
169
141
|
const configsToDestroy = unionBy(modifiedConfigs, configsWithNames, id);
|
|
170
|
-
await this.uninstall(configsToDestroy.toReversed(), options);
|
|
142
|
+
await this.uninstall(pluginPath, configsToDestroy.toReversed(), options);
|
|
171
143
|
}
|
|
172
144
|
}
|
|
173
145
|
|
|
174
|
-
async uninstall(configs: ResourceConfig[], options?: {
|
|
146
|
+
static async uninstall(pluginPath: string, configs: ResourceConfig[], options?: {
|
|
175
147
|
validateDestroy?: (plans: PlanResponseData[]) => Promise<void> | void
|
|
176
148
|
}) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
plans
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
for (const plan of plans) {
|
|
190
|
-
if (plan.operation !== ResourceOperation.DESTROY) {
|
|
191
|
-
throw new Error(`Expect resource operation to be 'destroy' but instead received plan: \n ${JSON.stringify(plan, null, 2)}`)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
await this.apply({
|
|
195
|
-
planId: plan.planId
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (options?.validateDestroy) {
|
|
200
|
-
await options.validateDestroy(plans);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async initialize(): Promise<InitializeResponseData> {
|
|
205
|
-
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
206
|
-
cmd: 'initialize',
|
|
207
|
-
data: {},
|
|
208
|
-
requestId: nanoid(6),
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async validate(data: ValidateRequestData): Promise<ValidateResponseData> {
|
|
213
|
-
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
214
|
-
cmd: 'validate',
|
|
215
|
-
data,
|
|
216
|
-
requestId: nanoid(6),
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async plan(data: PlanRequestData): Promise<PlanResponseData> {
|
|
221
|
-
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
222
|
-
cmd: 'plan',
|
|
223
|
-
data,
|
|
224
|
-
requestId: nanoid(6),
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async apply(data: ApplyRequestData): Promise<void> {
|
|
229
|
-
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
230
|
-
cmd: 'apply',
|
|
231
|
-
data,
|
|
232
|
-
requestId: nanoid(6),
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async import(data: ImportRequestData): Promise<ImportResponseData> {
|
|
237
|
-
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
238
|
-
cmd: 'import',
|
|
239
|
-
data,
|
|
240
|
-
requestId: nanoid(6),
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
kill() {
|
|
245
|
-
this.childProcess.kill();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private handleSudoRequests(process: ChildProcess) {
|
|
249
|
-
// Listen for incoming sudo incoming sudo requests
|
|
250
|
-
process.on('message', async (message) => {
|
|
251
|
-
if (!ipcMessageValidator(message)) {
|
|
252
|
-
throw new Error(`Invalid message from plugin. ${JSON.stringify(message, null, 2)}`);
|
|
149
|
+
const destroyPlugin = new PluginProcess(pluginPath);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
console.info(chalk.cyan('Testing destroy...'))
|
|
153
|
+
|
|
154
|
+
const plans = [];
|
|
155
|
+
for (const config of configs) {
|
|
156
|
+
plans.push(await destroyPlugin.plan({
|
|
157
|
+
isStateful: true,
|
|
158
|
+
state: config,
|
|
159
|
+
desired: undefined
|
|
160
|
+
}))
|
|
253
161
|
}
|
|
254
162
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
throw new Error(`Invalid sudo request from plugin. ${JSON.stringify(sudoRequestValidator.errors, null, 2)}`);
|
|
163
|
+
for (const plan of plans) {
|
|
164
|
+
if (plan.operation !== ResourceOperation.DESTROY) {
|
|
165
|
+
throw new Error(`Expect resource operation to be 'destroy' but instead received plan: \n ${JSON.stringify(plans, null, 2)}`)
|
|
259
166
|
}
|
|
260
167
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
168
|
+
await destroyPlugin.apply({
|
|
169
|
+
planId: plan.planId
|
|
170
|
+
});
|
|
171
|
+
}
|
|
265
172
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
data: result,
|
|
269
|
-
requestId,
|
|
270
|
-
})
|
|
173
|
+
if (options?.validateDestroy) {
|
|
174
|
+
await options.validateDestroy(plans);
|
|
271
175
|
}
|
|
272
|
-
}
|
|
176
|
+
} finally {
|
|
177
|
+
destroyPlugin.kill();
|
|
178
|
+
}
|
|
273
179
|
}
|
|
274
180
|
|
|
275
|
-
private addNamesToConfigs(configs: ResourceConfig[]): ResourceConfig[] {
|
|
181
|
+
private static addNamesToConfigs(configs: ResourceConfig[]): ResourceConfig[] {
|
|
276
182
|
const configsWithNames = new Array<ResourceConfig>();
|
|
277
183
|
|
|
278
184
|
const typeSet = new Set(configs.map((c) => c.type));
|
|
@@ -289,59 +195,3 @@ ${JSON.stringify(modifyPlans, null, 2)}`)
|
|
|
289
195
|
}
|
|
290
196
|
}
|
|
291
197
|
|
|
292
|
-
|
|
293
|
-
type CodifySpawnOptions = {
|
|
294
|
-
cwd?: string;
|
|
295
|
-
throws?: boolean,
|
|
296
|
-
} & Omit<SpawnOptions, 'detached' | 'shell' | 'stdio'>
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
*
|
|
300
|
-
* @param cmd Command to run. Ex: `rm -rf`
|
|
301
|
-
* @param opts Options for spawn
|
|
302
|
-
*
|
|
303
|
-
* @see promiseSpawn
|
|
304
|
-
* @see spawn
|
|
305
|
-
*
|
|
306
|
-
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
|
|
307
|
-
*/
|
|
308
|
-
async function sudoSpawn(
|
|
309
|
-
cmd: string,
|
|
310
|
-
opts: CodifySpawnOptions,
|
|
311
|
-
): Promise<{ data: string, status: SpawnStatus }> {
|
|
312
|
-
return new Promise((resolve) => {
|
|
313
|
-
const output: string[] = [];
|
|
314
|
-
|
|
315
|
-
const _cmd = `sudo ${cmd}`;
|
|
316
|
-
|
|
317
|
-
// Source start up shells to emulate a users environment vs. a non-interactive non-login shell script
|
|
318
|
-
// Ignore all stdin
|
|
319
|
-
const _process = spawn(`source ~/.zshrc; ${_cmd}`, [], {
|
|
320
|
-
...opts,
|
|
321
|
-
shell: 'zsh',
|
|
322
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const { stderr, stdout } = _process
|
|
326
|
-
stdout.setEncoding('utf8');
|
|
327
|
-
stderr.setEncoding('utf8');
|
|
328
|
-
|
|
329
|
-
stdout.on('data', (data) => {
|
|
330
|
-
output.push(data.toString());
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
stderr.on('data', (data) => {
|
|
334
|
-
output.push(data.toString());
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
stdout.pipe(process.stdout);
|
|
338
|
-
stderr.pipe(process.stderr);
|
|
339
|
-
|
|
340
|
-
_process.on('close', (code) => {
|
|
341
|
-
resolve({
|
|
342
|
-
data: output.join(''),
|
|
343
|
-
status: code === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
|
|
344
|
-
})
|
|
345
|
-
})
|
|
346
|
-
})
|
|
347
|
-
}
|