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