codify-plugin-lib 1.0.65 → 1.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entities/change-set.js +4 -4
- package/dist/entities/plan-types.d.ts +8 -0
- package/dist/entities/plan.d.ts +2 -2
- package/dist/entities/plan.js +6 -0
- package/dist/entities/plugin.js +1 -1
- package/dist/entities/resource.d.ts +8 -7
- package/dist/entities/resource.js +63 -27
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/package.json +1 -1
- package/src/entities/change-set.ts +5 -4
- package/src/entities/plan-types.ts +8 -0
- package/src/entities/plan.test.ts +3 -15
- package/src/entities/plan.ts +10 -2
- package/src/entities/plugin.test.ts +105 -8
- package/src/entities/plugin.ts +5 -1
- package/src/entities/resource-parameters.test.ts +13 -25
- package/src/entities/resource-stateful-mode.test.ts +247 -0
- package/src/entities/resource.test.ts +8 -8
- package/src/entities/resource.ts +103 -46
- package/src/entities/stateful-parameter.test.ts +2 -5
- package/src/index.ts +0 -1
- package/src/utils/test-utils.test.ts +0 -52
- package/src/utils/test-utils.ts +0 -20
|
@@ -59,8 +59,8 @@ export class ChangeSet {
|
|
|
59
59
|
}
|
|
60
60
|
static calculateStatefulModeChangeSet(desired, current, parameterOptions) {
|
|
61
61
|
const parameterChangeSet = new Array();
|
|
62
|
-
const _desired = {
|
|
63
|
-
const _current = {
|
|
62
|
+
const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
|
|
63
|
+
const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
|
|
64
64
|
this.addDefaultValues(_desired, parameterOptions);
|
|
65
65
|
for (const [k, v] of Object.entries(_current)) {
|
|
66
66
|
if (_desired[k] == null) {
|
|
@@ -108,8 +108,8 @@ export class ChangeSet {
|
|
|
108
108
|
}
|
|
109
109
|
static calculateStatelessModeChangeSet(desired, current, parameterOptions) {
|
|
110
110
|
const parameterChangeSet = new Array();
|
|
111
|
-
const _desired = {
|
|
112
|
-
const _current = {
|
|
111
|
+
const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
|
|
112
|
+
const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
|
|
113
113
|
this.addDefaultValues(_desired, parameterOptions);
|
|
114
114
|
for (const [k, v] of Object.entries(_desired)) {
|
|
115
115
|
if (_current[k] == null) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Plan } from './plan.js';
|
|
2
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
1
3
|
export interface ParameterOptions {
|
|
2
4
|
modifyOnChange?: boolean;
|
|
3
5
|
isEqual?: (desired: any, current: any) => boolean;
|
|
@@ -9,3 +11,9 @@ export interface PlanOptions<T> {
|
|
|
9
11
|
statefulMode: boolean;
|
|
10
12
|
parameterOptions?: Record<keyof T, ParameterOptions>;
|
|
11
13
|
}
|
|
14
|
+
export type WithRequired<T, K extends keyof T> = T & {
|
|
15
|
+
[P in K]-?: T[P];
|
|
16
|
+
};
|
|
17
|
+
export type CreatePlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'desiredConfig'>;
|
|
18
|
+
export type DestroyPlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'currentConfig'>;
|
|
19
|
+
export type ModifyPlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'currentConfig' | 'desiredConfig'>;
|
package/dist/entities/plan.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
9
9
|
static create<T extends StringIndexedObject>(desiredParameters: Partial<T> | null, currentParameters: Partial<T> | null, resourceMetadata: ResourceConfig, options: PlanOptions<T>): Plan<T>;
|
|
10
10
|
getResourceType(): string;
|
|
11
11
|
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T>;
|
|
12
|
-
get desiredConfig(): T;
|
|
13
|
-
get currentConfig(): T;
|
|
12
|
+
get desiredConfig(): T | null;
|
|
13
|
+
get currentConfig(): T | null;
|
|
14
14
|
toResponse(): PlanResponseData;
|
|
15
15
|
}
|
package/dist/entities/plan.js
CHANGED
|
@@ -98,12 +98,18 @@ export class Plan {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
get desiredConfig() {
|
|
101
|
+
if (this.changeSet.operation === ResourceOperation.DESTROY) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
101
104
|
return {
|
|
102
105
|
...this.resourceMetadata,
|
|
103
106
|
...this.changeSet.desiredParameters,
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
109
|
get currentConfig() {
|
|
110
|
+
if (this.changeSet.operation === ResourceOperation.CREATE) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
107
113
|
return {
|
|
108
114
|
...this.resourceMetadata,
|
|
109
115
|
...this.changeSet.currentParameters,
|
package/dist/entities/plugin.js
CHANGED
|
@@ -64,7 +64,7 @@ export class Plugin {
|
|
|
64
64
|
throw new Error('Malformed plan with resource that cannot be found');
|
|
65
65
|
}
|
|
66
66
|
await resource.apply(plan);
|
|
67
|
-
const validationPlan = await resource.plan(
|
|
67
|
+
const validationPlan = await resource.plan(plan.desiredConfig, plan.currentConfig, true);
|
|
68
68
|
if (validationPlan.changeSet.operation !== ResourceOperation.NOOP) {
|
|
69
69
|
throw new ApplyValidationError(plan, validationPlan);
|
|
70
70
|
}
|
|
@@ -3,7 +3,7 @@ import { ParameterChange } from './change-set.js';
|
|
|
3
3
|
import { Plan } from './plan.js';
|
|
4
4
|
import { StatefulParameter } from './stateful-parameter.js';
|
|
5
5
|
import { ResourceParameterOptions, ValidationResult } from './resource-types.js';
|
|
6
|
-
import { ParameterOptions } from './plan-types.js';
|
|
6
|
+
import { CreatePlan, DestroyPlan, ModifyPlan, ParameterOptions } from './plan-types.js';
|
|
7
7
|
import { TransformParameter } from './transform-parameter.js';
|
|
8
8
|
import { ResourceOptions } from './resource-options.js';
|
|
9
9
|
import Ajv from 'ajv';
|
|
@@ -24,7 +24,7 @@ export declare abstract class Resource<T extends StringIndexedObject> {
|
|
|
24
24
|
protected constructor(options: ResourceOptions<T>);
|
|
25
25
|
onInitialize(): Promise<void>;
|
|
26
26
|
validateResource(parameters: unknown): Promise<ValidationResult>;
|
|
27
|
-
plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>>;
|
|
27
|
+
plan(desiredConfig: Partial<T> & ResourceConfig | null, currentConfig?: Partial<T> & ResourceConfig | null, statefulMode?: boolean): Promise<Plan<T>>;
|
|
28
28
|
apply(plan: Plan<T>): Promise<void>;
|
|
29
29
|
private _applyCreate;
|
|
30
30
|
private _applyModify;
|
|
@@ -32,11 +32,12 @@ export declare abstract class Resource<T extends StringIndexedObject> {
|
|
|
32
32
|
private validateRefreshResults;
|
|
33
33
|
private applyTransformParameters;
|
|
34
34
|
private addDefaultValues;
|
|
35
|
-
private
|
|
35
|
+
private refreshNonStatefulParameters;
|
|
36
36
|
private refreshStatefulParameters;
|
|
37
|
+
private validatePlanInputs;
|
|
37
38
|
validate(parameters: unknown): Promise<ValidationResult>;
|
|
38
|
-
abstract refresh(
|
|
39
|
-
abstract applyCreate(plan:
|
|
40
|
-
applyModify(pc: ParameterChange<T>, plan:
|
|
41
|
-
abstract applyDestroy(plan:
|
|
39
|
+
abstract refresh(values: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
|
|
40
|
+
abstract applyCreate(plan: CreatePlan<T>): Promise<void>;
|
|
41
|
+
applyModify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void>;
|
|
42
|
+
abstract applyDestroy(plan: DestroyPlan<T>): Promise<void>;
|
|
42
43
|
}
|
|
@@ -49,21 +49,22 @@ export class Resource {
|
|
|
49
49
|
}
|
|
50
50
|
return this.validate(parameters);
|
|
51
51
|
}
|
|
52
|
-
async plan(desiredConfig) {
|
|
52
|
+
async plan(desiredConfig, currentConfig = null, statefulMode = false) {
|
|
53
|
+
this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
|
|
53
54
|
const planOptions = {
|
|
54
|
-
statefulMode
|
|
55
|
+
statefulMode,
|
|
55
56
|
parameterOptions: this.parameterOptions,
|
|
56
57
|
};
|
|
57
58
|
this.addDefaultValues(desiredConfig);
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const currentParameters = await this.
|
|
59
|
+
await this.applyTransformParameters(desiredConfig);
|
|
60
|
+
const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters);
|
|
61
|
+
const { desiredParameters, resourceMetadata, nonStatefulParameters, statefulParameters, } = parsedConfig;
|
|
62
|
+
const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
|
|
62
63
|
if (currentParameters == null) {
|
|
63
|
-
return Plan.create(
|
|
64
|
+
return Plan.create(desiredParameters, null, resourceMetadata, planOptions);
|
|
64
65
|
}
|
|
65
66
|
const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
|
|
66
|
-
return Plan.create(
|
|
67
|
+
return Plan.create(desiredParameters, { ...currentParameters, ...statefulCurrentParameters }, resourceMetadata, planOptions);
|
|
67
68
|
}
|
|
68
69
|
async apply(plan) {
|
|
69
70
|
if (plan.getResourceType() !== this.typeId) {
|
|
@@ -151,20 +152,30 @@ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
|
|
|
151
152
|
Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
|
-
async applyTransformParameters(
|
|
155
|
-
|
|
155
|
+
async applyTransformParameters(desired) {
|
|
156
|
+
if (!desired) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const transformParameters = [...this.transformParameters.entries()]
|
|
156
160
|
.sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA) - this.transformParameterOrder.get(keyB));
|
|
157
|
-
for (const [key,
|
|
158
|
-
|
|
161
|
+
for (const [key, transformParameter] of transformParameters) {
|
|
162
|
+
if (desired[key] === undefined) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const transformedValue = await transformParameter.transform(desired[key]);
|
|
159
166
|
if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
|
|
160
167
|
throw new Error(`Transform parameter ${key} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
|
|
161
168
|
}
|
|
169
|
+
delete desired[key];
|
|
162
170
|
Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
|
|
163
171
|
desired[tvKey] = tvValue;
|
|
164
172
|
});
|
|
165
173
|
}
|
|
166
174
|
}
|
|
167
175
|
addDefaultValues(desired) {
|
|
176
|
+
if (!desired) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
168
179
|
Object.entries(this.defaultValues)
|
|
169
180
|
.forEach(([key, defaultValue]) => {
|
|
170
181
|
if (defaultValue !== undefined && desired[key] === undefined) {
|
|
@@ -172,7 +183,7 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
|
172
183
|
}
|
|
173
184
|
});
|
|
174
185
|
}
|
|
175
|
-
async
|
|
186
|
+
async refreshNonStatefulParameters(resourceParameters) {
|
|
176
187
|
const entriesToRefresh = new Map(Object.entries(resourceParameters));
|
|
177
188
|
const currentParameters = await this.refresh(entriesToRefresh);
|
|
178
189
|
this.validateRefreshResults(currentParameters, entriesToRefresh);
|
|
@@ -204,6 +215,14 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
|
204
215
|
}
|
|
205
216
|
return currentParameters;
|
|
206
217
|
}
|
|
218
|
+
validatePlanInputs(desired, current, statefulMode) {
|
|
219
|
+
if (!desired && !current) {
|
|
220
|
+
throw new Error('Desired config and current config cannot both be missing');
|
|
221
|
+
}
|
|
222
|
+
if (!statefulMode && !desired) {
|
|
223
|
+
throw new Error('Desired config must be provided in non-stateful mode');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
207
226
|
async validate(parameters) {
|
|
208
227
|
return {
|
|
209
228
|
isValid: true,
|
|
@@ -214,23 +233,46 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
|
214
233
|
;
|
|
215
234
|
}
|
|
216
235
|
class ConfigParser {
|
|
217
|
-
|
|
236
|
+
desiredConfig;
|
|
237
|
+
currentConfig;
|
|
218
238
|
statefulParametersMap;
|
|
219
239
|
transformParametersMap;
|
|
220
|
-
constructor(
|
|
221
|
-
this.
|
|
240
|
+
constructor(desiredConfig, currentConfig, statefulParameters, transformParameters) {
|
|
241
|
+
this.desiredConfig = desiredConfig;
|
|
242
|
+
this.currentConfig = currentConfig;
|
|
222
243
|
this.statefulParametersMap = statefulParameters;
|
|
223
244
|
this.transformParametersMap = transformParameters;
|
|
224
245
|
}
|
|
225
246
|
get resourceMetadata() {
|
|
226
|
-
const
|
|
227
|
-
|
|
247
|
+
const desiredMetadata = this.desiredConfig ? splitUserConfig(this.desiredConfig).resourceMetadata : undefined;
|
|
248
|
+
const currentMetadata = this.currentConfig ? splitUserConfig(this.currentConfig).resourceMetadata : undefined;
|
|
249
|
+
if (!desiredMetadata && !currentMetadata) {
|
|
250
|
+
throw new Error(`Unable to parse resource metadata from ${this.desiredConfig}, ${this.currentConfig}`);
|
|
251
|
+
}
|
|
252
|
+
if (currentMetadata && desiredMetadata && (Object.keys(desiredMetadata).length !== Object.keys(currentMetadata).length
|
|
253
|
+
|| Object.entries(desiredMetadata).some(([key, value]) => currentMetadata[key] !== value))) {
|
|
254
|
+
throw new Error(`The metadata for the current config does not match the desired config.
|
|
255
|
+
Desired metadata:
|
|
256
|
+
${JSON.stringify(desiredMetadata, null, 2)}
|
|
257
|
+
|
|
258
|
+
Current metadata:
|
|
259
|
+
${JSON.stringify(currentMetadata, null, 2)}`);
|
|
260
|
+
}
|
|
261
|
+
return desiredMetadata ?? currentMetadata;
|
|
228
262
|
}
|
|
229
|
-
get
|
|
230
|
-
|
|
263
|
+
get desiredParameters() {
|
|
264
|
+
if (!this.desiredConfig) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const { parameters } = splitUserConfig(this.desiredConfig);
|
|
231
268
|
return parameters;
|
|
232
269
|
}
|
|
233
|
-
get
|
|
270
|
+
get parameters() {
|
|
271
|
+
const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
|
|
272
|
+
const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
|
|
273
|
+
return { ...(desiredParameters ?? {}), ...(currentParameters ?? {}) };
|
|
274
|
+
}
|
|
275
|
+
get nonStatefulParameters() {
|
|
234
276
|
const parameters = this.parameters;
|
|
235
277
|
return Object.fromEntries([
|
|
236
278
|
...Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))),
|
|
@@ -242,10 +284,4 @@ class ConfigParser {
|
|
|
242
284
|
...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
|
|
243
285
|
]);
|
|
244
286
|
}
|
|
245
|
-
get transformParameters() {
|
|
246
|
-
const parameters = this.parameters;
|
|
247
|
-
return Object.fromEntries([
|
|
248
|
-
...Object.entries(parameters).filter(([key]) => this.transformParametersMap.has(key)),
|
|
249
|
-
]);
|
|
250
|
-
}
|
|
251
287
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,5 @@ export * from './entities/plan.js';
|
|
|
8
8
|
export * from './entities/plan-types.js';
|
|
9
9
|
export * from './entities/stateful-parameter.js';
|
|
10
10
|
export * from './entities/errors.js';
|
|
11
|
-
export * from './utils/test-utils.js';
|
|
12
11
|
export * from './utils/utils.js';
|
|
13
12
|
export declare function runPlugin(plugin: Plugin): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,6 @@ export * from './entities/plan.js';
|
|
|
8
8
|
export * from './entities/plan-types.js';
|
|
9
9
|
export * from './entities/stateful-parameter.js';
|
|
10
10
|
export * from './entities/errors.js';
|
|
11
|
-
export * from './utils/test-utils.js';
|
|
12
11
|
export * from './utils/utils.js';
|
|
13
12
|
export async function runPlugin(plugin) {
|
|
14
13
|
const messageHandler = new MessageHandler(plugin);
|
package/package.json
CHANGED
|
@@ -123,8 +123,8 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
123
123
|
): ParameterChange<T>[] {
|
|
124
124
|
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
125
125
|
|
|
126
|
-
const _desired = {
|
|
127
|
-
const _current = {
|
|
126
|
+
const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
|
|
127
|
+
const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
|
|
128
128
|
|
|
129
129
|
this.addDefaultValues(_desired, parameterOptions);
|
|
130
130
|
|
|
@@ -190,8 +190,9 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
190
190
|
): ParameterChange<T>[] {
|
|
191
191
|
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
192
192
|
|
|
193
|
-
const _desired = {
|
|
194
|
-
const _current = {
|
|
193
|
+
const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
|
|
194
|
+
const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
|
|
195
|
+
|
|
195
196
|
|
|
196
197
|
this.addDefaultValues(_desired, parameterOptions);
|
|
197
198
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Plan } from './plan.js';
|
|
2
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Customize properties for specific parameters. This will alter the way the library process changes to the parameter.
|
|
3
6
|
*/
|
|
@@ -24,3 +27,8 @@ export interface PlanOptions<T> {
|
|
|
24
27
|
statefulMode: boolean;
|
|
25
28
|
parameterOptions?: Record<keyof T, ParameterOptions>;
|
|
26
29
|
}
|
|
30
|
+
|
|
31
|
+
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }
|
|
32
|
+
export type CreatePlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'desiredConfig'>
|
|
33
|
+
export type DestroyPlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'currentConfig'>
|
|
34
|
+
export type ModifyPlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'currentConfig' | 'desiredConfig'>
|
|
@@ -19,11 +19,7 @@ describe('Plan entity tests', () => {
|
|
|
19
19
|
}]
|
|
20
20
|
}, resource.defaultValues);
|
|
21
21
|
|
|
22
|
-
expect(plan.currentConfig).
|
|
23
|
-
type: 'type',
|
|
24
|
-
propA: null,
|
|
25
|
-
propB: null,
|
|
26
|
-
})
|
|
22
|
+
expect(plan.currentConfig).to.be.null;
|
|
27
23
|
|
|
28
24
|
expect(plan.desiredConfig).toMatchObject({
|
|
29
25
|
type: 'type',
|
|
@@ -56,11 +52,7 @@ describe('Plan entity tests', () => {
|
|
|
56
52
|
propB: 'propBValue',
|
|
57
53
|
})
|
|
58
54
|
|
|
59
|
-
expect(plan.desiredConfig).
|
|
60
|
-
type: 'type',
|
|
61
|
-
propA: null,
|
|
62
|
-
propB: null,
|
|
63
|
-
})
|
|
55
|
+
expect(plan.desiredConfig).to.be.null;
|
|
64
56
|
|
|
65
57
|
expect(plan.changeSet.parameterChanges
|
|
66
58
|
.every((pc) => pc.operation === ParameterOperation.REMOVE)
|
|
@@ -117,11 +109,7 @@ describe('Plan entity tests', () => {
|
|
|
117
109
|
}]
|
|
118
110
|
}, resource.defaultValues);
|
|
119
111
|
|
|
120
|
-
expect(plan.currentConfig).
|
|
121
|
-
type: 'type',
|
|
122
|
-
propA: null,
|
|
123
|
-
propB: null,
|
|
124
|
-
})
|
|
112
|
+
expect(plan.currentConfig).to.be.null
|
|
125
113
|
|
|
126
114
|
expect(plan.desiredConfig).toMatchObject({
|
|
127
115
|
type: 'type',
|
package/src/entities/plan.ts
CHANGED
|
@@ -144,14 +144,22 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
144
144
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
get desiredConfig(): T {
|
|
147
|
+
get desiredConfig(): T | null {
|
|
148
|
+
if (this.changeSet.operation === ResourceOperation.DESTROY) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
return {
|
|
149
153
|
...this.resourceMetadata,
|
|
150
154
|
...this.changeSet.desiredParameters,
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
get currentConfig(): T {
|
|
158
|
+
get currentConfig(): T | null {
|
|
159
|
+
if (this.changeSet.operation === ResourceOperation.CREATE) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
return {
|
|
156
164
|
...this.resourceMetadata,
|
|
157
165
|
...this.changeSet.currentParameters,
|
|
@@ -44,7 +44,7 @@ class TestResource extends Resource<TestConfig> {
|
|
|
44
44
|
|
|
45
45
|
describe('Plugin tests', () => {
|
|
46
46
|
it('Validates that applies were successfully applied', async () => {
|
|
47
|
-
const resource
|
|
47
|
+
const resource= new class extends TestResource {
|
|
48
48
|
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -56,9 +56,9 @@ describe('Plugin tests', () => {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const
|
|
59
|
+
const plugin = Plugin.create('testPlugin', [resource])
|
|
60
60
|
|
|
61
|
-
const
|
|
61
|
+
const plan = {
|
|
62
62
|
operation: ResourceOperation.CREATE,
|
|
63
63
|
resourceType: 'testResource',
|
|
64
64
|
parameters: [
|
|
@@ -67,7 +67,7 @@ describe('Plugin tests', () => {
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
// If this doesn't throw then it passes the test
|
|
70
|
-
await
|
|
70
|
+
await plugin.apply({ plan });
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it('Validates that applies were successfully applied (error)', async () => {
|
|
@@ -80,10 +80,9 @@ describe('Plugin tests', () => {
|
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
const plugin = Plugin.create('testPlugin', [resource])
|
|
83
84
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const desiredPlan = {
|
|
85
|
+
const plan = {
|
|
87
86
|
operation: ResourceOperation.CREATE,
|
|
88
87
|
resourceType: 'testResource',
|
|
89
88
|
parameters: [
|
|
@@ -91,6 +90,104 @@ describe('Plugin tests', () => {
|
|
|
91
90
|
]
|
|
92
91
|
};
|
|
93
92
|
|
|
94
|
-
await expect(async () =>
|
|
93
|
+
await expect(async () => plugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('Validates that deletes were successfully applied', async () => {
|
|
97
|
+
const resource = new class extends TestResource {
|
|
98
|
+
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Return null to indicate that the resource was deleted
|
|
102
|
+
async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const testPlugin = Plugin.create('testPlugin', [resource])
|
|
108
|
+
|
|
109
|
+
const plan = {
|
|
110
|
+
operation: ResourceOperation.DESTROY,
|
|
111
|
+
resourceType: 'testResource',
|
|
112
|
+
parameters: [
|
|
113
|
+
{ name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' },
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// If this doesn't throw then it passes the test
|
|
118
|
+
await testPlugin.apply({ plan })
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('Validates that deletes were successfully applied (error)', async () => {
|
|
122
|
+
const resource = new class extends TestResource {
|
|
123
|
+
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Return a value to indicate that the resource still exists
|
|
127
|
+
async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
|
|
128
|
+
return { propA: 'abc' };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const testPlugin = Plugin.create('testPlugin', [resource])
|
|
133
|
+
|
|
134
|
+
const plan = {
|
|
135
|
+
operation: ResourceOperation.DESTROY,
|
|
136
|
+
resourceType: 'testResource',
|
|
137
|
+
parameters: [
|
|
138
|
+
{ name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' },
|
|
139
|
+
]
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// If this doesn't throw then it passes the test
|
|
143
|
+
expect(async () => await testPlugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('Validates that re-create was successfully applied', async () => {
|
|
147
|
+
const resource = new class extends TestResource {
|
|
148
|
+
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
|
|
152
|
+
return { propA: 'def'};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const testPlugin = Plugin.create('testPlugin', [resource])
|
|
157
|
+
|
|
158
|
+
const plan = {
|
|
159
|
+
operation: ResourceOperation.RECREATE,
|
|
160
|
+
resourceType: 'testResource',
|
|
161
|
+
parameters: [
|
|
162
|
+
{ name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
|
|
163
|
+
]
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// If this doesn't throw then it passes the test
|
|
167
|
+
await testPlugin.apply({ plan })
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('Validates that modify was successfully applied (error)', async () => {
|
|
171
|
+
const resource = new class extends TestResource {
|
|
172
|
+
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
|
|
176
|
+
return { propA: 'abc' };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const testPlugin = Plugin.create('testPlugin', [resource])
|
|
181
|
+
|
|
182
|
+
const plan = {
|
|
183
|
+
operation: ResourceOperation.DESTROY,
|
|
184
|
+
resourceType: 'testResource',
|
|
185
|
+
parameters: [
|
|
186
|
+
{ name: 'propA', operation: ParameterOperation.REMOVE, newValue: 'def', previousValue: 'abc' },
|
|
187
|
+
]
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// If this doesn't throw then it passes the test
|
|
191
|
+
expect(async () => await testPlugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
|
|
95
192
|
});
|
|
96
193
|
});
|
package/src/entities/plugin.ts
CHANGED
|
@@ -95,7 +95,11 @@ export class Plugin {
|
|
|
95
95
|
|
|
96
96
|
// Perform a validation check after to ensure that the plan was properly applied.
|
|
97
97
|
// Sometimes no errors are returned (exit code 0) but the apply was not successful
|
|
98
|
-
const validationPlan = await resource.plan(
|
|
98
|
+
const validationPlan = await resource.plan(
|
|
99
|
+
plan.desiredConfig,
|
|
100
|
+
plan.currentConfig,
|
|
101
|
+
true,
|
|
102
|
+
);
|
|
99
103
|
if (validationPlan.changeSet.operation !== ResourceOperation.NOOP) {
|
|
100
104
|
throw new ApplyValidationError(plan, validationPlan);
|
|
101
105
|
}
|
|
@@ -6,7 +6,7 @@ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
|
6
6
|
import { TestConfig, TestResource } from './resource.test.js';
|
|
7
7
|
import { TransformParameter } from './transform-parameter.js';
|
|
8
8
|
|
|
9
|
-
class TestParameter extends StatefulParameter<TestConfig, string> {
|
|
9
|
+
export class TestParameter extends StatefulParameter<TestConfig, string> {
|
|
10
10
|
constructor(options?: StatefulParameterOptions<string>) {
|
|
11
11
|
super(options ?? {})
|
|
12
12
|
}
|
|
@@ -55,11 +55,7 @@ describe('Resource parameter tests', () => {
|
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
expect(statefulParameter.refresh.notCalled).to.be.true;
|
|
58
|
-
expect(plan.currentConfig).
|
|
59
|
-
type: 'resource',
|
|
60
|
-
propA: null,
|
|
61
|
-
propB: null,
|
|
62
|
-
})
|
|
58
|
+
expect(plan.currentConfig).to.be.null;
|
|
63
59
|
expect(plan.desiredConfig).toMatchObject({
|
|
64
60
|
type: 'resource',
|
|
65
61
|
propA: 'a',
|
|
@@ -171,10 +167,7 @@ describe('Resource parameter tests', () => {
|
|
|
171
167
|
})
|
|
172
168
|
|
|
173
169
|
expect(statefulParameter.refresh.notCalled).to.be.true;
|
|
174
|
-
expect(plan.currentConfig).
|
|
175
|
-
type: 'resource',
|
|
176
|
-
propA: null,
|
|
177
|
-
})
|
|
170
|
+
expect(plan.currentConfig).to.be.null;
|
|
178
171
|
expect(plan.desiredConfig).toMatchObject({
|
|
179
172
|
type: 'resource',
|
|
180
173
|
propA: 'abc',
|
|
@@ -182,7 +175,6 @@ describe('Resource parameter tests', () => {
|
|
|
182
175
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
|
|
183
176
|
})
|
|
184
177
|
|
|
185
|
-
|
|
186
178
|
it('Filters array results in stateless mode to prevent modify from being called', async () => {
|
|
187
179
|
const statefulParameter = new class extends TestParameter {
|
|
188
180
|
async refresh(): Promise<any | null> {
|
|
@@ -350,10 +342,10 @@ describe('Resource parameter tests', () => {
|
|
|
350
342
|
propE: 'propE',
|
|
351
343
|
});
|
|
352
344
|
|
|
353
|
-
expect(plan.currentConfig
|
|
354
|
-
expect(plan.currentConfig
|
|
355
|
-
expect(plan.currentConfig
|
|
356
|
-
expect(plan.currentConfig
|
|
345
|
+
expect(plan.currentConfig?.propB).to.be.lessThan(plan.currentConfig?.propC as any);
|
|
346
|
+
expect(plan.currentConfig?.propC).to.be.lessThan(plan.currentConfig?.propA as any);
|
|
347
|
+
expect(plan.currentConfig?.propA).to.be.lessThan(plan.currentConfig?.propD as any);
|
|
348
|
+
expect(plan.currentConfig?.propD).to.be.lessThan(plan.currentConfig?.propE as any);
|
|
357
349
|
})
|
|
358
350
|
|
|
359
351
|
it('Applies stateful parameters in the order specified', async () => {
|
|
@@ -483,9 +475,9 @@ describe('Resource parameter tests', () => {
|
|
|
483
475
|
expect(resource.refresh.getCall(0).firstArg.has('propB')).to.be.true;
|
|
484
476
|
expect(resource.refresh.getCall(0).firstArg.has('propC')).to.be.false;
|
|
485
477
|
|
|
486
|
-
expect(plan.desiredConfig
|
|
487
|
-
expect(plan.desiredConfig
|
|
488
|
-
expect(plan.desiredConfig
|
|
478
|
+
expect(plan.desiredConfig?.propA).to.eq('propA');
|
|
479
|
+
expect(plan.desiredConfig?.propB).to.eq(10);
|
|
480
|
+
expect(plan.desiredConfig?.propC).to.be.undefined;
|
|
489
481
|
|
|
490
482
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
491
483
|
})
|
|
@@ -570,8 +562,8 @@ describe('Resource parameter tests', () => {
|
|
|
570
562
|
propC: 'propC',
|
|
571
563
|
});
|
|
572
564
|
|
|
573
|
-
expect(plan.desiredConfig
|
|
574
|
-
expect(plan.desiredConfig
|
|
565
|
+
expect(plan.desiredConfig?.propE).to.be.lessThan(plan.desiredConfig?.propF as any);
|
|
566
|
+
expect(plan.desiredConfig?.propF).to.be.lessThan(plan.desiredConfig?.propD as any);
|
|
575
567
|
})
|
|
576
568
|
|
|
577
569
|
it('Plans transform even for creating new resources', async () => {
|
|
@@ -602,11 +594,7 @@ describe('Resource parameter tests', () => {
|
|
|
602
594
|
propB: 10,
|
|
603
595
|
propC: 'propC',
|
|
604
596
|
});
|
|
605
|
-
expect(plan.currentConfig).
|
|
606
|
-
type: 'resourceType',
|
|
607
|
-
propD: null,
|
|
608
|
-
propE: null,
|
|
609
|
-
})
|
|
597
|
+
expect(plan.currentConfig).to.be.null;
|
|
610
598
|
expect(plan.desiredConfig).toMatchObject({
|
|
611
599
|
type: 'resourceType',
|
|
612
600
|
propD: 'abc',
|