codify-plugin-lib 1.0.76 → 1.0.78

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.
Files changed (74) hide show
  1. package/.eslintrc.json +11 -4
  2. package/.github/workflows/release.yaml +19 -0
  3. package/.github/workflows/unit-test-ci.yaml +19 -0
  4. package/dist/errors.d.ts +4 -0
  5. package/dist/errors.js +7 -0
  6. package/dist/index.d.ts +10 -10
  7. package/dist/index.js +9 -9
  8. package/dist/messages/handlers.d.ts +1 -1
  9. package/dist/messages/handlers.js +2 -1
  10. package/dist/plan/change-set.d.ts +47 -0
  11. package/dist/plan/change-set.js +156 -0
  12. package/dist/plan/plan-types.d.ts +23 -0
  13. package/dist/plan/plan-types.js +1 -0
  14. package/dist/plan/plan.d.ts +59 -0
  15. package/dist/plan/plan.js +228 -0
  16. package/dist/plugin/plugin.d.ts +17 -0
  17. package/dist/plugin/plugin.js +83 -0
  18. package/dist/resource/config-parser.d.ts +14 -0
  19. package/dist/resource/config-parser.js +48 -0
  20. package/dist/resource/parsed-resource-settings.d.ts +26 -0
  21. package/dist/resource/parsed-resource-settings.js +126 -0
  22. package/dist/resource/resource-controller.d.ts +30 -0
  23. package/dist/resource/resource-controller.js +249 -0
  24. package/dist/resource/resource-settings.d.ts +149 -0
  25. package/dist/resource/resource-settings.js +9 -0
  26. package/dist/resource/resource.d.ts +137 -0
  27. package/dist/resource/resource.js +44 -0
  28. package/dist/resource/stateful-parameter.d.ts +164 -0
  29. package/dist/resource/stateful-parameter.js +94 -0
  30. package/dist/utils/spawn-2.d.ts +5 -0
  31. package/dist/utils/spawn-2.js +7 -0
  32. package/dist/utils/spawn.d.ts +29 -0
  33. package/dist/utils/spawn.js +124 -0
  34. package/dist/utils/utils.d.ts +19 -3
  35. package/dist/utils/utils.js +52 -3
  36. package/package.json +5 -3
  37. package/src/index.ts +10 -11
  38. package/src/messages/handlers.test.ts +10 -37
  39. package/src/messages/handlers.ts +2 -2
  40. package/src/plan/change-set.test.ts +220 -0
  41. package/src/plan/change-set.ts +235 -0
  42. package/src/plan/plan-types.ts +27 -0
  43. package/src/{entities → plan}/plan.test.ts +35 -29
  44. package/src/plan/plan.ts +353 -0
  45. package/src/{entities → plugin}/plugin.test.ts +14 -13
  46. package/src/{entities → plugin}/plugin.ts +28 -24
  47. package/src/resource/config-parser.ts +77 -0
  48. package/src/{entities/resource-options.test.ts → resource/parsed-resource-settings.test.ts} +8 -7
  49. package/src/resource/parsed-resource-settings.ts +179 -0
  50. package/src/{entities/resource-stateful-mode.test.ts → resource/resource-controller-stateful-mode.test.ts} +36 -39
  51. package/src/{entities/resource.test.ts → resource/resource-controller.test.ts} +116 -176
  52. package/src/resource/resource-controller.ts +343 -0
  53. package/src/resource/resource-settings.test.ts +494 -0
  54. package/src/resource/resource-settings.ts +192 -0
  55. package/src/resource/resource.ts +149 -0
  56. package/src/resource/stateful-parameter.test.ts +93 -0
  57. package/src/resource/stateful-parameter.ts +217 -0
  58. package/src/utils/test-utils.test.ts +87 -0
  59. package/src/utils/utils.test.ts +2 -2
  60. package/src/utils/utils.ts +51 -5
  61. package/tsconfig.json +0 -1
  62. package/vitest.config.ts +10 -0
  63. package/src/entities/change-set.test.ts +0 -155
  64. package/src/entities/change-set.ts +0 -244
  65. package/src/entities/plan-types.ts +0 -44
  66. package/src/entities/plan.ts +0 -178
  67. package/src/entities/resource-options.ts +0 -155
  68. package/src/entities/resource-parameters.test.ts +0 -604
  69. package/src/entities/resource-types.ts +0 -31
  70. package/src/entities/resource.ts +0 -470
  71. package/src/entities/stateful-parameter.test.ts +0 -114
  72. package/src/entities/stateful-parameter.ts +0 -92
  73. package/src/entities/transform-parameter.ts +0 -13
  74. /package/src/{entities/errors.ts → errors.ts} +0 -0
@@ -0,0 +1,343 @@
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 { ParameterChange } from '../plan/change-set.js';
11
+ import { Plan } from '../plan/plan.js';
12
+ import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
13
+ import { splitUserConfig } from '../utils/utils.js';
14
+ import { ConfigParser } from './config-parser.js';
15
+ import { ParsedResourceSettings } from './parsed-resource-settings.js';
16
+ import { Resource } from './resource.js';
17
+ import { ResourceSettings } from './resource-settings.js';
18
+
19
+ export class ResourceController<T extends StringIndexedObject> {
20
+ readonly resource: Resource<T>
21
+ readonly settings: ResourceSettings<T>
22
+ readonly parsedSettings: ParsedResourceSettings<T>
23
+
24
+ readonly typeId: string;
25
+ readonly dependencies: string[];
26
+
27
+ protected ajv?: Ajv;
28
+ protected schemaValidator?: ValidateFunction;
29
+
30
+ constructor(
31
+ resource: Resource<T>,
32
+ ) {
33
+ this.resource = resource;
34
+ this.settings = resource.getSettings();
35
+
36
+ this.typeId = this.settings.id;
37
+ this.dependencies = this.settings.dependencies ?? [];
38
+
39
+ if (this.settings.schema) {
40
+ this.ajv = new Ajv({
41
+ allErrors: true,
42
+ strict: true,
43
+ strictRequired: false,
44
+ })
45
+ this.schemaValidator = this.ajv.compile(this.settings.schema);
46
+ }
47
+
48
+ this.parsedSettings = new ParsedResourceSettings<T>(this.settings);
49
+ }
50
+
51
+ async initialize(): Promise<void> {
52
+ return this.resource.initialize();
53
+ }
54
+
55
+ async validate(
56
+ parameters: Partial<T>,
57
+ resourceMetaData: ResourceConfig
58
+ ): Promise<ValidateResponseData['resourceValidations'][0]> {
59
+ if (this.schemaValidator) {
60
+ const isValid = this.schemaValidator(parameters);
61
+
62
+ if (!isValid) {
63
+ return {
64
+ isValid: false,
65
+ resourceName: resourceMetaData.name,
66
+ resourceType: resourceMetaData.type,
67
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
68
+ }
69
+ }
70
+ }
71
+
72
+ let isValid = true;
73
+ let customValidationErrorMessage;
74
+ try {
75
+ await this.resource.validate(parameters);
76
+ } catch (error) {
77
+ isValid = false;
78
+ customValidationErrorMessage = (error as Error).message;
79
+ }
80
+
81
+ if (!isValid) {
82
+ return {
83
+ customValidationErrorMessage,
84
+ isValid: false,
85
+ resourceName: resourceMetaData.name,
86
+ resourceType: resourceMetaData.type,
87
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
88
+ }
89
+ }
90
+
91
+ return {
92
+ isValid: true,
93
+ resourceName: resourceMetaData.name,
94
+ resourceType: resourceMetaData.type,
95
+ schemaValidationErrors: [],
96
+ }
97
+ }
98
+
99
+ async plan(
100
+ desiredConfig: Partial<T> & ResourceConfig | null,
101
+ stateConfig: Partial<T> & ResourceConfig | null = null,
102
+ statefulMode = false,
103
+ ): Promise<Plan<T>> {
104
+ this.validatePlanInputs(desiredConfig, stateConfig, statefulMode);
105
+
106
+ this.addDefaultValues(desiredConfig);
107
+ await this.applyTransformParameters(desiredConfig);
108
+
109
+ // Parse data from the user supplied config
110
+ const parsedConfig = new ConfigParser(desiredConfig, stateConfig, this.parsedSettings.statefulParameters)
111
+ const {
112
+ coreParameters,
113
+ desiredParameters,
114
+ stateParameters,
115
+ allNonStatefulParameters,
116
+ allStatefulParameters,
117
+ } = parsedConfig;
118
+
119
+ // Refresh resource parameters. This refreshes the parameters that configure the resource itself
120
+ const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
121
+
122
+ // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
123
+ if (currentParametersArray === null
124
+ || currentParametersArray === undefined
125
+ || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
126
+ || currentParametersArray.length === 0
127
+ || currentParametersArray.filter(Boolean).length === 0
128
+ ) {
129
+ return Plan.calculate({
130
+ desiredParameters,
131
+ currentParametersArray,
132
+ stateParameters,
133
+ coreParameters,
134
+ settings: this.parsedSettings,
135
+ statefulMode,
136
+ });
137
+ }
138
+
139
+ // Refresh stateful parameters. These parameters have state external to the resource. allowMultiple
140
+ // does not work together with stateful parameters
141
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters);
142
+
143
+ return Plan.calculate({
144
+ desiredParameters,
145
+ currentParametersArray: [{ ...currentParametersArray[0], ...statefulCurrentParameters }] as Partial<T>[],
146
+ stateParameters,
147
+ coreParameters,
148
+ settings: this.parsedSettings,
149
+ statefulMode
150
+ })
151
+ }
152
+
153
+ async apply(plan: Plan<T>): Promise<void> {
154
+ if (plan.getResourceType() !== this.typeId) {
155
+ throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
156
+ }
157
+
158
+ switch (plan.changeSet.operation) {
159
+ case ResourceOperation.CREATE: {
160
+ return this.applyCreate(plan);
161
+ }
162
+
163
+ case ResourceOperation.MODIFY: {
164
+ return this.applyModify(plan);
165
+ }
166
+
167
+ case ResourceOperation.RECREATE: {
168
+ await this.applyDestroy(plan);
169
+ return this.applyCreate(plan);
170
+ }
171
+
172
+ case ResourceOperation.DESTROY: {
173
+ return this.applyDestroy(plan);
174
+ }
175
+ }
176
+ }
177
+
178
+ private async applyCreate(plan: Plan<T>): Promise<void> {
179
+ await this.resource.create(plan as CreatePlan<T>);
180
+
181
+ const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
182
+
183
+ for (const parameterChange of statefulParameterChanges) {
184
+ const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
185
+ await statefulParameter.add(parameterChange.newValue, plan);
186
+ }
187
+ }
188
+
189
+ private async applyModify(plan: Plan<T>): Promise<void> {
190
+ const parameterChanges = plan
191
+ .changeSet
192
+ .parameterChanges
193
+ .filter((c: ParameterChange<T>) => c.operation !== ParameterOperation.NOOP);
194
+
195
+ const statelessParameterChanges = parameterChanges
196
+ .filter((pc: ParameterChange<T>) => !this.parsedSettings.statefulParameters.has(pc.name))
197
+
198
+ for (const pc of statelessParameterChanges) {
199
+ await this.resource.modify(pc, plan as ModifyPlan<T>);
200
+ }
201
+
202
+ const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
203
+
204
+ for (const parameterChange of statefulParameterChanges) {
205
+ const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
206
+
207
+ switch (parameterChange.operation) {
208
+ case ParameterOperation.ADD: {
209
+ await statefulParameter.add(parameterChange.newValue, plan);
210
+ break;
211
+ }
212
+
213
+ case ParameterOperation.MODIFY: {
214
+ await statefulParameter.modify(parameterChange.newValue, parameterChange.previousValue, plan);
215
+ break;
216
+ }
217
+
218
+ case ParameterOperation.REMOVE: {
219
+ await statefulParameter.remove(parameterChange.previousValue, plan);
220
+ break;
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ private async applyDestroy(plan: Plan<T>): Promise<void> {
227
+ // If this option is set (defaults to false), then stateful parameters need to be destroyed
228
+ // as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
229
+ if (this.settings.removeStatefulParametersBeforeDestroy) {
230
+ const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
231
+
232
+ for (const parameterChange of statefulParameterChanges) {
233
+ const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
234
+ await statefulParameter.remove(parameterChange.previousValue, plan);
235
+ }
236
+ }
237
+
238
+ await this.resource.destroy(plan as DestroyPlan<T>);
239
+ }
240
+
241
+ private validateRefreshResults(refresh: Array<Partial<T>> | null) {
242
+ if (!refresh) {
243
+ return;
244
+ }
245
+
246
+ if (!this.settings.allowMultiple && refresh.length > 1) {
247
+ throw new Error(`Resource: ${this.settings.id}. Allow multiple was set to false but multiple refresh results were returned.
248
+
249
+ ${JSON.stringify(refresh, null, 2)}
250
+ `)
251
+ }
252
+ }
253
+
254
+ private async applyTransformParameters(desired: Partial<T> & ResourceConfig | null): Promise<void> {
255
+ if (!desired) {
256
+ return;
257
+ }
258
+
259
+ for (const [key, inputTransformation] of Object.entries(this.parsedSettings.inputTransformations)) {
260
+ if (desired[key] === undefined || !inputTransformation) {
261
+ continue;
262
+ }
263
+
264
+ (desired as Record<string, unknown>)[key] = await inputTransformation(desired[key]);
265
+ }
266
+
267
+ if (this.settings.inputTransformation) {
268
+ const { parameters, coreParameters } = splitUserConfig(desired);
269
+
270
+ const transformed = await this.settings.inputTransformation(parameters)
271
+ Object.keys(desired).forEach((k) => delete desired[k])
272
+ Object.assign(desired, transformed, coreParameters);
273
+ }
274
+ }
275
+
276
+ private addDefaultValues(desired: Partial<T> | null): void {
277
+ if (!desired) {
278
+ return;
279
+ }
280
+
281
+ for (const [key, defaultValue] of Object.entries(this.parsedSettings.defaultValues)) {
282
+ if (defaultValue !== undefined && (desired[key] === undefined || desired[key] === null)) {
283
+ (desired as Record<string, unknown>)[key] = defaultValue;
284
+ }
285
+ }
286
+ }
287
+
288
+ private async refreshNonStatefulParameters(resourceParameters: Partial<T>): Promise<Array<Partial<T>> | null> {
289
+ const result = await this.resource.refresh(resourceParameters);
290
+
291
+ const currentParametersArray = Array.isArray(result) || result === null
292
+ ? result
293
+ : [result]
294
+
295
+ this.validateRefreshResults(currentParametersArray);
296
+ return currentParametersArray;
297
+ }
298
+
299
+ // Refresh stateful parameters
300
+ // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
301
+ private async refreshStatefulParameters(statefulParametersConfig: Partial<T>): Promise<Partial<T>> {
302
+ const result: Partial<T> = {}
303
+ const sortedEntries = Object.entries(statefulParametersConfig)
304
+ .sort(
305
+ ([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)!
306
+ )
307
+
308
+ for (const [key, desiredValue] of sortedEntries) {
309
+ const statefulParameter = this.parsedSettings.statefulParameters.get(key);
310
+ if (!statefulParameter) {
311
+ throw new Error(`Stateful parameter ${key} was not found`);
312
+ }
313
+
314
+ (result as Record<string, unknown>)[key] = await statefulParameter.refresh(desiredValue ?? null)
315
+ }
316
+
317
+ return result;
318
+ }
319
+
320
+ private validatePlanInputs(
321
+ desired: Partial<T> & ResourceConfig | null,
322
+ current: Partial<T> & ResourceConfig | null,
323
+ statefulMode: boolean,
324
+ ) {
325
+ if (!desired && !current) {
326
+ throw new Error('Desired config and current config cannot both be missing')
327
+ }
328
+
329
+ if (!statefulMode && !desired) {
330
+ throw new Error('Desired config must be provided in non-stateful mode')
331
+ }
332
+ }
333
+
334
+ private getSortedStatefulParameterChanges(parameterChanges: ParameterChange<T>[]) {
335
+ return parameterChanges
336
+ .filter((pc: ParameterChange<T>) => this.parsedSettings.statefulParameters.has(pc.name))
337
+ .sort((a, b) =>
338
+ this.parsedSettings.statefulParameterOrder.get(a.name)! - this.parsedSettings.statefulParameterOrder.get(b.name)!
339
+ )
340
+ }
341
+
342
+ }
343
+