codify-plugin-test 0.0.32 → 0.0.34
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 +112 -0
- package/dist/plugin-process.js.map +1 -0
- package/dist/plugin-tester.d.ts +4 -14
- package/dist/plugin-tester.js +103 -176
- package/dist/plugin-tester.js.map +1 -1
- package/package.json +3 -2
- package/src/plugin-process.ts +179 -0
- package/src/plugin-tester.ts +114 -253
- package/test/plugin-tester.test.ts +71 -67
- package/test/test-plugin.ts +10 -10
|
@@ -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
|
+
}
|
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,197 +24,161 @@ 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 initializeResult = await this.initialize();
|
|
79
|
-
|
|
80
|
-
const unsupportedConfigs = configs.filter((c) =>
|
|
81
|
-
!initializeResult.resourceDefinitions.some((rd) => rd.type === c.type)
|
|
82
|
-
)
|
|
83
|
-
if (unsupportedConfigs.length > 0) {
|
|
84
|
-
throw new Error(`The plugin does not support the following configs supplied:\n ${JSON.stringify(unsupportedConfigs, null, 2)}\n Initialize result: ${JSON.stringify(initializeResult)}`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const validate = await this.validate({ configs });
|
|
88
34
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
35
|
+
const plugin = new PluginProcess(pluginPath);
|
|
36
|
+
try {
|
|
37
|
+
console.info(chalk.cyan('Testing initialization...'))
|
|
38
|
+
const initializeResult = await plugin.initialize();
|
|
93
39
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (options?.validatePlan) {
|
|
104
|
-
await options.validatePlan(plans);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const plan of plans) {
|
|
108
|
-
await this.apply({
|
|
109
|
-
planId: plan.planId
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (options?.validateApply) {
|
|
114
|
-
await options.validateApply(plans);
|
|
115
|
-
}
|
|
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
|
+
}
|
|
116
46
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const importResult = await this.import({ config })
|
|
120
|
-
importResults.push(importResult);
|
|
121
|
-
}
|
|
47
|
+
console.info(chalk.cyan('Testing validate...'))
|
|
48
|
+
const validate = await plugin.validate({ configs });
|
|
122
49
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
}
|
|
126
54
|
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
for (const config of
|
|
130
|
-
|
|
55
|
+
console.info(chalk.cyan('Testing plan...'))
|
|
56
|
+
const plans = [];
|
|
57
|
+
for (const config of configs) {
|
|
58
|
+
plans.push(await plugin.plan({
|
|
131
59
|
desired: config,
|
|
132
60
|
isStateful: false,
|
|
133
61
|
state: undefined,
|
|
134
62
|
}));
|
|
135
63
|
}
|
|
136
64
|
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
${JSON.stringify(modifyPlans, null, 2)}`)
|
|
65
|
+
if (options?.validatePlan) {
|
|
66
|
+
await options.validatePlan(plans);
|
|
140
67
|
}
|
|
141
68
|
|
|
142
|
-
|
|
143
|
-
|
|
69
|
+
console.info(chalk.cyan('Testing apply...'))
|
|
70
|
+
for (const plan of plans) {
|
|
71
|
+
await plugin.apply({
|
|
144
72
|
planId: plan.planId
|
|
145
73
|
});
|
|
146
74
|
}
|
|
147
75
|
|
|
148
|
-
if (options
|
|
149
|
-
await options.
|
|
76
|
+
if (options?.validateApply) {
|
|
77
|
+
await options.validateApply(plans);
|
|
150
78
|
}
|
|
79
|
+
} finally {
|
|
80
|
+
plugin.kill();
|
|
151
81
|
}
|
|
152
82
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const modifiedConfigs = this.addNamesToConfigs(options?.testModify?.modifiedConfigs ?? [])
|
|
83
|
+
const importPlugin = new PluginProcess(pluginPath);
|
|
84
|
+
try {
|
|
85
|
+
console.info(chalk.cyan('Testing import...'))
|
|
157
86
|
|
|
158
|
-
const
|
|
87
|
+
const importResults = [];
|
|
88
|
+
for (const config of configs) {
|
|
89
|
+
const importResult = await importPlugin.import({ config })
|
|
90
|
+
importResults.push(importResult);
|
|
91
|
+
}
|
|
159
92
|
|
|
160
|
-
|
|
161
|
-
|
|
93
|
+
if (options?.validateImport) {
|
|
94
|
+
await options.validateImport(importResults.map((r) => r.result[0]));
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
importPlugin.kill();
|
|
162
98
|
}
|
|
163
|
-
}
|
|
164
99
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
100
|
+
if (options?.testModify) {
|
|
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
|
+
}
|
|
177
114
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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:
|
|
117
|
+
${JSON.stringify(modifyPlans, null, 2)}`)
|
|
118
|
+
}
|
|
182
119
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
120
|
+
for (const plan of modifyPlans) {
|
|
121
|
+
await modifyPlugin.apply({
|
|
122
|
+
planId: plan.planId
|
|
123
|
+
});
|
|
124
|
+
}
|
|
187
125
|
|
|
188
|
-
|
|
189
|
-
|
|
126
|
+
if (options.testModify.validateModify) {
|
|
127
|
+
await options.testModify.validateModify(modifyPlans);
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
modifyPlugin.kill();
|
|
131
|
+
}
|
|
190
132
|
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async initialize(): Promise<InitializeResponseData> {
|
|
194
|
-
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
195
|
-
cmd: 'initialize',
|
|
196
|
-
data: {},
|
|
197
|
-
requestId: nanoid(6),
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
133
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
requestId: nanoid(6),
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async plan(data: PlanRequestData): Promise<PlanResponseData> {
|
|
210
|
-
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
211
|
-
cmd: 'plan',
|
|
212
|
-
data,
|
|
213
|
-
requestId: nanoid(6),
|
|
214
|
-
});
|
|
215
|
-
}
|
|
134
|
+
if (!skipUninstall) {
|
|
135
|
+
// We need to add unique names to multiple configs with the same type or else it breaks the unionBy below.
|
|
136
|
+
const configsWithNames = this.addNamesToConfigs(configs);
|
|
137
|
+
const modifiedConfigs = this.addNamesToConfigs(options?.testModify?.modifiedConfigs ?? [])
|
|
216
138
|
|
|
217
|
-
|
|
218
|
-
return CodifyTestUtils.sendMessageAndAwaitResponse(this.childProcess, {
|
|
219
|
-
cmd: 'apply',
|
|
220
|
-
data,
|
|
221
|
-
requestId: nanoid(6),
|
|
222
|
-
});
|
|
223
|
-
}
|
|
139
|
+
const id = (config: ResourceConfig) => config.type + (config.name ? `.${config.name}` : '')
|
|
224
140
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
data,
|
|
229
|
-
requestId: nanoid(6),
|
|
230
|
-
});
|
|
141
|
+
const configsToDestroy = unionBy(modifiedConfigs, configsWithNames, id);
|
|
142
|
+
await this.uninstall(pluginPath, configsToDestroy.toReversed(), options);
|
|
143
|
+
}
|
|
231
144
|
}
|
|
232
145
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
146
|
+
static async uninstall(pluginPath: string, configs: ResourceConfig[], options?: {
|
|
147
|
+
validateDestroy?: (plans: PlanResponseData[]) => Promise<void> | void
|
|
148
|
+
}) {
|
|
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
|
+
}))
|
|
242
161
|
}
|
|
243
162
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
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)}`)
|
|
248
166
|
}
|
|
249
167
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
168
|
+
await destroyPlugin.apply({
|
|
169
|
+
planId: plan.planId
|
|
170
|
+
});
|
|
171
|
+
}
|
|
254
172
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
data: result,
|
|
258
|
-
requestId,
|
|
259
|
-
})
|
|
173
|
+
if (options?.validateDestroy) {
|
|
174
|
+
await options.validateDestroy(plans);
|
|
260
175
|
}
|
|
261
|
-
}
|
|
176
|
+
} finally {
|
|
177
|
+
destroyPlugin.kill();
|
|
178
|
+
}
|
|
262
179
|
}
|
|
263
180
|
|
|
264
|
-
private addNamesToConfigs(configs: ResourceConfig[]): ResourceConfig[] {
|
|
181
|
+
private static addNamesToConfigs(configs: ResourceConfig[]): ResourceConfig[] {
|
|
265
182
|
const configsWithNames = new Array<ResourceConfig>();
|
|
266
183
|
|
|
267
184
|
const typeSet = new Set(configs.map((c) => c.type));
|
|
@@ -278,59 +195,3 @@ ${JSON.stringify(modifyPlans, null, 2)}`)
|
|
|
278
195
|
}
|
|
279
196
|
}
|
|
280
197
|
|
|
281
|
-
|
|
282
|
-
type CodifySpawnOptions = {
|
|
283
|
-
cwd?: string;
|
|
284
|
-
throws?: boolean,
|
|
285
|
-
} & Omit<SpawnOptions, 'detached' | 'shell' | 'stdio'>
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
*
|
|
289
|
-
* @param cmd Command to run. Ex: `rm -rf`
|
|
290
|
-
* @param opts Options for spawn
|
|
291
|
-
*
|
|
292
|
-
* @see promiseSpawn
|
|
293
|
-
* @see spawn
|
|
294
|
-
*
|
|
295
|
-
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
|
|
296
|
-
*/
|
|
297
|
-
async function sudoSpawn(
|
|
298
|
-
cmd: string,
|
|
299
|
-
opts: CodifySpawnOptions,
|
|
300
|
-
): Promise<{ data: string, status: SpawnStatus }> {
|
|
301
|
-
return new Promise((resolve) => {
|
|
302
|
-
const output: string[] = [];
|
|
303
|
-
|
|
304
|
-
const _cmd = `sudo ${cmd}`;
|
|
305
|
-
|
|
306
|
-
// Source start up shells to emulate a users environment vs. a non-interactive non-login shell script
|
|
307
|
-
// Ignore all stdin
|
|
308
|
-
const _process = spawn(`source ~/.zshrc; ${_cmd}`, [], {
|
|
309
|
-
...opts,
|
|
310
|
-
shell: 'zsh',
|
|
311
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
const { stderr, stdout } = _process
|
|
315
|
-
stdout.setEncoding('utf8');
|
|
316
|
-
stderr.setEncoding('utf8');
|
|
317
|
-
|
|
318
|
-
stdout.on('data', (data) => {
|
|
319
|
-
output.push(data.toString());
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
stderr.on('data', (data) => {
|
|
323
|
-
output.push(data.toString());
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
stdout.pipe(process.stdout);
|
|
327
|
-
stderr.pipe(process.stderr);
|
|
328
|
-
|
|
329
|
-
_process.on('close', (code) => {
|
|
330
|
-
resolve({
|
|
331
|
-
data: output.join(''),
|
|
332
|
-
status: code === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
|
|
333
|
-
})
|
|
334
|
-
})
|
|
335
|
-
})
|
|
336
|
-
}
|