codify-plugin-lib 1.0.64 → 1.0.66
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/entities/change-set.js +4 -4
- package/dist/entities/errors.d.ts +7 -0
- package/dist/entities/errors.js +9 -0
- package/dist/entities/plan-types.d.ts +1 -1
- package/dist/entities/plan.d.ts +3 -3
- package/dist/entities/plan.js +9 -3
- package/dist/entities/plugin.d.ts +2 -1
- package/dist/entities/plugin.js +10 -0
- package/dist/entities/resource-types.d.ts +1 -1
- package/dist/entities/resource.d.ts +3 -2
- package/dist/entities/resource.js +63 -27
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/messages/handlers.js +16 -2
- package/package.json +1 -1
- package/src/entities/change-set.ts +5 -4
- package/src/entities/errors.ts +17 -0
- package/src/entities/plan-types.ts +1 -1
- package/src/entities/plan.test.ts +3 -15
- package/src/entities/plan.ts +14 -6
- package/src/entities/plugin.test.ts +193 -0
- package/src/entities/plugin.ts +22 -1
- package/src/entities/resource-parameters.test.ts +14 -26
- package/src/entities/resource-stateful-mode.test.ts +247 -0
- package/src/entities/resource-types.ts +1 -1
- package/src/entities/resource.test.ts +12 -24
- package/src/entities/resource.ts +98 -41
- package/src/entities/stateful-parameter.test.ts +2 -5
- package/src/index.ts +0 -1
- package/src/messages/handlers.ts +17 -2
- package/src/utils/test-utils.test.ts +0 -52
- package/src/utils/test-utils.ts +0 -20
package/src/entities/resource.ts
CHANGED
|
@@ -74,38 +74,39 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
74
74
|
return this.validate(parameters);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// TODO:
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// TODO: Currently stateful mode expects that the currentConfig does not need any additional transformations (default and transform parameters)
|
|
78
|
+
// This may change in the future?
|
|
79
|
+
async plan(
|
|
80
|
+
desiredConfig: Partial<T> & ResourceConfig | null,
|
|
81
|
+
currentConfig: Partial<T> & ResourceConfig | null = null,
|
|
82
|
+
statefulMode = false,
|
|
83
|
+
): Promise<Plan<T>> {
|
|
84
|
+
this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
|
|
85
|
+
|
|
81
86
|
const planOptions: PlanOptions<T> = {
|
|
82
|
-
statefulMode
|
|
87
|
+
statefulMode,
|
|
83
88
|
parameterOptions: this.parameterOptions,
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
this.addDefaultValues(desiredConfig);
|
|
92
|
+
await this.applyTransformParameters(desiredConfig);
|
|
87
93
|
|
|
88
94
|
// Parse data from the user supplied config
|
|
89
|
-
const parsedConfig = new ConfigParser(desiredConfig, this.statefulParameters, this.transformParameters)
|
|
95
|
+
const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters)
|
|
90
96
|
const {
|
|
91
|
-
|
|
97
|
+
desiredParameters,
|
|
92
98
|
resourceMetadata,
|
|
93
|
-
|
|
99
|
+
nonStatefulParameters,
|
|
94
100
|
statefulParameters,
|
|
95
|
-
transformParameters,
|
|
96
101
|
} = parsedConfig;
|
|
97
102
|
|
|
98
|
-
// Apply transform parameters. Transform parameters turn into other parameters.
|
|
99
|
-
// Ex: csvFile: './location' => { password: 'pass', 'username': 'user' }
|
|
100
|
-
await this.applyTransformParameters(transformParameters, resourceParameters);
|
|
101
|
-
|
|
102
103
|
// Refresh resource parameters. This refreshes the parameters that configure the resource itself
|
|
103
|
-
const currentParameters = await this.
|
|
104
|
+
const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
|
|
104
105
|
|
|
105
106
|
// Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
|
|
106
107
|
if (currentParameters == null) {
|
|
107
108
|
return Plan.create(
|
|
108
|
-
|
|
109
|
+
desiredParameters,
|
|
109
110
|
null,
|
|
110
111
|
resourceMetadata,
|
|
111
112
|
planOptions,
|
|
@@ -116,7 +117,7 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
116
117
|
const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
|
|
117
118
|
|
|
118
119
|
return Plan.create(
|
|
119
|
-
|
|
120
|
+
desiredParameters,
|
|
120
121
|
{ ...currentParameters, ...statefulCurrentParameters } as Partial<T>,
|
|
121
122
|
resourceMetadata,
|
|
122
123
|
planOptions,
|
|
@@ -232,17 +233,29 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
|
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
private async applyTransformParameters(
|
|
236
|
-
|
|
236
|
+
private async applyTransformParameters(desired: Partial<T> | null): Promise<void> {
|
|
237
|
+
if (!desired) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const transformParameters = [...this.transformParameters.entries()]
|
|
237
242
|
.sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA)! - this.transformParameterOrder.get(keyB)!)
|
|
238
243
|
|
|
239
|
-
for (const [key,
|
|
240
|
-
|
|
244
|
+
for (const [key, transformParameter] of transformParameters) {
|
|
245
|
+
if (desired[key] === undefined) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const transformedValue = await transformParameter.transform(desired[key]);
|
|
241
250
|
|
|
242
251
|
if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
|
|
243
252
|
throw new Error(`Transform parameter ${key as string} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
// Remove original transform parameter from the config
|
|
256
|
+
delete desired[key];
|
|
257
|
+
|
|
258
|
+
// Add the new transformed values
|
|
246
259
|
Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
|
|
247
260
|
// @ts-ignore
|
|
248
261
|
desired[tvKey] = tvValue;
|
|
@@ -250,7 +263,11 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
|
|
|
250
263
|
}
|
|
251
264
|
}
|
|
252
265
|
|
|
253
|
-
private addDefaultValues(desired: Partial<T>): void {
|
|
266
|
+
private addDefaultValues(desired: Partial<T> | null): void {
|
|
267
|
+
if (!desired) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
254
271
|
Object.entries(this.defaultValues)
|
|
255
272
|
.forEach(([key, defaultValue]) => {
|
|
256
273
|
if (defaultValue !== undefined && desired[key as any] === undefined) {
|
|
@@ -260,10 +277,11 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
|
|
|
260
277
|
});
|
|
261
278
|
}
|
|
262
279
|
|
|
263
|
-
private async
|
|
264
|
-
const entriesToRefresh = new Map(
|
|
280
|
+
private async refreshNonStatefulParameters(resourceParameters: Partial<T>): Promise<Partial<T> | null> {
|
|
281
|
+
const entriesToRefresh = new Map<keyof T, T[keyof T]>(
|
|
282
|
+
Object.entries(resourceParameters)
|
|
283
|
+
)
|
|
265
284
|
const currentParameters = await this.refresh(entriesToRefresh);
|
|
266
|
-
|
|
267
285
|
this.validateRefreshResults(currentParameters, entriesToRefresh);
|
|
268
286
|
return currentParameters;
|
|
269
287
|
}
|
|
@@ -308,6 +326,20 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
|
|
|
308
326
|
return currentParameters;
|
|
309
327
|
}
|
|
310
328
|
|
|
329
|
+
private validatePlanInputs(
|
|
330
|
+
desired: Partial<T> & ResourceConfig | null,
|
|
331
|
+
current: Partial<T> & ResourceConfig | null,
|
|
332
|
+
statefulMode: boolean,
|
|
333
|
+
) {
|
|
334
|
+
if (!desired && !current) {
|
|
335
|
+
throw new Error('Desired config and current config cannot both be missing')
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!statefulMode && !desired) {
|
|
339
|
+
throw new Error('Desired config must be provided in non-stateful mode')
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
311
343
|
async validate(parameters: unknown): Promise<ValidationResult> {
|
|
312
344
|
return {
|
|
313
345
|
isValid: true,
|
|
@@ -324,51 +356,76 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
|
|
|
324
356
|
}
|
|
325
357
|
|
|
326
358
|
class ConfigParser<T extends StringIndexedObject> {
|
|
327
|
-
private
|
|
359
|
+
private desiredConfig: Partial<T> & ResourceConfig | null;
|
|
360
|
+
private currentConfig: Partial<T> & ResourceConfig | null;
|
|
328
361
|
private statefulParametersMap: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
|
|
329
362
|
private transformParametersMap: Map<keyof T, TransformParameter<T>>;
|
|
330
363
|
|
|
331
364
|
constructor(
|
|
332
|
-
|
|
365
|
+
desiredConfig: Partial<T> & ResourceConfig | null,
|
|
366
|
+
currentConfig: Partial<T> & ResourceConfig | null,
|
|
333
367
|
statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>,
|
|
334
|
-
|
|
368
|
+
transformParameters: Map<keyof T, TransformParameter<T>>,
|
|
335
369
|
) {
|
|
336
|
-
this.
|
|
370
|
+
this.desiredConfig = desiredConfig;
|
|
371
|
+
this.currentConfig = currentConfig
|
|
337
372
|
this.statefulParametersMap = statefulParameters;
|
|
338
373
|
this.transformParametersMap = transformParameters;
|
|
339
374
|
}
|
|
340
375
|
|
|
341
376
|
get resourceMetadata(): ResourceConfig {
|
|
342
|
-
const
|
|
343
|
-
|
|
377
|
+
const desiredMetadata = this.desiredConfig ? splitUserConfig(this.desiredConfig).resourceMetadata : undefined;
|
|
378
|
+
const currentMetadata = this.currentConfig ? splitUserConfig(this.currentConfig).resourceMetadata : undefined;
|
|
379
|
+
|
|
380
|
+
if (!desiredMetadata && !currentMetadata) {
|
|
381
|
+
throw new Error(`Unable to parse resource metadata from ${this.desiredConfig}, ${this.currentConfig}`)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (currentMetadata && desiredMetadata && (
|
|
385
|
+
Object.keys(desiredMetadata).length !== Object.keys(currentMetadata).length
|
|
386
|
+
|| Object.entries(desiredMetadata).some(([key, value]) => currentMetadata[key] !== value)
|
|
387
|
+
)) {
|
|
388
|
+
throw new Error(`The metadata for the current config does not match the desired config.
|
|
389
|
+
Desired metadata:
|
|
390
|
+
${JSON.stringify(desiredMetadata, null, 2)}
|
|
391
|
+
|
|
392
|
+
Current metadata:
|
|
393
|
+
${JSON.stringify(currentMetadata, null, 2)}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return desiredMetadata ?? currentMetadata!;
|
|
344
397
|
}
|
|
345
398
|
|
|
346
|
-
get
|
|
347
|
-
|
|
399
|
+
get desiredParameters(): Partial<T> | null {
|
|
400
|
+
if (!this.desiredConfig) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const { parameters } = splitUserConfig(this.desiredConfig);
|
|
348
405
|
return parameters;
|
|
349
406
|
}
|
|
350
407
|
|
|
351
|
-
get resourceParameters(): Partial<T> {
|
|
352
|
-
const parameters = this.parameters;
|
|
353
408
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
409
|
+
get parameters(): Partial<T> {
|
|
410
|
+
const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
|
|
411
|
+
const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
|
|
412
|
+
|
|
413
|
+
return { ...(desiredParameters ?? {}), ...(currentParameters ?? {}) } as Partial<T>;
|
|
357
414
|
}
|
|
358
415
|
|
|
359
|
-
get
|
|
416
|
+
get nonStatefulParameters(): Partial<T> {
|
|
360
417
|
const parameters = this.parameters;
|
|
361
418
|
|
|
362
419
|
return Object.fromEntries([
|
|
363
|
-
...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
|
|
420
|
+
...Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))),
|
|
364
421
|
]) as Partial<T>;
|
|
365
422
|
}
|
|
366
423
|
|
|
367
|
-
get
|
|
424
|
+
get statefulParameters(): Partial<T> {
|
|
368
425
|
const parameters = this.parameters;
|
|
369
426
|
|
|
370
427
|
return Object.fromEntries([
|
|
371
|
-
...Object.entries(parameters).filter(([key]) => this.
|
|
428
|
+
...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
|
|
372
429
|
]) as Partial<T>;
|
|
373
430
|
}
|
|
374
431
|
}
|
|
@@ -10,10 +10,8 @@ interface TestConfig {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
class TestArrayParameter extends ArrayStatefulParameter<TestConfig, string> {
|
|
13
|
-
constructor(options?: ArrayStatefulParameterOptions<
|
|
14
|
-
super(options
|
|
15
|
-
name: 'propA'
|
|
16
|
-
})
|
|
13
|
+
constructor(options?: ArrayStatefulParameterOptions<string>) {
|
|
14
|
+
super(options)
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
async applyAddItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
|
|
@@ -104,7 +102,6 @@ describe('Stateful parameter tests', () => {
|
|
|
104
102
|
const testParameter = spy(new class extends TestArrayParameter {
|
|
105
103
|
constructor() {
|
|
106
104
|
super({
|
|
107
|
-
name: 'propA',
|
|
108
105
|
isElementEqual: (desired, current) => current.includes(desired),
|
|
109
106
|
});
|
|
110
107
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,6 @@ export * from './entities/plan-types.js'
|
|
|
11
11
|
export * from './entities/stateful-parameter.js'
|
|
12
12
|
export * from './entities/errors.js'
|
|
13
13
|
|
|
14
|
-
export * from './utils/test-utils.js'
|
|
15
14
|
export * from './utils/utils.js'
|
|
16
15
|
|
|
17
16
|
export async function runPlugin(plugin: Plugin) {
|
package/src/messages/handlers.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
ValidateResponseDataSchema
|
|
16
16
|
} from 'codify-schemas';
|
|
17
17
|
import Ajv2020, { SchemaObject, ValidateFunction } from 'ajv/dist/2020.js';
|
|
18
|
-
import { SudoError } from '../entities/errors.js';
|
|
18
|
+
import { ApplyValidationError, SudoError } from '../entities/errors.js';
|
|
19
19
|
|
|
20
20
|
const SupportedRequests: Record<string, { requestValidator: SchemaObject; responseValidator: SchemaObject; handler: (plugin: Plugin, data: any) => Promise<unknown> }> = {
|
|
21
21
|
'initialize': {
|
|
@@ -117,13 +117,28 @@ export class MessageHandler {
|
|
|
117
117
|
const cmd = message.cmd + '_Response';
|
|
118
118
|
|
|
119
119
|
if (e instanceof SudoError) {
|
|
120
|
-
process.send?.({
|
|
120
|
+
return process.send?.({
|
|
121
121
|
cmd,
|
|
122
122
|
status: MessageStatus.ERROR,
|
|
123
123
|
data: `Plugin: '${this.plugin.name}'. Forbidden usage of sudo for command '${e.command}'. Please contact the plugin developer to fix this.`,
|
|
124
124
|
})
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
if (e instanceof ApplyValidationError) {
|
|
128
|
+
return process.send?.({
|
|
129
|
+
cmd,
|
|
130
|
+
status: MessageStatus.ERROR,
|
|
131
|
+
data: `Plugin: '${this.plugin.name}'. Apply validation was not successful (additional changes are needed to match the desired plan).
|
|
132
|
+
|
|
133
|
+
Validation plan:
|
|
134
|
+
${JSON.stringify(e.validatedPlan, null, 2)},
|
|
135
|
+
|
|
136
|
+
User desired plan:
|
|
137
|
+
${JSON.stringify(e.desiredPlan, null, 2)}
|
|
138
|
+
`
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
const isDebug = process.env.DEBUG?.includes('*') ?? false;
|
|
128
143
|
|
|
129
144
|
process.send?.({
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { ChildProcess } from 'node:child_process';
|
|
3
|
-
import { Readable } from 'stream';
|
|
4
|
-
import { mock } from 'node:test';
|
|
5
|
-
import { AssertionError } from 'chai';
|
|
6
|
-
import { CodifyTestUtils } from './test-utils.js';
|
|
7
|
-
import { describe, expect, it } from 'vitest';
|
|
8
|
-
|
|
9
|
-
describe('Test Utils tests', async () => {
|
|
10
|
-
|
|
11
|
-
const mockChildProcess = () => {
|
|
12
|
-
const process = new ChildProcess();
|
|
13
|
-
process.stdout = new EventEmitter() as Readable;
|
|
14
|
-
process.stderr = new EventEmitter() as Readable
|
|
15
|
-
process.send = () => true;
|
|
16
|
-
|
|
17
|
-
return process;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
it('send a message', async () => {
|
|
21
|
-
const process = mockChildProcess();
|
|
22
|
-
const sendMock = mock.method(process, 'send');
|
|
23
|
-
|
|
24
|
-
CodifyTestUtils.sendMessageToProcessAwaitResponse(process, { cmd: 'message', data: 'data' })
|
|
25
|
-
|
|
26
|
-
expect(sendMock.mock.calls.length).to.eq(1);
|
|
27
|
-
expect(sendMock.mock.calls[0].arguments[0]).to.deep.eq({ cmd: 'message', data: 'data' });
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('send a message and receives the response', async () => {
|
|
31
|
-
const process = mockChildProcess();
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const result = await Promise.all([
|
|
35
|
-
(async () => {
|
|
36
|
-
await sleep(30);
|
|
37
|
-
process.emit('message', { cmd: 'messageResult', data: 'data' })
|
|
38
|
-
})(),
|
|
39
|
-
CodifyTestUtils.sendMessageToProcessAwaitResponse(process, { cmd: 'message', data: 'data' }),
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
expect(result[1]).to.deep.eq({ cmd: 'messageResult', data: 'data' })
|
|
43
|
-
} catch (e) {
|
|
44
|
-
console.log(e);
|
|
45
|
-
throw new AssertionError('Failed to receive message');
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
async function sleep(ms: number) {
|
|
51
|
-
return new Promise((resolve, reject) => setTimeout(resolve, ms))
|
|
52
|
-
}
|
package/src/utils/test-utils.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { ChildProcess } from 'child_process';
|
|
2
|
-
|
|
3
|
-
export class CodifyTestUtils {
|
|
4
|
-
static sendMessageToProcessAwaitResponse(process: ChildProcess, message: any): Promise<any> {
|
|
5
|
-
return new Promise((resolve, reject) => {
|
|
6
|
-
process.on('message', (response) => {
|
|
7
|
-
resolve(response)
|
|
8
|
-
});
|
|
9
|
-
process.on('error', (err) => reject(err))
|
|
10
|
-
process.on('exit', (code) => {
|
|
11
|
-
if (code != 0) {
|
|
12
|
-
reject('Exit code is not 0');
|
|
13
|
-
}
|
|
14
|
-
resolve(code);
|
|
15
|
-
})
|
|
16
|
-
process.send(message);
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
}
|