codify-plugin-lib 1.0.76 → 1.0.77
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/.eslintrc.json +11 -4
- package/.github/workflows/release.yaml +19 -0
- package/.github/workflows/unit-test-ci.yaml +19 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +10 -10
- package/dist/index.js +9 -9
- package/dist/messages/handlers.d.ts +1 -1
- package/dist/messages/handlers.js +2 -1
- package/dist/plan/change-set.d.ts +37 -0
- package/dist/plan/change-set.js +146 -0
- package/dist/plan/plan-types.d.ts +23 -0
- package/dist/plan/plan-types.js +1 -0
- package/dist/plan/plan.d.ts +59 -0
- package/dist/plan/plan.js +228 -0
- package/dist/plugin/plugin.d.ts +17 -0
- package/dist/plugin/plugin.js +83 -0
- package/dist/resource/config-parser.d.ts +14 -0
- package/dist/resource/config-parser.js +48 -0
- package/dist/resource/parsed-resource-settings.d.ts +26 -0
- package/dist/resource/parsed-resource-settings.js +126 -0
- package/dist/resource/resource-controller.d.ts +30 -0
- package/dist/resource/resource-controller.js +247 -0
- package/dist/resource/resource-settings.d.ts +149 -0
- package/dist/resource/resource-settings.js +9 -0
- package/dist/resource/resource.d.ts +137 -0
- package/dist/resource/resource.js +44 -0
- package/dist/resource/stateful-parameter.d.ts +164 -0
- package/dist/resource/stateful-parameter.js +94 -0
- package/dist/utils/utils.d.ts +19 -3
- package/dist/utils/utils.js +52 -3
- package/package.json +5 -3
- package/src/index.ts +10 -11
- package/src/messages/handlers.test.ts +10 -37
- package/src/messages/handlers.ts +2 -2
- package/src/plan/change-set.test.ts +220 -0
- package/src/plan/change-set.ts +225 -0
- package/src/plan/plan-types.ts +27 -0
- package/src/{entities → plan}/plan.test.ts +35 -29
- package/src/plan/plan.ts +353 -0
- package/src/{entities → plugin}/plugin.test.ts +14 -13
- package/src/{entities → plugin}/plugin.ts +28 -24
- package/src/resource/config-parser.ts +77 -0
- package/src/{entities/resource-options.test.ts → resource/parsed-resource-settings.test.ts} +8 -7
- package/src/resource/parsed-resource-settings.ts +179 -0
- package/src/{entities/resource-stateful-mode.test.ts → resource/resource-controller-stateful-mode.test.ts} +36 -39
- package/src/{entities/resource.test.ts → resource/resource-controller.test.ts} +116 -176
- package/src/resource/resource-controller.ts +340 -0
- package/src/resource/resource-settings.test.ts +494 -0
- package/src/resource/resource-settings.ts +192 -0
- package/src/resource/resource.ts +149 -0
- package/src/resource/stateful-parameter.test.ts +93 -0
- package/src/resource/stateful-parameter.ts +217 -0
- package/src/utils/test-utils.test.ts +87 -0
- package/src/utils/utils.test.ts +2 -2
- package/src/utils/utils.ts +51 -5
- package/tsconfig.json +0 -1
- package/vitest.config.ts +10 -0
- package/src/entities/change-set.test.ts +0 -155
- package/src/entities/change-set.ts +0 -244
- package/src/entities/plan-types.ts +0 -44
- package/src/entities/plan.ts +0 -178
- package/src/entities/resource-options.ts +0 -155
- package/src/entities/resource-parameters.test.ts +0 -604
- package/src/entities/resource-types.ts +0 -31
- package/src/entities/resource.ts +0 -470
- package/src/entities/stateful-parameter.test.ts +0 -114
- package/src/entities/stateful-parameter.ts +0 -92
- package/src/entities/transform-parameter.ts +0 -13
- /package/src/{entities/errors.ts → errors.ts} +0 -0
package/src/entities/resource.ts
DELETED
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
import { Ajv, ValidateFunction } from 'ajv';
|
|
2
|
-
import {
|
|
3
|
-
ParameterOperation,
|
|
4
|
-
ResourceConfig,
|
|
5
|
-
ResourceOperation,
|
|
6
|
-
StringIndexedObject,
|
|
7
|
-
ValidateResponseData,
|
|
8
|
-
} from 'codify-schemas';
|
|
9
|
-
|
|
10
|
-
import { setsEqual, splitUserConfig } from '../utils/utils.js';
|
|
11
|
-
import { ParameterChange } from './change-set.js';
|
|
12
|
-
import { Plan } from './plan.js';
|
|
13
|
-
import { CreatePlan, DestroyPlan, ModifyPlan, ParameterOptions, PlanOptions } from './plan-types.js';
|
|
14
|
-
import { ResourceOptions, ResourceOptionsParser } from './resource-options.js';
|
|
15
|
-
import { ResourceParameterOptions } from './resource-types.js';
|
|
16
|
-
import { StatefulParameter } from './stateful-parameter.js';
|
|
17
|
-
import { TransformParameter } from './transform-parameter.js';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Description of resource here
|
|
21
|
-
* Two main functions:
|
|
22
|
-
* - Plan
|
|
23
|
-
* - Apply
|
|
24
|
-
*
|
|
25
|
-
*/
|
|
26
|
-
export abstract class Resource<T extends StringIndexedObject> {
|
|
27
|
-
readonly typeId: string;
|
|
28
|
-
readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
|
|
29
|
-
readonly transformParameters: Map<keyof T, TransformParameter<T>>
|
|
30
|
-
readonly resourceParameters: Map<keyof T, ResourceParameterOptions>;
|
|
31
|
-
|
|
32
|
-
readonly statefulParameterOrder: Map<keyof T, number>;
|
|
33
|
-
readonly transformParameterOrder: Map<keyof T, number>;
|
|
34
|
-
|
|
35
|
-
readonly dependencies: string[]; // TODO: Change this to a string
|
|
36
|
-
readonly parameterOptions: Record<keyof T, ParameterOptions>
|
|
37
|
-
readonly options: ResourceOptions<T>;
|
|
38
|
-
readonly defaultValues: Partial<Record<keyof T, unknown>>;
|
|
39
|
-
|
|
40
|
-
protected ajv?: Ajv;
|
|
41
|
-
protected schemaValidator?: ValidateFunction;
|
|
42
|
-
|
|
43
|
-
protected constructor(options: ResourceOptions<T>) {
|
|
44
|
-
this.typeId = options.type;
|
|
45
|
-
this.dependencies = options.dependencies ?? [];
|
|
46
|
-
this.options = options;
|
|
47
|
-
|
|
48
|
-
if (this.options.schema) {
|
|
49
|
-
this.ajv = new Ajv({
|
|
50
|
-
allErrors: true,
|
|
51
|
-
strict: true,
|
|
52
|
-
strictRequired: false,
|
|
53
|
-
})
|
|
54
|
-
this.schemaValidator = this.ajv.compile(this.options.schema);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const parser = new ResourceOptionsParser<T>(options);
|
|
58
|
-
this.statefulParameters = parser.statefulParameters;
|
|
59
|
-
this.transformParameters = parser.transformParameters;
|
|
60
|
-
this.resourceParameters = parser.resourceParameters;
|
|
61
|
-
this.parameterOptions = parser.changeSetParameterOptions;
|
|
62
|
-
this.defaultValues = parser.defaultValues;
|
|
63
|
-
this.statefulParameterOrder = parser.statefulParameterOrder;
|
|
64
|
-
this.transformParameterOrder = parser.transformParameterOrder;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async onInitialize(): Promise<void> {}
|
|
68
|
-
|
|
69
|
-
async validate(
|
|
70
|
-
parameters: Partial<T>,
|
|
71
|
-
resourceMetaData: ResourceConfig
|
|
72
|
-
): Promise<ValidateResponseData['resourceValidations'][0]> {
|
|
73
|
-
if (this.schemaValidator) {
|
|
74
|
-
const isValid = this.schemaValidator(parameters);
|
|
75
|
-
|
|
76
|
-
if (!isValid) {
|
|
77
|
-
return {
|
|
78
|
-
isValid: false,
|
|
79
|
-
resourceName: resourceMetaData.name,
|
|
80
|
-
resourceType: resourceMetaData.type,
|
|
81
|
-
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
let isValid = true;
|
|
87
|
-
let customValidationErrorMessage;
|
|
88
|
-
try {
|
|
89
|
-
await this.customValidation(parameters);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
isValid = false;
|
|
92
|
-
customValidationErrorMessage = (error as Error).message;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!isValid) {
|
|
96
|
-
return {
|
|
97
|
-
customValidationErrorMessage,
|
|
98
|
-
isValid: false,
|
|
99
|
-
resourceName: resourceMetaData.name,
|
|
100
|
-
resourceType: resourceMetaData.type,
|
|
101
|
-
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
isValid: true,
|
|
107
|
-
resourceName: resourceMetaData.name,
|
|
108
|
-
resourceType: resourceMetaData.type,
|
|
109
|
-
schemaValidationErrors: [],
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// TODO: Currently stateful mode expects that the currentConfig does not need any additional transformations (default and transform parameters)
|
|
114
|
-
// This may change in the future?
|
|
115
|
-
async plan(
|
|
116
|
-
desiredConfig: Partial<T> & ResourceConfig | null,
|
|
117
|
-
currentConfig: Partial<T> & ResourceConfig | null = null,
|
|
118
|
-
statefulMode = false,
|
|
119
|
-
): Promise<Plan<T>> {
|
|
120
|
-
this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
|
|
121
|
-
|
|
122
|
-
const planOptions: PlanOptions<T> = {
|
|
123
|
-
parameterOptions: this.parameterOptions,
|
|
124
|
-
statefulMode,
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
this.addDefaultValues(desiredConfig);
|
|
128
|
-
await this.applyTransformParameters(desiredConfig);
|
|
129
|
-
|
|
130
|
-
// Parse data from the user supplied config
|
|
131
|
-
const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters)
|
|
132
|
-
const {
|
|
133
|
-
desiredParameters,
|
|
134
|
-
nonStatefulParameters,
|
|
135
|
-
resourceMetadata,
|
|
136
|
-
statefulParameters,
|
|
137
|
-
} = parsedConfig;
|
|
138
|
-
|
|
139
|
-
// Refresh resource parameters. This refreshes the parameters that configure the resource itself
|
|
140
|
-
const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
|
|
141
|
-
|
|
142
|
-
// Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
|
|
143
|
-
if (currentParameters === null || currentParameters === undefined) {
|
|
144
|
-
return Plan.create(
|
|
145
|
-
desiredParameters,
|
|
146
|
-
null,
|
|
147
|
-
resourceMetadata,
|
|
148
|
-
planOptions,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Refresh stateful parameters. These parameters have state external to the resource
|
|
153
|
-
const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
|
|
154
|
-
|
|
155
|
-
return Plan.create(
|
|
156
|
-
desiredParameters,
|
|
157
|
-
{ ...currentParameters, ...statefulCurrentParameters } as Partial<T>,
|
|
158
|
-
resourceMetadata,
|
|
159
|
-
planOptions,
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async apply(plan: Plan<T>): Promise<void> {
|
|
164
|
-
if (plan.getResourceType() !== this.typeId) {
|
|
165
|
-
throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
switch (plan.changeSet.operation) {
|
|
169
|
-
case ResourceOperation.CREATE: {
|
|
170
|
-
return this._applyCreate(plan); // TODO: Add new parameters value so that apply
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
case ResourceOperation.MODIFY: {
|
|
174
|
-
return this._applyModify(plan);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
case ResourceOperation.RECREATE: {
|
|
178
|
-
await this._applyDestroy(plan);
|
|
179
|
-
return this._applyCreate(plan);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
case ResourceOperation.DESTROY: {
|
|
183
|
-
return this._applyDestroy(plan);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private async _applyCreate(plan: Plan<T>): Promise<void> {
|
|
189
|
-
await this.applyCreate(plan as CreatePlan<T>);
|
|
190
|
-
|
|
191
|
-
const statefulParameterChanges = plan.changeSet.parameterChanges
|
|
192
|
-
.filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
|
|
193
|
-
.sort((a, b) => this.statefulParameterOrder.get(a.name)! - this.statefulParameterOrder.get(b.name)!)
|
|
194
|
-
|
|
195
|
-
for (const parameterChange of statefulParameterChanges) {
|
|
196
|
-
const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
|
|
197
|
-
await statefulParameter.applyAdd(parameterChange.newValue, plan);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private async _applyModify(plan: Plan<T>): Promise<void> {
|
|
202
|
-
const parameterChanges = plan
|
|
203
|
-
.changeSet
|
|
204
|
-
.parameterChanges
|
|
205
|
-
.filter((c: ParameterChange<T>) => c.operation !== ParameterOperation.NOOP);
|
|
206
|
-
|
|
207
|
-
const statelessParameterChanges = parameterChanges
|
|
208
|
-
.filter((pc: ParameterChange<T>) => !this.statefulParameters.has(pc.name))
|
|
209
|
-
|
|
210
|
-
for (const pc of statelessParameterChanges) {
|
|
211
|
-
// TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
|
|
212
|
-
await this.applyModify(pc, plan as ModifyPlan<T>);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const statefulParameterChanges = parameterChanges
|
|
216
|
-
.filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
|
|
217
|
-
.sort((a, b) => this.statefulParameterOrder.get(a.name)! - this.statefulParameterOrder.get(b.name)!)
|
|
218
|
-
|
|
219
|
-
for (const parameterChange of statefulParameterChanges) {
|
|
220
|
-
const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
|
|
221
|
-
|
|
222
|
-
switch (parameterChange.operation) {
|
|
223
|
-
case ParameterOperation.ADD: {
|
|
224
|
-
await statefulParameter.applyAdd(parameterChange.newValue, plan);
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
case ParameterOperation.MODIFY: {
|
|
229
|
-
// TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
|
|
230
|
-
await statefulParameter.applyModify(parameterChange.newValue, parameterChange.previousValue, false, plan);
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
case ParameterOperation.REMOVE: {
|
|
235
|
-
await statefulParameter.applyRemove(parameterChange.previousValue, plan);
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
private async _applyDestroy(plan: Plan<T>): Promise<void> {
|
|
243
|
-
// If this option is set (defaults to false), then stateful parameters need to be destroyed
|
|
244
|
-
// as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
|
|
245
|
-
if (this.options.callStatefulParameterRemoveOnDestroy) {
|
|
246
|
-
const statefulParameterChanges = plan.changeSet.parameterChanges
|
|
247
|
-
.filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
|
|
248
|
-
.sort((a, b) => this.statefulParameterOrder.get(a.name)! - this.statefulParameterOrder.get(b.name)!)
|
|
249
|
-
|
|
250
|
-
for (const parameterChange of statefulParameterChanges) {
|
|
251
|
-
const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
|
|
252
|
-
await statefulParameter.applyRemove(parameterChange.previousValue, plan);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
await this.applyDestroy(plan as DestroyPlan<T>);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private validateRefreshResults(refresh: Partial<T> | null, desired: Partial<T>) {
|
|
260
|
-
if (!refresh) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const desiredKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
|
|
265
|
-
const refreshKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
|
|
266
|
-
|
|
267
|
-
if (!setsEqual(desiredKeys, refreshKeys)) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
`Resource ${this.typeId}
|
|
270
|
-
refresh() must return back exactly the keys that were provided
|
|
271
|
-
Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
|
|
272
|
-
Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
private async applyTransformParameters(desired: Partial<T> | null): Promise<void> {
|
|
278
|
-
if (!desired) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const transformParameters = [...this.transformParameters.entries()]
|
|
283
|
-
.sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA)! - this.transformParameterOrder.get(keyB)!)
|
|
284
|
-
|
|
285
|
-
for (const [key, transformParameter] of transformParameters) {
|
|
286
|
-
if (desired[key] === undefined) {
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const transformedValue = await transformParameter.transform(desired[key]);
|
|
291
|
-
|
|
292
|
-
if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
|
|
293
|
-
throw new Error(`Transform parameter ${key as string} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Remove original transform parameter from the config
|
|
297
|
-
delete desired[key];
|
|
298
|
-
|
|
299
|
-
// Add the new transformed values
|
|
300
|
-
for (const [tvKey, tvValue] of Object.entries(transformedValue)) {
|
|
301
|
-
// @ts-ignore
|
|
302
|
-
desired[tvKey] = tvValue;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private addDefaultValues(desired: Partial<T> | null): void {
|
|
308
|
-
if (!desired) {
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
for (const [key, defaultValue] of Object.entries(this.defaultValues)) {
|
|
313
|
-
if (defaultValue !== undefined && desired[key as any] === undefined) {
|
|
314
|
-
// @ts-ignore
|
|
315
|
-
desired[key] = defaultValue;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
private async refreshNonStatefulParameters(resourceParameters: Partial<T>): Promise<Partial<T> | null> {
|
|
321
|
-
const currentParameters = await this.refresh(resourceParameters);
|
|
322
|
-
this.validateRefreshResults(currentParameters, resourceParameters);
|
|
323
|
-
return currentParameters;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Refresh stateful parameters
|
|
327
|
-
// This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
|
|
328
|
-
private async refreshStatefulParameters(statefulParametersConfig: Partial<T>, isStatefulMode: boolean): Promise<Partial<T>> {
|
|
329
|
-
const currentParameters: Partial<T> = {}
|
|
330
|
-
const sortedEntries = Object.entries(statefulParametersConfig)
|
|
331
|
-
.sort(([key1], [key2]) => this.statefulParameterOrder.get(key1)! - this.statefulParameterOrder.get(key2)!)
|
|
332
|
-
|
|
333
|
-
for(const [key, desiredValue] of sortedEntries) {
|
|
334
|
-
const statefulParameter = this.statefulParameters.get(key);
|
|
335
|
-
if (!statefulParameter) {
|
|
336
|
-
throw new Error(`Stateful parameter ${key} was not found`);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
let currentValue = await statefulParameter.refresh(desiredValue ?? null);
|
|
340
|
-
|
|
341
|
-
// In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
|
|
342
|
-
// Otherwise the change set will pick up the extra keys from the current and try to delete them
|
|
343
|
-
// This allows arrays within stateful parameters to be first class objects
|
|
344
|
-
if (Array.isArray(currentValue)
|
|
345
|
-
&& Array.isArray(desiredValue)
|
|
346
|
-
&& !isStatefulMode
|
|
347
|
-
&& !statefulParameter.options.disableStatelessModeArrayFiltering
|
|
348
|
-
) {
|
|
349
|
-
currentValue = currentValue.filter((c) => desiredValue?.some((d) => {
|
|
350
|
-
const parameterOptions = statefulParameter.options as any;
|
|
351
|
-
if (parameterOptions && parameterOptions.isElementEqual) {
|
|
352
|
-
return parameterOptions.isElementEqual(d, c);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return d === c;
|
|
356
|
-
})) as any;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// @ts-ignore
|
|
360
|
-
currentParameters[key] = currentValue;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return currentParameters;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private validatePlanInputs(
|
|
367
|
-
desired: Partial<T> & ResourceConfig | null,
|
|
368
|
-
current: Partial<T> & ResourceConfig | null,
|
|
369
|
-
statefulMode: boolean,
|
|
370
|
-
) {
|
|
371
|
-
if (!desired && !current) {
|
|
372
|
-
throw new Error('Desired config and current config cannot both be missing')
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (!statefulMode && !desired) {
|
|
376
|
-
throw new Error('Desired config must be provided in non-stateful mode')
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Add custom validation logic in-addition to the default schema validation.
|
|
382
|
-
* In this method throw an error if the object did not validate. The message of the
|
|
383
|
-
* error will be shown to the user.
|
|
384
|
-
* @param parameters
|
|
385
|
-
*/
|
|
386
|
-
async customValidation(parameters: Partial<T>): Promise<void> {};
|
|
387
|
-
|
|
388
|
-
abstract refresh(parameters: Partial<T>): Promise<Partial<T> | null>;
|
|
389
|
-
|
|
390
|
-
abstract applyCreate(plan: CreatePlan<T>): Promise<void>;
|
|
391
|
-
|
|
392
|
-
async applyModify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void> {};
|
|
393
|
-
|
|
394
|
-
abstract applyDestroy(plan: DestroyPlan<T>): Promise<void>;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
class ConfigParser<T extends StringIndexedObject> {
|
|
398
|
-
private desiredConfig: Partial<T> & ResourceConfig | null;
|
|
399
|
-
private currentConfig: Partial<T> & ResourceConfig | null;
|
|
400
|
-
private statefulParametersMap: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
|
|
401
|
-
private transformParametersMap: Map<keyof T, TransformParameter<T>>;
|
|
402
|
-
|
|
403
|
-
constructor(
|
|
404
|
-
desiredConfig: Partial<T> & ResourceConfig | null,
|
|
405
|
-
currentConfig: Partial<T> & ResourceConfig | null,
|
|
406
|
-
statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>,
|
|
407
|
-
transformParameters: Map<keyof T, TransformParameter<T>>,
|
|
408
|
-
) {
|
|
409
|
-
this.desiredConfig = desiredConfig;
|
|
410
|
-
this.currentConfig = currentConfig
|
|
411
|
-
this.statefulParametersMap = statefulParameters;
|
|
412
|
-
this.transformParametersMap = transformParameters;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
get resourceMetadata(): ResourceConfig {
|
|
416
|
-
const desiredMetadata = this.desiredConfig ? splitUserConfig(this.desiredConfig).resourceMetadata : undefined;
|
|
417
|
-
const currentMetadata = this.currentConfig ? splitUserConfig(this.currentConfig).resourceMetadata : undefined;
|
|
418
|
-
|
|
419
|
-
if (!desiredMetadata && !currentMetadata) {
|
|
420
|
-
throw new Error(`Unable to parse resource metadata from ${this.desiredConfig}, ${this.currentConfig}`)
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (currentMetadata && desiredMetadata && (
|
|
424
|
-
Object.keys(desiredMetadata).length !== Object.keys(currentMetadata).length
|
|
425
|
-
|| Object.entries(desiredMetadata).some(([key, value]) => currentMetadata[key] !== value)
|
|
426
|
-
)) {
|
|
427
|
-
throw new Error(`The metadata for the current config does not match the desired config.
|
|
428
|
-
Desired metadata:
|
|
429
|
-
${JSON.stringify(desiredMetadata, null, 2)}
|
|
430
|
-
|
|
431
|
-
Current metadata:
|
|
432
|
-
${JSON.stringify(currentMetadata, null, 2)}`);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return desiredMetadata ?? currentMetadata!;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
get desiredParameters(): Partial<T> | null {
|
|
439
|
-
if (!this.desiredConfig) {
|
|
440
|
-
return null;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const { parameters } = splitUserConfig(this.desiredConfig);
|
|
444
|
-
return parameters;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
get parameters(): Partial<T> {
|
|
449
|
-
const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
|
|
450
|
-
const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
|
|
451
|
-
|
|
452
|
-
return { ...desiredParameters, ...currentParameters } as Partial<T>;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
get nonStatefulParameters(): Partial<T> {
|
|
456
|
-
const { parameters } = this;
|
|
457
|
-
|
|
458
|
-
return Object.fromEntries(
|
|
459
|
-
Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key)))
|
|
460
|
-
) as Partial<T>;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
get statefulParameters(): Partial<T> {
|
|
464
|
-
const { parameters } = this;
|
|
465
|
-
|
|
466
|
-
return Object.fromEntries(
|
|
467
|
-
Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key))
|
|
468
|
-
) as Partial<T>;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { ArrayStatefulParameter, ArrayStatefulParameterOptions, } from './stateful-parameter.js';
|
|
3
|
-
import { Plan } from './plan.js';
|
|
4
|
-
import { spy } from 'sinon';
|
|
5
|
-
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
6
|
-
|
|
7
|
-
interface TestConfig {
|
|
8
|
-
propA: string[];
|
|
9
|
-
[x: string]: unknown;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
class TestArrayParameter extends ArrayStatefulParameter<TestConfig, string> {
|
|
13
|
-
constructor(options?: ArrayStatefulParameterOptions<string>) {
|
|
14
|
-
super(options)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async applyAddItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
|
|
18
|
-
async applyRemoveItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
|
|
19
|
-
|
|
20
|
-
async refresh(): Promise<string[] | null> {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('Stateful parameter tests', () => {
|
|
26
|
-
it('applyAddItem is called the correct number of times', async () => {
|
|
27
|
-
const plan = Plan.create<TestConfig>(
|
|
28
|
-
{ propA: ['a', 'b', 'c'] },
|
|
29
|
-
null,
|
|
30
|
-
{ type: 'typeA' },
|
|
31
|
-
{ statefulMode: false }
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
|
|
35
|
-
expect(plan.changeSet.parameterChanges.length).to.eq(1);
|
|
36
|
-
|
|
37
|
-
const testParameter = spy(new TestArrayParameter());
|
|
38
|
-
await testParameter.applyAdd(plan.desiredConfig!.propA, plan);
|
|
39
|
-
|
|
40
|
-
expect(testParameter.applyAddItem.callCount).to.eq(3);
|
|
41
|
-
expect(testParameter.applyRemoveItem.called).to.be.false;
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('applyRemoveItem is called the correct number of times', async () => {
|
|
45
|
-
const plan = Plan.create<TestConfig>(
|
|
46
|
-
null,
|
|
47
|
-
{ propA: ['a', 'b', 'c'] },
|
|
48
|
-
{ type: 'typeA' },
|
|
49
|
-
{ statefulMode: true }
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY);
|
|
53
|
-
expect(plan.changeSet.parameterChanges.length).to.eq(1);
|
|
54
|
-
|
|
55
|
-
const testParameter = spy(new TestArrayParameter());
|
|
56
|
-
await testParameter.applyRemove(plan.currentConfig!.propA, plan);
|
|
57
|
-
|
|
58
|
-
expect(testParameter.applyAddItem.called).to.be.false;
|
|
59
|
-
expect(testParameter.applyRemoveItem.callCount).to.eq(3);
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('In stateless mode only applyAddItem is called only for modifies', async () => {
|
|
63
|
-
const plan = Plan.create<TestConfig>(
|
|
64
|
-
{ propA: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
|
|
65
|
-
{ propA: ['a', 'b', 'c'] },
|
|
66
|
-
{ type: 'typeA' },
|
|
67
|
-
{ statefulMode: true, parameterOptions: { propA: { isStatefulParameter: true }} }
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
|
|
71
|
-
expect(plan.changeSet.parameterChanges[0]).toMatchObject({
|
|
72
|
-
name: 'propA',
|
|
73
|
-
previousValue: ['a', 'b', 'c'],
|
|
74
|
-
newValue: ['a', 'c', 'd', 'e', 'f'],
|
|
75
|
-
operation: ParameterOperation.MODIFY,
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
const testParameter = spy(new TestArrayParameter());
|
|
79
|
-
await testParameter.applyModify(plan.desiredConfig!.propA, plan.currentConfig!.propA, false, plan);
|
|
80
|
-
|
|
81
|
-
expect(testParameter.applyAddItem.calledThrice).to.be.true;
|
|
82
|
-
expect(testParameter.applyRemoveItem.called).to.be.false;
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('isElementEqual is called for modifies', async () => {
|
|
86
|
-
const plan = Plan.create<TestConfig>(
|
|
87
|
-
{ propA: ['9.12', '9.13'] }, // b to remove, d, e, f to add
|
|
88
|
-
{ propA: ['9.12.9'] },
|
|
89
|
-
{ type: 'typeA' },
|
|
90
|
-
{ statefulMode: false, parameterOptions: { propA: { isStatefulParameter: true }} }
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
|
|
94
|
-
expect(plan.changeSet.parameterChanges[0]).toMatchObject({
|
|
95
|
-
name: 'propA',
|
|
96
|
-
previousValue: ['9.12.9'],
|
|
97
|
-
newValue: ['9.12', '9.13'],
|
|
98
|
-
operation: ParameterOperation.MODIFY,
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
const testParameter = spy(new class extends TestArrayParameter {
|
|
102
|
-
constructor() {
|
|
103
|
-
super({
|
|
104
|
-
isElementEqual: (desired, current) => current.includes(desired),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
await testParameter.applyModify(plan.desiredConfig!.propA, plan.currentConfig!.propA, false, plan);
|
|
110
|
-
|
|
111
|
-
expect(testParameter.applyAddItem.calledOnce).to.be.true;
|
|
112
|
-
expect(testParameter.applyRemoveItem.called).to.be.false;
|
|
113
|
-
})
|
|
114
|
-
})
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { Plan } from './plan.js';
|
|
2
|
-
import { StringIndexedObject } from 'codify-schemas';
|
|
3
|
-
|
|
4
|
-
export interface StatefulParameterOptions<V> {
|
|
5
|
-
isEqual?: (desired: any, current: any) => boolean;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* In stateless mode, array refresh results (current) will be automatically filtered by the user config (desired).
|
|
9
|
-
* This is done to ensure that for modify operations, stateless mode will not try to delete existing resources.
|
|
10
|
-
*
|
|
11
|
-
* Ex: System has python 3.11.9 and 3.12.7 installed (current). Desired is 3.11. Without filtering 3.12.7 will be deleted
|
|
12
|
-
* in the next modify
|
|
13
|
-
*
|
|
14
|
-
* Set this flag to true to disable this behaviour
|
|
15
|
-
*/
|
|
16
|
-
disableStatelessModeArrayFiltering?: boolean;
|
|
17
|
-
default?: V;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ArrayStatefulParameterOptions<V> extends StatefulParameterOptions<V> {
|
|
21
|
-
isEqual?: (desired: any[], current: any[]) => boolean;
|
|
22
|
-
isElementEqual?: (desired: any, current: any) => boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
|
|
27
|
-
readonly options: StatefulParameterOptions<V>;
|
|
28
|
-
|
|
29
|
-
constructor(options: StatefulParameterOptions<V> = {}) {
|
|
30
|
-
this.options = options
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
abstract refresh(desired: V | null): Promise<V | null>;
|
|
34
|
-
|
|
35
|
-
// TODO: Add an additional parameter here for what has actually changed.
|
|
36
|
-
abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
|
|
37
|
-
abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
|
|
38
|
-
abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
|
|
42
|
-
options: ArrayStatefulParameterOptions<V>;
|
|
43
|
-
|
|
44
|
-
constructor(options: ArrayStatefulParameterOptions<V> = {}) {
|
|
45
|
-
super(options);
|
|
46
|
-
this.options = options;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
|
|
50
|
-
for (const value of valuesToAdd) {
|
|
51
|
-
await this.applyAddItem(value, plan);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void> {
|
|
56
|
-
const options = this.options as ArrayStatefulParameterOptions<V>;
|
|
57
|
-
|
|
58
|
-
const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
|
|
59
|
-
if (options.isElementEqual) {
|
|
60
|
-
return options.isElementEqual(n, p);
|
|
61
|
-
}
|
|
62
|
-
return n === p;
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
|
-
const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
|
|
66
|
-
if (options.isElementEqual) {
|
|
67
|
-
return options.isElementEqual(n, p);
|
|
68
|
-
}
|
|
69
|
-
return n === p;
|
|
70
|
-
}));
|
|
71
|
-
|
|
72
|
-
for (const value of valuesToAdd) {
|
|
73
|
-
await this.applyAddItem(value, plan)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (allowDeletes) {
|
|
77
|
-
for (const value of valuesToRemove) {
|
|
78
|
-
await this.applyRemoveItem(value, plan)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void> {
|
|
84
|
-
for (const value of valuesToRemove) {
|
|
85
|
-
await this.applyRemoveItem(value as V, plan);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
abstract refresh(desired: V[] | null): Promise<V[] | null>;
|
|
90
|
-
abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
|
|
91
|
-
abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
|
|
92
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Transform parameters convert the provided value into
|
|
5
|
-
* other parameters. Transform parameters will not show up
|
|
6
|
-
* in the refresh or the plan. Transform parameters get processed after
|
|
7
|
-
* default values.
|
|
8
|
-
*/
|
|
9
|
-
export abstract class TransformParameter<T extends StringIndexedObject> {
|
|
10
|
-
|
|
11
|
-
abstract transform(value: any): Promise<Partial<T>>
|
|
12
|
-
|
|
13
|
-
}
|
|
File without changes
|