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.
- package/dist/common/errors.d.ts +8 -0
- package/dist/common/errors.js +24 -0
- package/dist/plan/plan.d.ts +10 -1
- package/dist/plan/plan.js +22 -6
- package/dist/plugin/plugin.d.ts +1 -1
- package/dist/plugin/plugin.js +7 -4
- package/package.json +2 -2
- package/src/common/errors.test.ts +44 -0
- package/src/common/errors.ts +31 -0
- package/src/plan/plan.test.ts +8 -4
- package/src/plan/plan.ts +30 -4
- package/src/plugin/plugin.test.ts +117 -13
- package/src/plugin/plugin.ts +14 -5
- 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/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,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
|
-
|
|
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]) => {
|
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>>;
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
+
}
|
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
|
/**
|
|
@@ -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
|
});
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -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
|
|