codify-plugin-lib 1.0.114 → 1.0.115

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.
@@ -0,0 +1,8 @@
1
+ import { Plan } from '../plan/plan.js';
2
+ export declare class ApplyValidationError extends Error {
3
+ resourceType: string;
4
+ resourceName?: string;
5
+ plan: Plan<any>;
6
+ constructor(plan: Plan<any>);
7
+ private static prettyPrintPlan;
8
+ }
@@ -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
+ }
@@ -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
- constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
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,8 @@ 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
+ requiresChanges(): boolean;
22
31
  /**
23
32
  * When multiples of the same resource are allowed, this matching function will match a given config with one of the
24
33
  * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
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
- constructor(id, changeSet, resourceMetadata) {
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,14 @@ 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
+ requiresChanges() {
56
+ return this.changeSet.operation !== ResourceOperation.NOOP;
57
+ }
42
58
  /**
43
59
  * When multiples of the same resource are allowed, this matching function will match a given config with one of the
44
60
  * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
@@ -88,19 +104,19 @@ export class Plan {
88
104
  });
89
105
  // Empty
90
106
  if (!filteredCurrentParameters && !desiredParameters) {
91
- return new Plan(uuidV4(), ChangeSet.empty(), coreParameters);
107
+ return new Plan(uuidV4(), ChangeSet.empty(), coreParameters, statefulMode);
92
108
  }
93
109
  // CREATE
94
110
  if (!filteredCurrentParameters && desiredParameters) {
95
- return new Plan(uuidV4(), ChangeSet.create(desiredParameters), coreParameters);
111
+ return new Plan(uuidV4(), ChangeSet.create(desiredParameters), coreParameters, statefulMode);
96
112
  }
97
113
  // DESTROY
98
114
  if (filteredCurrentParameters && !desiredParameters) {
99
- return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters);
115
+ return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters, statefulMode);
100
116
  }
101
117
  // NO-OP, MODIFY or RE-CREATE
102
118
  const changeSet = ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
103
- return new Plan(uuidV4(), changeSet, coreParameters);
119
+ return new Plan(uuidV4(), changeSet, coreParameters, statefulMode);
104
120
  }
105
121
  /**
106
122
  * Only keep relevant params for the plan. We don't want to change settings that were not already
@@ -208,7 +224,7 @@ export class Plan {
208
224
  return new Plan(uuidV4(), new ChangeSet(data.operation, data.parameters), {
209
225
  type: data.resourceType,
210
226
  name: data.resourceName,
211
- });
227
+ }, data.statefulMode);
212
228
  function addDefaultValues() {
213
229
  Object.entries(defaultValues ?? {})
214
230
  .forEach(([key, defaultValue]) => {
@@ -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>>;
@@ -1,7 +1,8 @@
1
+ import { ApplyValidationError } from '../common/errors.js';
1
2
  import { Plan } from '../plan/plan.js';
3
+ import { BackgroundPty } from '../pty/background-pty.js';
2
4
  import { ResourceController } from '../resource/resource-controller.js';
3
5
  import { ptyLocalStorage } from '../utils/pty-local-storage.js';
4
- import { BackgroundPty } from '../pty/background-pty.js';
5
6
  export class Plugin {
6
7
  name;
7
8
  resourceControllers;
@@ -82,9 +83,7 @@ export class Plugin {
82
83
  if (!type || !this.resourceControllers.has(type)) {
83
84
  throw new Error(`Resource type not found: ${type}`);
84
85
  }
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
- });
86
+ const plan = await ptyLocalStorage.run(this.planPty, async () => this.resourceControllers.get(type).plan(data.desired ?? null, data.state ?? null, data.isStateful));
88
87
  this.planStorage.set(plan.id, plan);
89
88
  return plan.toResponse();
90
89
  }
@@ -98,6 +97,10 @@ export class Plugin {
98
97
  throw new Error('Malformed plan with resource that cannot be found');
99
98
  }
100
99
  await resource.apply(plan);
100
+ const validationPlan = await ptyLocalStorage.run(new BackgroundPty(), async () => resource.plan(plan.desiredConfig, plan.currentConfig, plan.statefulMode));
101
+ if (validationPlan.requiresChanges()) {
102
+ throw new ApplyValidationError(plan);
103
+ }
101
104
  }
102
105
  resolvePlan(data) {
103
106
  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.114",
3
+ "version": "1.0.115",
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.53",
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,44 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ApplyValidationError } from './errors.js';
3
+ import { Plan } from '../plan/plan.js';
4
+ import { testPlan } from '../utils/test-utils.test.js';
5
+
6
+ describe('Test file for errors file', () => {
7
+ it('Can properly format ApplyValidationError', () => {
8
+ const plan = testPlan({
9
+ desired: null,
10
+ current: [{ propZ: ['a', 'b', 'c'] }],
11
+ state: { propZ: ['a', 'b', 'c'] },
12
+ core: {
13
+ type: 'homebrew',
14
+ name: 'first'
15
+ },
16
+ statefulMode: true,
17
+ })
18
+
19
+ try {
20
+ throw new ApplyValidationError(plan);
21
+ } catch (e) {
22
+ console.error(e);
23
+ expect(e.message).toMatch(
24
+ `Failed to apply changes to resource: "homebrew.first". Additional changes are needed to complete apply.
25
+ Changes remaining:
26
+ {
27
+ "operation": "destroy",
28
+ "parameters": [
29
+ {
30
+ "name": "propZ",
31
+ "operation": "remove",
32
+ "currentValue": [
33
+ "a",
34
+ "b",
35
+ "c"
36
+ ],
37
+ "desiredValue": null
38
+ }
39
+ ]
40
+ }`
41
+ )
42
+ }
43
+ })
44
+ })
@@ -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
+ }
@@ -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
- constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig) {
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
  /**
@@ -59,6 +70,16 @@ export class Plan<T extends StringIndexedObject> {
59
70
  ...this.changeSet.currentParameters,
60
71
  }
61
72
  }
73
+
74
+ get resourceId(): string {
75
+ return this.coreParameters.name
76
+ ? `${this.coreParameters.type}.${this.coreParameters.name}`
77
+ : this.coreParameters.type;
78
+ }
79
+
80
+ requiresChanges(): boolean {
81
+ return this.changeSet.operation !== ResourceOperation.NOOP;
82
+ }
62
83
 
63
84
  /**
64
85
  * When multiples of the same resource are allowed, this matching function will match a given config with one of the
@@ -148,6 +169,7 @@ export class Plan<T extends StringIndexedObject> {
148
169
  uuidV4(),
149
170
  ChangeSet.empty<T>(),
150
171
  coreParameters,
172
+ statefulMode,
151
173
  )
152
174
  }
153
175
 
@@ -156,7 +178,8 @@ export class Plan<T extends StringIndexedObject> {
156
178
  return new Plan(
157
179
  uuidV4(),
158
180
  ChangeSet.create(desiredParameters),
159
- coreParameters
181
+ coreParameters,
182
+ statefulMode,
160
183
  )
161
184
  }
162
185
 
@@ -165,7 +188,8 @@ export class Plan<T extends StringIndexedObject> {
165
188
  return new Plan(
166
189
  uuidV4(),
167
190
  ChangeSet.destroy(filteredCurrentParameters),
168
- coreParameters
191
+ coreParameters,
192
+ statefulMode,
169
193
  )
170
194
  }
171
195
 
@@ -180,6 +204,7 @@ export class Plan<T extends StringIndexedObject> {
180
204
  uuidV4(),
181
205
  changeSet,
182
206
  coreParameters,
207
+ statefulMode,
183
208
  );
184
209
  }
185
210
 
@@ -340,6 +365,7 @@ export class Plan<T extends StringIndexedObject> {
340
365
  type: data.resourceType,
341
366
  name: data.resourceName,
342
367
  },
368
+ data.statefulMode
343
369
  );
344
370
 
345
371
  function addDefaultValues(): void {
@@ -1,10 +1,14 @@
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 { ResourceController } from '../resource/resource-controller.js';
11
+ import { getPty } from '../pty/index.js';
8
12
 
9
13
  interface TestConfig extends StringIndexedObject {
10
14
  propA: string;
@@ -38,15 +42,22 @@ class TestResource extends Resource<TestConfig> {
38
42
 
39
43
  describe('Plugin tests', () => {
40
44
  it('Can apply resource', async () => {
41
- const resource= spy(new TestResource())
45
+ const resource= spy(new class extends TestResource {
46
+ async refresh(): Promise<Partial<TestConfig> | null> {
47
+ return {
48
+ propA: 'abc',
49
+ }
50
+ }
51
+ })
42
52
  const plugin = Plugin.create('testPlugin', [resource as any])
43
53
 
44
- const plan = {
54
+ const plan: ApplyRequestData['plan'] = {
45
55
  operation: ResourceOperation.CREATE,
46
56
  resourceType: 'testResource',
47
57
  parameters: [
48
58
  { name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null },
49
- ]
59
+ ],
60
+ statefulMode: false,
50
61
  };
51
62
 
52
63
  await plugin.apply({ plan });
@@ -54,15 +65,20 @@ describe('Plugin tests', () => {
54
65
  });
55
66
 
56
67
  it('Can destroy resource', async () => {
57
- const resource = spy(new TestResource());
68
+ const resource = spy(new class extends TestResource {
69
+ async refresh(): Promise<Partial<TestConfig> | null> {
70
+ return null;
71
+ }
72
+ });
58
73
  const testPlugin = Plugin.create('testPlugin', [resource as any])
59
74
 
60
- const plan = {
75
+ const plan: ApplyRequestData['plan'] = {
61
76
  operation: ResourceOperation.DESTROY,
62
77
  resourceType: 'testResource',
63
78
  parameters: [
64
79
  { name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' },
65
- ]
80
+ ],
81
+ statefulMode: true,
66
82
  };
67
83
 
68
84
  await testPlugin.apply({ plan })
@@ -70,15 +86,22 @@ describe('Plugin tests', () => {
70
86
  });
71
87
 
72
88
  it('Can re-create resource', async () => {
73
- const resource = spy(new TestResource())
89
+ const resource = spy(new class extends TestResource {
90
+ async refresh(): Promise<Partial<TestConfig> | null> {
91
+ return {
92
+ propA: 'def',
93
+ }
94
+ }
95
+ })
74
96
  const testPlugin = Plugin.create('testPlugin', [resource as any])
75
97
 
76
- const plan = {
98
+ const plan: ApplyRequestData['plan'] = {
77
99
  operation: ResourceOperation.RECREATE,
78
100
  resourceType: 'testResource',
79
101
  parameters: [
80
102
  { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
81
- ]
103
+ ],
104
+ statefulMode: false,
82
105
  };
83
106
 
84
107
  await testPlugin.apply({ plan })
@@ -87,15 +110,22 @@ describe('Plugin tests', () => {
87
110
  });
88
111
 
89
112
  it('Can modify resource', async () => {
90
- const resource = spy(new TestResource())
113
+ const resource = spy(new class extends TestResource {
114
+ async refresh(): Promise<Partial<TestConfig> | null> {
115
+ return {
116
+ propA: 'def',
117
+ }
118
+ }
119
+ })
91
120
  const testPlugin = Plugin.create('testPlugin', [resource as any])
92
121
 
93
- const plan = {
122
+ const plan: ApplyRequestData['plan'] = {
94
123
  operation: ResourceOperation.MODIFY,
95
124
  resourceType: 'testResource',
96
125
  parameters: [
97
126
  { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
98
- ]
127
+ ],
128
+ statefulMode: false,
99
129
  };
100
130
 
101
131
  await testPlugin.apply({ plan })
@@ -178,4 +208,78 @@ describe('Plugin tests', () => {
178
208
  requiredParameters: []
179
209
  })
180
210
  })
211
+
212
+ it('Fails an apply if the validation fails', async () => {
213
+ const resource = spy(new class extends TestResource {
214
+ async refresh(): Promise<Partial<TestConfig> | null> {
215
+ return {
216
+ propA: 'abc',
217
+ }
218
+ }
219
+ })
220
+ const testPlugin = Plugin.create('testPlugin', [resource as any])
221
+
222
+ const plan: ApplyRequestData['plan'] = {
223
+ operation: ResourceOperation.MODIFY,
224
+ resourceType: 'testResource',
225
+ parameters: [
226
+ { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
227
+ ],
228
+ statefulMode: false,
229
+ };
230
+
231
+ await expect(() => testPlugin.apply({ plan }))
232
+ .rejects
233
+ .toThrowError(new ApplyValidationError(Plan.fromResponse(plan)));
234
+ expect(resource.modify.calledOnce).to.be.true;
235
+ })
236
+
237
+ it('Allows the usage of pty in refresh (plan)', async () => {
238
+ const resource = spy(new class extends TestResource {
239
+ async refresh(): Promise<Partial<TestConfig> | null> {
240
+ expect(getPty()).to.not.be.undefined;
241
+ expect(getPty()).to.not.be.null;
242
+
243
+ return null;
244
+ }
245
+ })
246
+
247
+ const testPlugin = Plugin.create('testPlugin', [resource as any]);
248
+ await testPlugin.plan({
249
+ desired: {
250
+ type: 'testResource'
251
+ },
252
+ state: undefined,
253
+ isStateful: false,
254
+ })
255
+
256
+ expect(resource.refresh.calledOnce).to.be.true;
257
+ });
258
+
259
+ it('Allows the usage of pty in validation refresh (apply)', async () => {
260
+ const resource = spy(new class extends TestResource {
261
+ async refresh(): Promise<Partial<TestConfig> | null> {
262
+ expect(getPty()).to.not.be.undefined;
263
+ expect(getPty()).to.not.be.null;
264
+
265
+ return {
266
+ propA: 'abc'
267
+ };
268
+ }
269
+ })
270
+
271
+ const testPlugin = Plugin.create('testPlugin', [resource as any]);
272
+
273
+ const plan: ApplyRequestData['plan'] = {
274
+ operation: ResourceOperation.CREATE,
275
+ resourceType: 'testResource',
276
+ parameters: [
277
+ { name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null },
278
+ ],
279
+ statefulMode: false,
280
+ };
281
+
282
+ await testPlugin.apply({ plan })
283
+ expect(resource.refresh.calledOnce).to.be.true;
284
+ })
181
285
  });
@@ -13,11 +13,12 @@ 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';
17
19
  import { Resource } from '../resource/resource.js';
18
20
  import { ResourceController } from '../resource/resource-controller.js';
19
21
  import { ptyLocalStorage } from '../utils/pty-local-storage.js';
20
- import { BackgroundPty } from '../pty/background-pty.js';
21
22
 
22
23
  export class Plugin {
23
24
  planStorage: Map<string, Plan<any>>;
@@ -122,13 +123,11 @@ export class Plugin {
122
123
  throw new Error(`Resource type not found: ${type}`);
123
124
  }
124
125
 
125
- const plan = await ptyLocalStorage.run(this.planPty, async () => {
126
- return this.resourceControllers.get(type)!.plan(
126
+ const plan = await ptyLocalStorage.run(this.planPty, async () => this.resourceControllers.get(type)!.plan(
127
127
  data.desired ?? null,
128
128
  data.state ?? null,
129
129
  data.isStateful
130
- );
131
- })
130
+ ))
132
131
 
133
132
  this.planStorage.set(plan.id, plan);
134
133
 
@@ -148,6 +147,16 @@ export class Plugin {
148
147
  }
149
148
 
150
149
  await resource.apply(plan);
150
+
151
+ const validationPlan = await ptyLocalStorage.run(new BackgroundPty(), async () => resource.plan(
152
+ plan.desiredConfig,
153
+ plan.currentConfig,
154
+ plan.statefulMode
155
+ ))
156
+
157
+ if (validationPlan.requiresChanges()) {
158
+ throw new ApplyValidationError(plan);
159
+ }
151
160
  }
152
161
 
153
162
  private resolvePlan(data: ApplyRequestData): Plan<ResourceConfig> {
@@ -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
 
package/vitest.config.ts CHANGED
@@ -2,6 +2,7 @@ import { defaultExclude, defineConfig } from 'vitest/config';
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
+ pool: 'forks',
5
6
  exclude: [
6
7
  ...defaultExclude,
7
8
  './src/utils/test-utils.test.ts',