codify-plugin-lib 1.0.114 → 1.0.116
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/common/errors.d.ts +8 -0
- package/dist/common/errors.js +24 -0
- package/dist/index.js +3 -0
- package/dist/plan/plan.d.ts +19 -10
- package/dist/plan/plan.js +99 -83
- package/dist/plugin/plugin.d.ts +2 -1
- package/dist/plugin/plugin.js +15 -4
- package/package.json +2 -2
- package/src/common/errors.test.ts +43 -0
- package/src/common/errors.ts +31 -0
- package/src/index.ts +4 -0
- package/src/plan/plan.test.ts +8 -4
- package/src/plan/plan.ts +145 -118
- package/src/plugin/plugin.test.ts +116 -13
- package/src/plugin/plugin.ts +24 -6
- package/src/resource/resource-settings.test.ts +6 -3
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class ApplyValidationError extends Error {
|
|
2
|
+
resourceType;
|
|
3
|
+
resourceName;
|
|
4
|
+
plan;
|
|
5
|
+
constructor(plan) {
|
|
6
|
+
super(`Failed to apply changes to resource: "${plan.resourceId}". Additional changes are needed to complete apply.\nChanges remaining:\n${ApplyValidationError.prettyPrintPlan(plan)}`);
|
|
7
|
+
this.resourceType = plan.coreParameters.type;
|
|
8
|
+
this.resourceName = plan.coreParameters.name;
|
|
9
|
+
this.plan = plan;
|
|
10
|
+
}
|
|
11
|
+
static prettyPrintPlan(plan) {
|
|
12
|
+
const { operation, parameters } = plan.toResponse();
|
|
13
|
+
const prettyParameters = parameters.map(({ name, operation, previousValue, newValue }) => ({
|
|
14
|
+
name,
|
|
15
|
+
operation,
|
|
16
|
+
currentValue: previousValue,
|
|
17
|
+
desiredValue: newValue,
|
|
18
|
+
}));
|
|
19
|
+
return JSON.stringify({
|
|
20
|
+
operation,
|
|
21
|
+
parameters: prettyParameters,
|
|
22
|
+
}, null, 2);
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -13,4 +13,7 @@ export * from './utils/utils.js';
|
|
|
13
13
|
export async function runPlugin(plugin) {
|
|
14
14
|
const messageHandler = new MessageHandler(plugin);
|
|
15
15
|
process.on('message', (message) => messageHandler.onMessage(message));
|
|
16
|
+
process.on('beforeExit', () => {
|
|
17
|
+
plugin.kill();
|
|
18
|
+
});
|
|
16
19
|
}
|
package/dist/plan/plan.d.ts
CHANGED
|
@@ -8,9 +8,16 @@ import { ChangeSet } from './change-set.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare class Plan<T extends StringIndexedObject> {
|
|
10
10
|
id: string;
|
|
11
|
+
/**
|
|
12
|
+
* List of changes to make
|
|
13
|
+
*/
|
|
11
14
|
changeSet: ChangeSet<T>;
|
|
15
|
+
/**
|
|
16
|
+
* Ex: name, type, dependsOn etc. Metadata parameters
|
|
17
|
+
*/
|
|
12
18
|
coreParameters: ResourceConfig;
|
|
13
|
-
|
|
19
|
+
statefulMode: boolean;
|
|
20
|
+
constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig, statefulMode: boolean);
|
|
14
21
|
/**
|
|
15
22
|
* The desired config that a plan will achieve after executing all the actions.
|
|
16
23
|
*/
|
|
@@ -19,6 +26,15 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
19
26
|
* The current config that the plan is changing.
|
|
20
27
|
*/
|
|
21
28
|
get currentConfig(): T | null;
|
|
29
|
+
get resourceId(): string;
|
|
30
|
+
static calculate<T extends StringIndexedObject>(params: {
|
|
31
|
+
desiredParameters: Partial<T> | null;
|
|
32
|
+
currentParametersArray: Partial<T>[] | null;
|
|
33
|
+
stateParameters: Partial<T> | null;
|
|
34
|
+
coreParameters: ResourceConfig;
|
|
35
|
+
settings: ParsedResourceSettings<T>;
|
|
36
|
+
statefulMode: boolean;
|
|
37
|
+
}): Plan<T>;
|
|
22
38
|
/**
|
|
23
39
|
* When multiples of the same resource are allowed, this matching function will match a given config with one of the
|
|
24
40
|
* existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
|
|
@@ -34,14 +50,7 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
34
50
|
* @return string
|
|
35
51
|
*/
|
|
36
52
|
getResourceType(): string;
|
|
37
|
-
static
|
|
38
|
-
desiredParameters: Partial<T> | null;
|
|
39
|
-
currentParametersArray: Partial<T>[] | null;
|
|
40
|
-
stateParameters: Partial<T> | null;
|
|
41
|
-
coreParameters: ResourceConfig;
|
|
42
|
-
settings: ParsedResourceSettings<T>;
|
|
43
|
-
statefulMode: boolean;
|
|
44
|
-
}): Plan<T>;
|
|
53
|
+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T>;
|
|
45
54
|
/**
|
|
46
55
|
* Only keep relevant params for the plan. We don't want to change settings that were not already
|
|
47
56
|
* defined.
|
|
@@ -51,7 +60,7 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
51
60
|
* or wants to set. If a parameter is not specified then it's not managed by Codify.
|
|
52
61
|
*/
|
|
53
62
|
private static filterCurrentParams;
|
|
54
|
-
|
|
63
|
+
requiresChanges(): boolean;
|
|
55
64
|
/**
|
|
56
65
|
* Convert the plan to a JSON response object
|
|
57
66
|
*/
|
package/dist/plan/plan.js
CHANGED
|
@@ -8,12 +8,20 @@ import { ChangeSet } from './change-set.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export class Plan {
|
|
10
10
|
id;
|
|
11
|
+
/**
|
|
12
|
+
* List of changes to make
|
|
13
|
+
*/
|
|
11
14
|
changeSet;
|
|
15
|
+
/**
|
|
16
|
+
* Ex: name, type, dependsOn etc. Metadata parameters
|
|
17
|
+
*/
|
|
12
18
|
coreParameters;
|
|
13
|
-
|
|
19
|
+
statefulMode;
|
|
20
|
+
constructor(id, changeSet, resourceMetadata, statefulMode) {
|
|
14
21
|
this.id = id;
|
|
15
22
|
this.changeSet = changeSet;
|
|
16
23
|
this.coreParameters = resourceMetadata;
|
|
24
|
+
this.statefulMode = statefulMode;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* The desired config that a plan will achieve after executing all the actions.
|
|
@@ -39,6 +47,43 @@ export class Plan {
|
|
|
39
47
|
...this.changeSet.currentParameters,
|
|
40
48
|
};
|
|
41
49
|
}
|
|
50
|
+
get resourceId() {
|
|
51
|
+
return this.coreParameters.name
|
|
52
|
+
? `${this.coreParameters.type}.${this.coreParameters.name}`
|
|
53
|
+
: this.coreParameters.type;
|
|
54
|
+
}
|
|
55
|
+
static calculate(params) {
|
|
56
|
+
const { desiredParameters, currentParametersArray, stateParameters, coreParameters, settings, statefulMode } = params;
|
|
57
|
+
const currentParameters = Plan.matchCurrentParameters({
|
|
58
|
+
desiredParameters,
|
|
59
|
+
currentParametersArray,
|
|
60
|
+
stateParameters,
|
|
61
|
+
settings,
|
|
62
|
+
statefulMode
|
|
63
|
+
});
|
|
64
|
+
const filteredCurrentParameters = Plan.filterCurrentParams({
|
|
65
|
+
desiredParameters,
|
|
66
|
+
currentParameters,
|
|
67
|
+
stateParameters,
|
|
68
|
+
settings,
|
|
69
|
+
statefulMode
|
|
70
|
+
});
|
|
71
|
+
// Empty
|
|
72
|
+
if (!filteredCurrentParameters && !desiredParameters) {
|
|
73
|
+
return new Plan(uuidV4(), ChangeSet.empty(), coreParameters, statefulMode);
|
|
74
|
+
}
|
|
75
|
+
// CREATE
|
|
76
|
+
if (!filteredCurrentParameters && desiredParameters) {
|
|
77
|
+
return new Plan(uuidV4(), ChangeSet.create(desiredParameters), coreParameters, statefulMode);
|
|
78
|
+
}
|
|
79
|
+
// DESTROY
|
|
80
|
+
if (filteredCurrentParameters && !desiredParameters) {
|
|
81
|
+
return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters, statefulMode);
|
|
82
|
+
}
|
|
83
|
+
// NO-OP, MODIFY or RE-CREATE
|
|
84
|
+
const changeSet = ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
|
|
85
|
+
return new Plan(uuidV4(), changeSet, coreParameters, statefulMode);
|
|
86
|
+
}
|
|
42
87
|
/**
|
|
43
88
|
* When multiples of the same resource are allowed, this matching function will match a given config with one of the
|
|
44
89
|
* existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
|
|
@@ -70,37 +115,59 @@ export class Plan {
|
|
|
70
115
|
getResourceType() {
|
|
71
116
|
return this.coreParameters.type;
|
|
72
117
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
currentParametersArray,
|
|
78
|
-
stateParameters,
|
|
79
|
-
settings,
|
|
80
|
-
statefulMode
|
|
81
|
-
});
|
|
82
|
-
const filteredCurrentParameters = Plan.filterCurrentParams({
|
|
83
|
-
desiredParameters,
|
|
84
|
-
currentParameters,
|
|
85
|
-
stateParameters,
|
|
86
|
-
settings,
|
|
87
|
-
statefulMode
|
|
88
|
-
});
|
|
89
|
-
// Empty
|
|
90
|
-
if (!filteredCurrentParameters && !desiredParameters) {
|
|
91
|
-
return new Plan(uuidV4(), ChangeSet.empty(), coreParameters);
|
|
92
|
-
}
|
|
93
|
-
// CREATE
|
|
94
|
-
if (!filteredCurrentParameters && desiredParameters) {
|
|
95
|
-
return new Plan(uuidV4(), ChangeSet.create(desiredParameters), coreParameters);
|
|
118
|
+
// 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted
|
|
119
|
+
static fromResponse(data, defaultValues) {
|
|
120
|
+
if (!data) {
|
|
121
|
+
throw new Error('Data is empty');
|
|
96
122
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
addDefaultValues();
|
|
124
|
+
return new Plan(uuidV4(), new ChangeSet(data.operation, data.parameters), {
|
|
125
|
+
type: data.resourceType,
|
|
126
|
+
name: data.resourceName,
|
|
127
|
+
}, data.statefulMode);
|
|
128
|
+
function addDefaultValues() {
|
|
129
|
+
Object.entries(defaultValues ?? {})
|
|
130
|
+
.forEach(([key, defaultValue]) => {
|
|
131
|
+
const configValueExists = data
|
|
132
|
+
.parameters
|
|
133
|
+
.some((p) => p.name === key);
|
|
134
|
+
// Only set default values if the value does not exist in the config
|
|
135
|
+
if (configValueExists) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
switch (data.operation) {
|
|
139
|
+
case ResourceOperation.CREATE: {
|
|
140
|
+
data.parameters.push({
|
|
141
|
+
name: key,
|
|
142
|
+
operation: ParameterOperation.ADD,
|
|
143
|
+
previousValue: null,
|
|
144
|
+
newValue: defaultValue,
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case ResourceOperation.DESTROY: {
|
|
149
|
+
data.parameters.push({
|
|
150
|
+
name: key,
|
|
151
|
+
operation: ParameterOperation.REMOVE,
|
|
152
|
+
previousValue: defaultValue,
|
|
153
|
+
newValue: null,
|
|
154
|
+
});
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case ResourceOperation.MODIFY:
|
|
158
|
+
case ResourceOperation.RECREATE:
|
|
159
|
+
case ResourceOperation.NOOP: {
|
|
160
|
+
data.parameters.push({
|
|
161
|
+
name: key,
|
|
162
|
+
operation: ParameterOperation.NOOP,
|
|
163
|
+
previousValue: defaultValue,
|
|
164
|
+
newValue: defaultValue,
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
100
170
|
}
|
|
101
|
-
// NO-OP, MODIFY or RE-CREATE
|
|
102
|
-
const changeSet = ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
|
|
103
|
-
return new Plan(uuidV4(), changeSet, coreParameters);
|
|
104
171
|
}
|
|
105
172
|
/**
|
|
106
173
|
* Only keep relevant params for the plan. We don't want to change settings that were not already
|
|
@@ -199,59 +266,8 @@ export class Plan {
|
|
|
199
266
|
}
|
|
200
267
|
// TODO: This needs to be revisited. I don't think this is valid anymore.
|
|
201
268
|
// 1. For all scenarios, there shouldn't be an apply without a plan beforehand
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (!data) {
|
|
205
|
-
throw new Error('Data is empty');
|
|
206
|
-
}
|
|
207
|
-
addDefaultValues();
|
|
208
|
-
return new Plan(uuidV4(), new ChangeSet(data.operation, data.parameters), {
|
|
209
|
-
type: data.resourceType,
|
|
210
|
-
name: data.resourceName,
|
|
211
|
-
});
|
|
212
|
-
function addDefaultValues() {
|
|
213
|
-
Object.entries(defaultValues ?? {})
|
|
214
|
-
.forEach(([key, defaultValue]) => {
|
|
215
|
-
const configValueExists = data
|
|
216
|
-
.parameters
|
|
217
|
-
.some((p) => p.name === key);
|
|
218
|
-
// Only set default values if the value does not exist in the config
|
|
219
|
-
if (configValueExists) {
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
switch (data.operation) {
|
|
223
|
-
case ResourceOperation.CREATE: {
|
|
224
|
-
data.parameters.push({
|
|
225
|
-
name: key,
|
|
226
|
-
operation: ParameterOperation.ADD,
|
|
227
|
-
previousValue: null,
|
|
228
|
-
newValue: defaultValue,
|
|
229
|
-
});
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
case ResourceOperation.DESTROY: {
|
|
233
|
-
data.parameters.push({
|
|
234
|
-
name: key,
|
|
235
|
-
operation: ParameterOperation.REMOVE,
|
|
236
|
-
previousValue: defaultValue,
|
|
237
|
-
newValue: null,
|
|
238
|
-
});
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
case ResourceOperation.MODIFY:
|
|
242
|
-
case ResourceOperation.RECREATE:
|
|
243
|
-
case ResourceOperation.NOOP: {
|
|
244
|
-
data.parameters.push({
|
|
245
|
-
name: key,
|
|
246
|
-
operation: ParameterOperation.NOOP,
|
|
247
|
-
previousValue: defaultValue,
|
|
248
|
-
newValue: defaultValue,
|
|
249
|
-
});
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
}
|
|
269
|
+
requiresChanges() {
|
|
270
|
+
return this.changeSet.operation !== ResourceOperation.NOOP;
|
|
255
271
|
}
|
|
256
272
|
/**
|
|
257
273
|
* Convert the plan to a JSON response object
|
package/dist/plugin/plugin.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ApplyRequestData, GetResourceInfoRequestData, GetResourceInfoResponseData, ImportRequestData, ImportResponseData, InitializeResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
|
|
2
2
|
import { Plan } from '../plan/plan.js';
|
|
3
|
+
import { BackgroundPty } from '../pty/background-pty.js';
|
|
3
4
|
import { Resource } from '../resource/resource.js';
|
|
4
5
|
import { ResourceController } from '../resource/resource-controller.js';
|
|
5
|
-
import { BackgroundPty } from '../pty/background-pty.js';
|
|
6
6
|
export declare class Plugin {
|
|
7
7
|
name: string;
|
|
8
8
|
resourceControllers: Map<string, ResourceController<ResourceConfig>>;
|
|
@@ -16,6 +16,7 @@ export declare class Plugin {
|
|
|
16
16
|
validate(data: ValidateRequestData): Promise<ValidateResponseData>;
|
|
17
17
|
plan(data: PlanRequestData): Promise<PlanResponseData>;
|
|
18
18
|
apply(data: ApplyRequestData): Promise<void>;
|
|
19
|
+
kill(): Promise<void>;
|
|
19
20
|
private resolvePlan;
|
|
20
21
|
protected crossValidateResources(configs: ResourceConfig[]): Promise<void>;
|
|
21
22
|
}
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ApplyValidationError } from '../common/errors.js';
|
|
1
2
|
import { Plan } from '../plan/plan.js';
|
|
3
|
+
import { BackgroundPty } from '../pty/background-pty.js';
|
|
4
|
+
import { getPty } from '../pty/index.js';
|
|
2
5
|
import { ResourceController } from '../resource/resource-controller.js';
|
|
3
6
|
import { ptyLocalStorage } from '../utils/pty-local-storage.js';
|
|
4
|
-
import { BackgroundPty } from '../pty/background-pty.js';
|
|
5
7
|
export class Plugin {
|
|
6
8
|
name;
|
|
7
9
|
resourceControllers;
|
|
@@ -82,9 +84,7 @@ export class Plugin {
|
|
|
82
84
|
if (!type || !this.resourceControllers.has(type)) {
|
|
83
85
|
throw new Error(`Resource type not found: ${type}`);
|
|
84
86
|
}
|
|
85
|
-
const plan = await ptyLocalStorage.run(this.planPty, async () =>
|
|
86
|
-
return this.resourceControllers.get(type).plan(data.desired ?? null, data.state ?? null, data.isStateful);
|
|
87
|
-
});
|
|
87
|
+
const plan = await ptyLocalStorage.run(this.planPty, async () => this.resourceControllers.get(type).plan(data.desired ?? null, data.state ?? null, data.isStateful));
|
|
88
88
|
this.planStorage.set(plan.id, plan);
|
|
89
89
|
return plan.toResponse();
|
|
90
90
|
}
|
|
@@ -98,6 +98,17 @@ export class Plugin {
|
|
|
98
98
|
throw new Error('Malformed plan with resource that cannot be found');
|
|
99
99
|
}
|
|
100
100
|
await resource.apply(plan);
|
|
101
|
+
const validationPlan = await ptyLocalStorage.run(new BackgroundPty(), async () => {
|
|
102
|
+
const result = await resource.plan(plan.desiredConfig, plan.currentConfig, plan.statefulMode);
|
|
103
|
+
await getPty().kill();
|
|
104
|
+
return result;
|
|
105
|
+
});
|
|
106
|
+
if (validationPlan.requiresChanges()) {
|
|
107
|
+
throw new ApplyValidationError(plan);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async kill() {
|
|
111
|
+
await this.planPty.kill();
|
|
101
112
|
}
|
|
102
113
|
resolvePlan(data) {
|
|
103
114
|
const { plan: planRequest, planId } = data;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.116",
|
|
4
4
|
"description": "Library plugin library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"ajv": "^8.12.0",
|
|
16
16
|
"ajv-formats": "^2.1.1",
|
|
17
|
-
"codify-schemas": "1.0.
|
|
17
|
+
"codify-schemas": "1.0.54",
|
|
18
18
|
"@npmcli/promise-spawn": "^7.0.1",
|
|
19
19
|
"uuid": "^10.0.0",
|
|
20
20
|
"lodash.isequal": "^4.5.0",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ApplyValidationError } from './errors.js';
|
|
3
|
+
import { testPlan } from '../utils/test-utils.test.js';
|
|
4
|
+
|
|
5
|
+
describe('Test file for errors file', () => {
|
|
6
|
+
it('Can properly format ApplyValidationError', () => {
|
|
7
|
+
const plan = testPlan({
|
|
8
|
+
desired: null,
|
|
9
|
+
current: [{ propZ: ['a', 'b', 'c'] }],
|
|
10
|
+
state: { propZ: ['a', 'b', 'c'] },
|
|
11
|
+
core: {
|
|
12
|
+
type: 'homebrew',
|
|
13
|
+
name: 'first'
|
|
14
|
+
},
|
|
15
|
+
statefulMode: true,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
throw new ApplyValidationError(plan);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error(e);
|
|
22
|
+
expect(e.message).toMatch(
|
|
23
|
+
`Failed to apply changes to resource: "homebrew.first". Additional changes are needed to complete apply.
|
|
24
|
+
Changes remaining:
|
|
25
|
+
{
|
|
26
|
+
"operation": "destroy",
|
|
27
|
+
"parameters": [
|
|
28
|
+
{
|
|
29
|
+
"name": "propZ",
|
|
30
|
+
"operation": "remove",
|
|
31
|
+
"currentValue": [
|
|
32
|
+
"a",
|
|
33
|
+
"b",
|
|
34
|
+
"c"
|
|
35
|
+
],
|
|
36
|
+
"desiredValue": null
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Plan } from '../plan/plan.js';
|
|
2
|
+
|
|
3
|
+
export class ApplyValidationError extends Error {
|
|
4
|
+
resourceType: string;
|
|
5
|
+
resourceName?: string;
|
|
6
|
+
plan: Plan<any>;
|
|
7
|
+
|
|
8
|
+
constructor(plan: Plan<any>) {
|
|
9
|
+
super(`Failed to apply changes to resource: "${plan.resourceId}". Additional changes are needed to complete apply.\nChanges remaining:\n${ApplyValidationError.prettyPrintPlan(plan)}`);
|
|
10
|
+
|
|
11
|
+
this.resourceType = plan.coreParameters.type;
|
|
12
|
+
this.resourceName = plan.coreParameters.name;
|
|
13
|
+
this.plan = plan;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private static prettyPrintPlan(plan: Plan<any>): string {
|
|
17
|
+
const { operation, parameters } = plan.toResponse();
|
|
18
|
+
|
|
19
|
+
const prettyParameters = parameters.map(({ name, operation, previousValue, newValue }) => ({
|
|
20
|
+
name,
|
|
21
|
+
operation,
|
|
22
|
+
currentValue: previousValue,
|
|
23
|
+
desiredValue: newValue,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
operation,
|
|
28
|
+
parameters: prettyParameters,
|
|
29
|
+
}, null, 2);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,4 +16,8 @@ export * from './utils/utils.js'
|
|
|
16
16
|
export async function runPlugin(plugin: Plugin) {
|
|
17
17
|
const messageHandler = new MessageHandler(plugin);
|
|
18
18
|
process.on('message', (message) => messageHandler.onMessage(message))
|
|
19
|
+
|
|
20
|
+
process.on('beforeExit', () => {
|
|
21
|
+
plugin.kill();
|
|
22
|
+
})
|
|
19
23
|
}
|
package/src/plan/plan.test.ts
CHANGED
|
@@ -19,7 +19,8 @@ describe('Plan entity tests', () => {
|
|
|
19
19
|
operation: ParameterOperation.ADD,
|
|
20
20
|
previousValue: null,
|
|
21
21
|
newValue: 'propBValue'
|
|
22
|
-
}]
|
|
22
|
+
}],
|
|
23
|
+
statefulMode: false,
|
|
23
24
|
}, controller.parsedSettings.defaultValues);
|
|
24
25
|
|
|
25
26
|
expect(plan.currentConfig).to.be.null;
|
|
@@ -47,7 +48,8 @@ describe('Plan entity tests', () => {
|
|
|
47
48
|
operation: ParameterOperation.REMOVE,
|
|
48
49
|
previousValue: 'propBValue',
|
|
49
50
|
newValue: null,
|
|
50
|
-
}]
|
|
51
|
+
}],
|
|
52
|
+
statefulMode: false,
|
|
51
53
|
}, controller.parsedSettings.defaultValues);
|
|
52
54
|
|
|
53
55
|
expect(plan.currentConfig).toMatchObject({
|
|
@@ -75,7 +77,8 @@ describe('Plan entity tests', () => {
|
|
|
75
77
|
operation: ParameterOperation.NOOP,
|
|
76
78
|
previousValue: 'propBValue',
|
|
77
79
|
newValue: 'propBValue',
|
|
78
|
-
}]
|
|
80
|
+
}],
|
|
81
|
+
statefulMode: false,
|
|
79
82
|
}, controller.parsedSettings.defaultValues);
|
|
80
83
|
|
|
81
84
|
expect(plan.currentConfig).toMatchObject({
|
|
@@ -112,7 +115,8 @@ describe('Plan entity tests', () => {
|
|
|
112
115
|
operation: ParameterOperation.ADD,
|
|
113
116
|
previousValue: null,
|
|
114
117
|
newValue: 'propAValue',
|
|
115
|
-
}]
|
|
118
|
+
}],
|
|
119
|
+
statefulMode: false,
|
|
116
120
|
}, controller.parsedSettings.defaultValues);
|
|
117
121
|
|
|
118
122
|
expect(plan.currentConfig).to.be.null
|
package/src/plan/plan.ts
CHANGED
|
@@ -23,13 +23,24 @@ import { ChangeSet } from './change-set.js';
|
|
|
23
23
|
*/
|
|
24
24
|
export class Plan<T extends StringIndexedObject> {
|
|
25
25
|
id: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* List of changes to make
|
|
29
|
+
*/
|
|
26
30
|
changeSet: ChangeSet<T>;
|
|
27
|
-
coreParameters: ResourceConfig
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Ex: name, type, dependsOn etc. Metadata parameters
|
|
34
|
+
*/
|
|
35
|
+
coreParameters: ResourceConfig;
|
|
36
|
+
|
|
37
|
+
statefulMode: boolean;
|
|
38
|
+
|
|
39
|
+
constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig, statefulMode: boolean) {
|
|
30
40
|
this.id = id;
|
|
31
41
|
this.changeSet = changeSet;
|
|
32
42
|
this.coreParameters = resourceMetadata;
|
|
43
|
+
this.statefulMode = statefulMode;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
/**
|
|
@@ -60,53 +71,10 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
* @param params
|
|
69
|
-
* @private
|
|
70
|
-
*/
|
|
71
|
-
private static matchCurrentParameters<T extends StringIndexedObject>(params: {
|
|
72
|
-
desiredParameters: Partial<T> | null,
|
|
73
|
-
currentParametersArray: Partial<T>[] | null,
|
|
74
|
-
stateParameters: Partial<T> | null,
|
|
75
|
-
settings: ResourceSettings<T>,
|
|
76
|
-
statefulMode: boolean,
|
|
77
|
-
}): Partial<T> | null {
|
|
78
|
-
const {
|
|
79
|
-
desiredParameters,
|
|
80
|
-
currentParametersArray,
|
|
81
|
-
stateParameters,
|
|
82
|
-
settings,
|
|
83
|
-
statefulMode
|
|
84
|
-
} = params;
|
|
85
|
-
|
|
86
|
-
if (!settings.allowMultiple) {
|
|
87
|
-
return currentParametersArray?.[0] ?? null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!currentParametersArray) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (statefulMode) {
|
|
95
|
-
return stateParameters
|
|
96
|
-
? settings.allowMultiple.matcher(stateParameters, currentParametersArray)
|
|
97
|
-
: null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return settings.allowMultiple.matcher(desiredParameters!, currentParametersArray);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* The type (id) of the resource
|
|
105
|
-
*
|
|
106
|
-
* @return string
|
|
107
|
-
*/
|
|
108
|
-
getResourceType(): string {
|
|
109
|
-
return this.coreParameters.type
|
|
74
|
+
get resourceId(): string {
|
|
75
|
+
return this.coreParameters.name
|
|
76
|
+
? `${this.coreParameters.type}.${this.coreParameters.name}`
|
|
77
|
+
: this.coreParameters.type;
|
|
110
78
|
}
|
|
111
79
|
|
|
112
80
|
static calculate<T extends StringIndexedObject>(params: {
|
|
@@ -148,6 +116,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
148
116
|
uuidV4(),
|
|
149
117
|
ChangeSet.empty<T>(),
|
|
150
118
|
coreParameters,
|
|
119
|
+
statefulMode,
|
|
151
120
|
)
|
|
152
121
|
}
|
|
153
122
|
|
|
@@ -156,7 +125,8 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
156
125
|
return new Plan(
|
|
157
126
|
uuidV4(),
|
|
158
127
|
ChangeSet.create(desiredParameters),
|
|
159
|
-
coreParameters
|
|
128
|
+
coreParameters,
|
|
129
|
+
statefulMode,
|
|
160
130
|
)
|
|
161
131
|
}
|
|
162
132
|
|
|
@@ -165,7 +135,8 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
165
135
|
return new Plan(
|
|
166
136
|
uuidV4(),
|
|
167
137
|
ChangeSet.destroy(filteredCurrentParameters),
|
|
168
|
-
coreParameters
|
|
138
|
+
coreParameters,
|
|
139
|
+
statefulMode,
|
|
169
140
|
)
|
|
170
141
|
}
|
|
171
142
|
|
|
@@ -180,7 +151,128 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
180
151
|
uuidV4(),
|
|
181
152
|
changeSet,
|
|
182
153
|
coreParameters,
|
|
154
|
+
statefulMode,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* When multiples of the same resource are allowed, this matching function will match a given config with one of the
|
|
160
|
+
* existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
|
|
161
|
+
* the application name and location to match it to our desired configs name and location.
|
|
162
|
+
*
|
|
163
|
+
* @param params
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
private static matchCurrentParameters<T extends StringIndexedObject>(params: {
|
|
167
|
+
desiredParameters: Partial<T> | null,
|
|
168
|
+
currentParametersArray: Partial<T>[] | null,
|
|
169
|
+
stateParameters: Partial<T> | null,
|
|
170
|
+
settings: ResourceSettings<T>,
|
|
171
|
+
statefulMode: boolean,
|
|
172
|
+
}): Partial<T> | null {
|
|
173
|
+
const {
|
|
174
|
+
desiredParameters,
|
|
175
|
+
currentParametersArray,
|
|
176
|
+
stateParameters,
|
|
177
|
+
settings,
|
|
178
|
+
statefulMode
|
|
179
|
+
} = params;
|
|
180
|
+
|
|
181
|
+
if (!settings.allowMultiple) {
|
|
182
|
+
return currentParametersArray?.[0] ?? null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!currentParametersArray) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (statefulMode) {
|
|
190
|
+
return stateParameters
|
|
191
|
+
? settings.allowMultiple.matcher(stateParameters, currentParametersArray)
|
|
192
|
+
: null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return settings.allowMultiple.matcher(desiredParameters!, currentParametersArray);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The type (id) of the resource
|
|
200
|
+
*
|
|
201
|
+
* @return string
|
|
202
|
+
*/
|
|
203
|
+
getResourceType(): string {
|
|
204
|
+
return this.coreParameters.type
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted
|
|
208
|
+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T> {
|
|
209
|
+
if (!data) {
|
|
210
|
+
throw new Error('Data is empty');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
addDefaultValues();
|
|
214
|
+
|
|
215
|
+
return new Plan(
|
|
216
|
+
uuidV4(),
|
|
217
|
+
new ChangeSet<T>(
|
|
218
|
+
data.operation,
|
|
219
|
+
data.parameters
|
|
220
|
+
),
|
|
221
|
+
{
|
|
222
|
+
type: data.resourceType,
|
|
223
|
+
name: data.resourceName,
|
|
224
|
+
},
|
|
225
|
+
data.statefulMode
|
|
183
226
|
);
|
|
227
|
+
|
|
228
|
+
function addDefaultValues(): void {
|
|
229
|
+
Object.entries(defaultValues ?? {})
|
|
230
|
+
.forEach(([key, defaultValue]) => {
|
|
231
|
+
const configValueExists = data!
|
|
232
|
+
.parameters
|
|
233
|
+
.some((p) => p.name === key);
|
|
234
|
+
|
|
235
|
+
// Only set default values if the value does not exist in the config
|
|
236
|
+
if (configValueExists) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
switch (data!.operation) {
|
|
241
|
+
case ResourceOperation.CREATE: {
|
|
242
|
+
data!.parameters.push({
|
|
243
|
+
name: key,
|
|
244
|
+
operation: ParameterOperation.ADD,
|
|
245
|
+
previousValue: null,
|
|
246
|
+
newValue: defaultValue,
|
|
247
|
+
});
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case ResourceOperation.DESTROY: {
|
|
252
|
+
data!.parameters.push({
|
|
253
|
+
name: key,
|
|
254
|
+
operation: ParameterOperation.REMOVE,
|
|
255
|
+
previousValue: defaultValue,
|
|
256
|
+
newValue: null,
|
|
257
|
+
});
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
case ResourceOperation.MODIFY:
|
|
262
|
+
case ResourceOperation.RECREATE:
|
|
263
|
+
case ResourceOperation.NOOP: {
|
|
264
|
+
data!.parameters.push({
|
|
265
|
+
name: key,
|
|
266
|
+
operation: ParameterOperation.NOOP,
|
|
267
|
+
previousValue: defaultValue,
|
|
268
|
+
newValue: defaultValue,
|
|
269
|
+
});
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
184
276
|
}
|
|
185
277
|
|
|
186
278
|
/**
|
|
@@ -322,74 +414,9 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
322
414
|
|
|
323
415
|
// TODO: This needs to be revisited. I don't think this is valid anymore.
|
|
324
416
|
// 1. For all scenarios, there shouldn't be an apply without a plan beforehand
|
|
325
|
-
// 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted
|
|
326
|
-
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T> {
|
|
327
|
-
if (!data) {
|
|
328
|
-
throw new Error('Data is empty');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
addDefaultValues();
|
|
332
|
-
|
|
333
|
-
return new Plan(
|
|
334
|
-
uuidV4(),
|
|
335
|
-
new ChangeSet<T>(
|
|
336
|
-
data.operation,
|
|
337
|
-
data.parameters
|
|
338
|
-
),
|
|
339
|
-
{
|
|
340
|
-
type: data.resourceType,
|
|
341
|
-
name: data.resourceName,
|
|
342
|
-
},
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
function addDefaultValues(): void {
|
|
346
|
-
Object.entries(defaultValues ?? {})
|
|
347
|
-
.forEach(([key, defaultValue]) => {
|
|
348
|
-
const configValueExists = data!
|
|
349
|
-
.parameters
|
|
350
|
-
.some((p) => p.name === key);
|
|
351
|
-
|
|
352
|
-
// Only set default values if the value does not exist in the config
|
|
353
|
-
if (configValueExists) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
switch (data!.operation) {
|
|
358
|
-
case ResourceOperation.CREATE: {
|
|
359
|
-
data!.parameters.push({
|
|
360
|
-
name: key,
|
|
361
|
-
operation: ParameterOperation.ADD,
|
|
362
|
-
previousValue: null,
|
|
363
|
-
newValue: defaultValue,
|
|
364
|
-
});
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
case ResourceOperation.DESTROY: {
|
|
369
|
-
data!.parameters.push({
|
|
370
|
-
name: key,
|
|
371
|
-
operation: ParameterOperation.REMOVE,
|
|
372
|
-
previousValue: defaultValue,
|
|
373
|
-
newValue: null,
|
|
374
|
-
});
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
case ResourceOperation.MODIFY:
|
|
379
|
-
case ResourceOperation.RECREATE:
|
|
380
|
-
case ResourceOperation.NOOP: {
|
|
381
|
-
data!.parameters.push({
|
|
382
|
-
name: key,
|
|
383
|
-
operation: ParameterOperation.NOOP,
|
|
384
|
-
previousValue: defaultValue,
|
|
385
|
-
newValue: defaultValue,
|
|
386
|
-
});
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
417
|
|
|
418
|
+
requiresChanges(): boolean {
|
|
419
|
+
return this.changeSet.operation !== ResourceOperation.NOOP;
|
|
393
420
|
}
|
|
394
421
|
|
|
395
422
|
/**
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { Plugin } from './plugin.js';
|
|
3
|
-
import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
3
|
+
import { ApplyRequestData, ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
4
4
|
import { Resource } from '../resource/resource.js';
|
|
5
5
|
import { Plan } from '../plan/plan.js';
|
|
6
6
|
import { spy } from 'sinon';
|
|
7
7
|
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
8
|
+
import { TestConfig } from '../utils/test-utils.test.js';
|
|
9
|
+
import { ApplyValidationError } from '../common/errors.js';
|
|
10
|
+
import { getPty } from '../pty/index.js';
|
|
8
11
|
|
|
9
12
|
interface TestConfig extends StringIndexedObject {
|
|
10
13
|
propA: string;
|
|
@@ -38,15 +41,22 @@ class TestResource extends Resource<TestConfig> {
|
|
|
38
41
|
|
|
39
42
|
describe('Plugin tests', () => {
|
|
40
43
|
it('Can apply resource', async () => {
|
|
41
|
-
const resource= spy(new TestResource
|
|
44
|
+
const resource = spy(new class extends TestResource {
|
|
45
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
46
|
+
return {
|
|
47
|
+
propA: 'abc',
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
})
|
|
42
51
|
const plugin = Plugin.create('testPlugin', [resource as any])
|
|
43
52
|
|
|
44
|
-
const plan = {
|
|
53
|
+
const plan: ApplyRequestData['plan'] = {
|
|
45
54
|
operation: ResourceOperation.CREATE,
|
|
46
55
|
resourceType: 'testResource',
|
|
47
56
|
parameters: [
|
|
48
57
|
{ name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null },
|
|
49
|
-
]
|
|
58
|
+
],
|
|
59
|
+
statefulMode: false,
|
|
50
60
|
};
|
|
51
61
|
|
|
52
62
|
await plugin.apply({ plan });
|
|
@@ -54,15 +64,20 @@ describe('Plugin tests', () => {
|
|
|
54
64
|
});
|
|
55
65
|
|
|
56
66
|
it('Can destroy resource', async () => {
|
|
57
|
-
const resource = spy(new TestResource
|
|
67
|
+
const resource = spy(new class extends TestResource {
|
|
68
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
58
72
|
const testPlugin = Plugin.create('testPlugin', [resource as any])
|
|
59
73
|
|
|
60
|
-
const plan = {
|
|
74
|
+
const plan: ApplyRequestData['plan'] = {
|
|
61
75
|
operation: ResourceOperation.DESTROY,
|
|
62
76
|
resourceType: 'testResource',
|
|
63
77
|
parameters: [
|
|
64
78
|
{ name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' },
|
|
65
|
-
]
|
|
79
|
+
],
|
|
80
|
+
statefulMode: true,
|
|
66
81
|
};
|
|
67
82
|
|
|
68
83
|
await testPlugin.apply({ plan })
|
|
@@ -70,15 +85,22 @@ describe('Plugin tests', () => {
|
|
|
70
85
|
});
|
|
71
86
|
|
|
72
87
|
it('Can re-create resource', async () => {
|
|
73
|
-
const resource = spy(new TestResource
|
|
88
|
+
const resource = spy(new class extends TestResource {
|
|
89
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
90
|
+
return {
|
|
91
|
+
propA: 'def',
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
})
|
|
74
95
|
const testPlugin = Plugin.create('testPlugin', [resource as any])
|
|
75
96
|
|
|
76
|
-
const plan = {
|
|
97
|
+
const plan: ApplyRequestData['plan'] = {
|
|
77
98
|
operation: ResourceOperation.RECREATE,
|
|
78
99
|
resourceType: 'testResource',
|
|
79
100
|
parameters: [
|
|
80
101
|
{ name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
|
|
81
|
-
]
|
|
102
|
+
],
|
|
103
|
+
statefulMode: false,
|
|
82
104
|
};
|
|
83
105
|
|
|
84
106
|
await testPlugin.apply({ plan })
|
|
@@ -87,15 +109,22 @@ describe('Plugin tests', () => {
|
|
|
87
109
|
});
|
|
88
110
|
|
|
89
111
|
it('Can modify resource', async () => {
|
|
90
|
-
const resource = spy(new TestResource
|
|
112
|
+
const resource = spy(new class extends TestResource {
|
|
113
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
114
|
+
return {
|
|
115
|
+
propA: 'def',
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|
|
91
119
|
const testPlugin = Plugin.create('testPlugin', [resource as any])
|
|
92
120
|
|
|
93
|
-
const plan = {
|
|
121
|
+
const plan: ApplyRequestData['plan'] = {
|
|
94
122
|
operation: ResourceOperation.MODIFY,
|
|
95
123
|
resourceType: 'testResource',
|
|
96
124
|
parameters: [
|
|
97
125
|
{ name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
|
|
98
|
-
]
|
|
126
|
+
],
|
|
127
|
+
statefulMode: false,
|
|
99
128
|
};
|
|
100
129
|
|
|
101
130
|
await testPlugin.apply({ plan })
|
|
@@ -178,4 +207,78 @@ describe('Plugin tests', () => {
|
|
|
178
207
|
requiredParameters: []
|
|
179
208
|
})
|
|
180
209
|
})
|
|
210
|
+
|
|
211
|
+
it('Fails an apply if the validation fails', async () => {
|
|
212
|
+
const resource = spy(new class extends TestResource {
|
|
213
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
214
|
+
return {
|
|
215
|
+
propA: 'abc',
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
const testPlugin = Plugin.create('testPlugin', [resource as any])
|
|
220
|
+
|
|
221
|
+
const plan: ApplyRequestData['plan'] = {
|
|
222
|
+
operation: ResourceOperation.MODIFY,
|
|
223
|
+
resourceType: 'testResource',
|
|
224
|
+
parameters: [
|
|
225
|
+
{ name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
|
|
226
|
+
],
|
|
227
|
+
statefulMode: false,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
await expect(() => testPlugin.apply({ plan }))
|
|
231
|
+
.rejects
|
|
232
|
+
.toThrowError(new ApplyValidationError(Plan.fromResponse(plan)));
|
|
233
|
+
expect(resource.modify.calledOnce).to.be.true;
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('Allows the usage of pty in refresh (plan)', async () => {
|
|
237
|
+
const resource = spy(new class extends TestResource {
|
|
238
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
239
|
+
expect(getPty()).to.not.be.undefined;
|
|
240
|
+
expect(getPty()).to.not.be.null;
|
|
241
|
+
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const testPlugin = Plugin.create('testPlugin', [resource as any]);
|
|
247
|
+
await testPlugin.plan({
|
|
248
|
+
desired: {
|
|
249
|
+
type: 'testResource'
|
|
250
|
+
},
|
|
251
|
+
state: undefined,
|
|
252
|
+
isStateful: false,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
expect(resource.refresh.calledOnce).to.be.true;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('Allows the usage of pty in validation refresh (apply)', async () => {
|
|
259
|
+
const resource = spy(new class extends TestResource {
|
|
260
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
261
|
+
expect(getPty()).to.not.be.undefined;
|
|
262
|
+
expect(getPty()).to.not.be.null;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
propA: 'abc'
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const testPlugin = Plugin.create('testPlugin', [resource as any]);
|
|
271
|
+
|
|
272
|
+
const plan: ApplyRequestData['plan'] = {
|
|
273
|
+
operation: ResourceOperation.CREATE,
|
|
274
|
+
resourceType: 'testResource',
|
|
275
|
+
parameters: [
|
|
276
|
+
{ name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null },
|
|
277
|
+
],
|
|
278
|
+
statefulMode: false,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
await testPlugin.apply({ plan })
|
|
282
|
+
expect(resource.refresh.calledOnce).to.be.true;
|
|
283
|
+
})
|
|
181
284
|
});
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -13,11 +13,13 @@ import {
|
|
|
13
13
|
ValidateResponseData
|
|
14
14
|
} from 'codify-schemas';
|
|
15
15
|
|
|
16
|
+
import { ApplyValidationError } from '../common/errors.js';
|
|
16
17
|
import { Plan } from '../plan/plan.js';
|
|
18
|
+
import { BackgroundPty } from '../pty/background-pty.js';
|
|
19
|
+
import { getPty } from '../pty/index.js';
|
|
17
20
|
import { Resource } from '../resource/resource.js';
|
|
18
21
|
import { ResourceController } from '../resource/resource-controller.js';
|
|
19
22
|
import { ptyLocalStorage } from '../utils/pty-local-storage.js';
|
|
20
|
-
import { BackgroundPty } from '../pty/background-pty.js';
|
|
21
23
|
|
|
22
24
|
export class Plugin {
|
|
23
25
|
planStorage: Map<string, Plan<any>>;
|
|
@@ -122,13 +124,11 @@ export class Plugin {
|
|
|
122
124
|
throw new Error(`Resource type not found: ${type}`);
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
const plan = await ptyLocalStorage.run(this.planPty, async () =>
|
|
126
|
-
return this.resourceControllers.get(type)!.plan(
|
|
127
|
+
const plan = await ptyLocalStorage.run(this.planPty, async () => this.resourceControllers.get(type)!.plan(
|
|
127
128
|
data.desired ?? null,
|
|
128
129
|
data.state ?? null,
|
|
129
130
|
data.isStateful
|
|
130
|
-
|
|
131
|
-
})
|
|
131
|
+
))
|
|
132
132
|
|
|
133
133
|
this.planStorage.set(plan.id, plan);
|
|
134
134
|
|
|
@@ -148,6 +148,25 @@ export class Plugin {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
await resource.apply(plan);
|
|
151
|
+
|
|
152
|
+
const validationPlan = await ptyLocalStorage.run(new BackgroundPty(), async () => {
|
|
153
|
+
const result = await resource.plan(
|
|
154
|
+
plan.desiredConfig,
|
|
155
|
+
plan.currentConfig,
|
|
156
|
+
plan.statefulMode
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await getPty().kill();
|
|
160
|
+
return result;
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
if (validationPlan.requiresChanges()) {
|
|
164
|
+
throw new ApplyValidationError(plan);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async kill() {
|
|
169
|
+
await this.planPty.kill();
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
private resolvePlan(data: ApplyRequestData): Plan<ResourceConfig> {
|
|
@@ -170,5 +189,4 @@ export class Plugin {
|
|
|
170
189
|
}
|
|
171
190
|
|
|
172
191
|
protected async crossValidateResources(configs: ResourceConfig[]): Promise<void> {}
|
|
173
|
-
|
|
174
192
|
}
|
|
@@ -499,7 +499,8 @@ describe('Resource parameter tests', () => {
|
|
|
499
499
|
{ name: 'propA', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
|
|
500
500
|
{ name: 'propB', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
|
|
501
501
|
{ name: 'propC', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
|
|
502
|
-
]
|
|
502
|
+
],
|
|
503
|
+
statefulMode: false,
|
|
503
504
|
}, {}) as any
|
|
504
505
|
);
|
|
505
506
|
|
|
@@ -521,7 +522,8 @@ describe('Resource parameter tests', () => {
|
|
|
521
522
|
{ name: 'propA', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
|
|
522
523
|
{ name: 'propB', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
|
|
523
524
|
{ name: 'propC', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
|
|
524
|
-
]
|
|
525
|
+
],
|
|
526
|
+
statefulMode: false,
|
|
525
527
|
}, {}) as any
|
|
526
528
|
);
|
|
527
529
|
|
|
@@ -539,7 +541,8 @@ describe('Resource parameter tests', () => {
|
|
|
539
541
|
{ name: 'propA', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
|
|
540
542
|
{ name: 'propB', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
|
|
541
543
|
{ name: 'propC', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
|
|
542
|
-
]
|
|
544
|
+
],
|
|
545
|
+
statefulMode: false,
|
|
543
546
|
}, {}) as any
|
|
544
547
|
);
|
|
545
548
|
|