@codifycli/plugin-core 1.0.0-beta1
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/.eslintignore +2 -0
- package/.eslintrc.json +30 -0
- package/.github/workflows/release.yaml +19 -0
- package/.github/workflows/unit-test-ci.yaml +18 -0
- package/.prettierrc.json +1 -0
- package/bin/build.js +189 -0
- package/dist/bin/build.d.ts +1 -0
- package/dist/bin/build.js +80 -0
- package/dist/bin/deploy-plugin.d.ts +2 -0
- package/dist/bin/deploy-plugin.js +8 -0
- package/dist/common/errors.d.ts +8 -0
- package/dist/common/errors.js +24 -0
- package/dist/entities/change-set.d.ts +24 -0
- package/dist/entities/change-set.js +152 -0
- package/dist/entities/errors.d.ts +4 -0
- package/dist/entities/errors.js +7 -0
- package/dist/entities/plan-types.d.ts +25 -0
- package/dist/entities/plan-types.js +1 -0
- package/dist/entities/plan.d.ts +15 -0
- package/dist/entities/plan.js +127 -0
- package/dist/entities/plugin.d.ts +16 -0
- package/dist/entities/plugin.js +80 -0
- package/dist/entities/resource-options.d.ts +31 -0
- package/dist/entities/resource-options.js +76 -0
- package/dist/entities/resource-types.d.ts +11 -0
- package/dist/entities/resource-types.js +1 -0
- package/dist/entities/resource.d.ts +42 -0
- package/dist/entities/resource.js +303 -0
- package/dist/entities/stateful-parameter.d.ts +29 -0
- package/dist/entities/stateful-parameter.js +46 -0
- package/dist/entities/transform-parameter.d.ts +4 -0
- package/dist/entities/transform-parameter.js +2 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +26 -0
- package/dist/messages/handlers.d.ts +14 -0
- package/dist/messages/handlers.js +134 -0
- package/dist/messages/sender.d.ts +11 -0
- package/dist/messages/sender.js +57 -0
- package/dist/plan/change-set.d.ts +53 -0
- package/dist/plan/change-set.js +153 -0
- package/dist/plan/plan-types.d.ts +23 -0
- package/dist/plan/plan-types.js +1 -0
- package/dist/plan/plan.d.ts +66 -0
- package/dist/plan/plan.js +328 -0
- package/dist/plugin/plugin.d.ts +24 -0
- package/dist/plugin/plugin.js +200 -0
- package/dist/pty/background-pty.d.ts +21 -0
- package/dist/pty/background-pty.js +127 -0
- package/dist/pty/index.d.ts +50 -0
- package/dist/pty/index.js +20 -0
- package/dist/pty/promise-queue.d.ts +5 -0
- package/dist/pty/promise-queue.js +26 -0
- package/dist/pty/seqeuntial-pty.d.ts +17 -0
- package/dist/pty/seqeuntial-pty.js +119 -0
- package/dist/pty/vitest.config.d.ts +2 -0
- package/dist/pty/vitest.config.js +11 -0
- package/dist/resource/config-parser.d.ts +11 -0
- package/dist/resource/config-parser.js +21 -0
- package/dist/resource/parsed-resource-settings.d.ts +47 -0
- package/dist/resource/parsed-resource-settings.js +196 -0
- package/dist/resource/resource-controller.d.ts +36 -0
- package/dist/resource/resource-controller.js +402 -0
- package/dist/resource/resource-settings.d.ts +303 -0
- package/dist/resource/resource-settings.js +147 -0
- package/dist/resource/resource.d.ts +144 -0
- package/dist/resource/resource.js +44 -0
- package/dist/resource/stateful-parameter.d.ts +165 -0
- package/dist/resource/stateful-parameter.js +94 -0
- package/dist/scripts/deploy.d.ts +1 -0
- package/dist/scripts/deploy.js +2 -0
- package/dist/stateful-parameter/stateful-parameter-controller.d.ts +21 -0
- package/dist/stateful-parameter/stateful-parameter-controller.js +81 -0
- package/dist/stateful-parameter/stateful-parameter.d.ts +144 -0
- package/dist/stateful-parameter/stateful-parameter.js +43 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +5 -0
- package/dist/utils/codify-spawn.d.ts +29 -0
- package/dist/utils/codify-spawn.js +136 -0
- package/dist/utils/debug.d.ts +2 -0
- package/dist/utils/debug.js +10 -0
- package/dist/utils/file-utils.d.ts +23 -0
- package/dist/utils/file-utils.js +186 -0
- package/dist/utils/functions.d.ts +12 -0
- package/dist/utils/functions.js +74 -0
- package/dist/utils/index.d.ts +46 -0
- package/dist/utils/index.js +271 -0
- package/dist/utils/internal-utils.d.ts +12 -0
- package/dist/utils/internal-utils.js +74 -0
- package/dist/utils/load-resources.d.ts +1 -0
- package/dist/utils/load-resources.js +46 -0
- package/dist/utils/package-json-utils.d.ts +12 -0
- package/dist/utils/package-json-utils.js +34 -0
- package/dist/utils/pty-local-storage.d.ts +2 -0
- package/dist/utils/pty-local-storage.js +2 -0
- package/dist/utils/spawn-2.d.ts +5 -0
- package/dist/utils/spawn-2.js +7 -0
- package/dist/utils/spawn.d.ts +29 -0
- package/dist/utils/spawn.js +124 -0
- package/dist/utils/utils.d.ts +18 -0
- package/dist/utils/utils.js +86 -0
- package/dist/utils/verbosity-level.d.ts +5 -0
- package/dist/utils/verbosity-level.js +9 -0
- package/package.json +59 -0
- package/rollup.config.js +24 -0
- package/src/common/errors.test.ts +43 -0
- package/src/common/errors.ts +31 -0
- package/src/errors.ts +8 -0
- package/src/index.test.ts +6 -0
- package/src/index.ts +30 -0
- package/src/messages/handlers.test.ts +329 -0
- package/src/messages/handlers.ts +181 -0
- package/src/messages/sender.ts +69 -0
- package/src/plan/change-set.test.ts +280 -0
- package/src/plan/change-set.ts +236 -0
- package/src/plan/plan-types.ts +27 -0
- package/src/plan/plan.test.ts +413 -0
- package/src/plan/plan.ts +499 -0
- package/src/plugin/plugin.test.ts +533 -0
- package/src/plugin/plugin.ts +291 -0
- package/src/pty/background-pty.test.ts +69 -0
- package/src/pty/background-pty.ts +154 -0
- package/src/pty/index.test.ts +129 -0
- package/src/pty/index.ts +66 -0
- package/src/pty/promise-queue.ts +33 -0
- package/src/pty/seqeuntial-pty.ts +151 -0
- package/src/pty/sequential-pty.test.ts +194 -0
- package/src/resource/config-parser.ts +42 -0
- package/src/resource/parsed-resource-settings.test.ts +186 -0
- package/src/resource/parsed-resource-settings.ts +307 -0
- package/src/resource/resource-controller-stateful-mode.test.ts +253 -0
- package/src/resource/resource-controller.test.ts +1081 -0
- package/src/resource/resource-controller.ts +563 -0
- package/src/resource/resource-settings.test.ts +1213 -0
- package/src/resource/resource-settings.ts +545 -0
- package/src/resource/resource.ts +157 -0
- package/src/stateful-parameter/stateful-parameter-controller.test.ts +244 -0
- package/src/stateful-parameter/stateful-parameter-controller.ts +111 -0
- package/src/stateful-parameter/stateful-parameter.ts +160 -0
- package/src/utils/debug.ts +11 -0
- package/src/utils/file-utils.test.ts +7 -0
- package/src/utils/file-utils.ts +231 -0
- package/src/utils/functions.ts +103 -0
- package/src/utils/index.ts +340 -0
- package/src/utils/internal-utils.test.ts +52 -0
- package/src/utils/pty-local-storage.ts +3 -0
- package/src/utils/test-utils.test.ts +96 -0
- package/src/utils/verbosity-level.ts +11 -0
- package/tsconfig.json +26 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import { Ajv, ValidateFunction } from 'ajv';
|
|
2
|
+
import cleanDeep from 'clean-deep';
|
|
3
|
+
import {
|
|
4
|
+
ParameterOperation,
|
|
5
|
+
ResourceConfig,
|
|
6
|
+
ResourceJson,
|
|
7
|
+
ResourceOperation,
|
|
8
|
+
StringIndexedObject,
|
|
9
|
+
ValidateResponseData
|
|
10
|
+
} from 'codify-schemas';
|
|
11
|
+
|
|
12
|
+
import { ParameterChange } from '../plan/change-set.js';
|
|
13
|
+
import { Plan } from '../plan/plan.js';
|
|
14
|
+
import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
|
|
15
|
+
import { ConfigParser } from './config-parser.js';
|
|
16
|
+
import { ParsedResourceSettings } from './parsed-resource-settings.js';
|
|
17
|
+
import { RefreshContext, Resource } from './resource.js';
|
|
18
|
+
import { ResourceSettings } from './resource-settings.js';
|
|
19
|
+
|
|
20
|
+
export class ResourceController<T extends StringIndexedObject> {
|
|
21
|
+
readonly resource: Resource<T>
|
|
22
|
+
readonly settings: ResourceSettings<T>
|
|
23
|
+
readonly parsedSettings: ParsedResourceSettings<T>
|
|
24
|
+
|
|
25
|
+
readonly typeId: string;
|
|
26
|
+
readonly dependencies: string[];
|
|
27
|
+
|
|
28
|
+
protected ajv?: Ajv;
|
|
29
|
+
protected schemaValidator?: ValidateFunction;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
resource: Resource<T>,
|
|
33
|
+
) {
|
|
34
|
+
this.resource = resource;
|
|
35
|
+
this.settings = resource.getSettings();
|
|
36
|
+
|
|
37
|
+
this.typeId = this.settings.id;
|
|
38
|
+
this.dependencies = this.settings.dependencies ?? [];
|
|
39
|
+
this.parsedSettings = new ParsedResourceSettings<T>(this.settings);
|
|
40
|
+
|
|
41
|
+
if (this.parsedSettings.schema) {
|
|
42
|
+
this.ajv = new Ajv({
|
|
43
|
+
allErrors: true,
|
|
44
|
+
strict: true,
|
|
45
|
+
strictRequired: false,
|
|
46
|
+
allowUnionTypes: true
|
|
47
|
+
})
|
|
48
|
+
this.schemaValidator = this.ajv.compile(this.parsedSettings.schema);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async initialize(): Promise<void> {
|
|
53
|
+
return this.resource.initialize();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async validate(
|
|
57
|
+
core: ResourceConfig,
|
|
58
|
+
parameters: Partial<T>,
|
|
59
|
+
): Promise<ValidateResponseData['resourceValidations'][0]> {
|
|
60
|
+
const originalParameters = structuredClone(parameters);
|
|
61
|
+
await this.applyTransformations(parameters, undefined, true);
|
|
62
|
+
this.addDefaultValues(parameters);
|
|
63
|
+
|
|
64
|
+
if (this.schemaValidator) {
|
|
65
|
+
// Schema validator uses pre transformation parameters
|
|
66
|
+
const isValid = this.schemaValidator(
|
|
67
|
+
// @ts-expect-error Non esm package
|
|
68
|
+
cleanDeep(originalParameters, {
|
|
69
|
+
nullValues: true,
|
|
70
|
+
undefinedValues: true,
|
|
71
|
+
emptyArrays: false,
|
|
72
|
+
emptyStrings: false,
|
|
73
|
+
emptyObjects: false,
|
|
74
|
+
NaNValues: false
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (!isValid) {
|
|
79
|
+
return {
|
|
80
|
+
isValid: false,
|
|
81
|
+
resourceName: core.name,
|
|
82
|
+
resourceType: core.type,
|
|
83
|
+
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let isValid = true;
|
|
89
|
+
let customValidationErrorMessage;
|
|
90
|
+
try {
|
|
91
|
+
await this.resource.validate(parameters);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
isValid = false;
|
|
94
|
+
customValidationErrorMessage = (error as Error).message;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!isValid) {
|
|
98
|
+
return {
|
|
99
|
+
customValidationErrorMessage,
|
|
100
|
+
isValid: false,
|
|
101
|
+
resourceName: core.name,
|
|
102
|
+
resourceType: core.type,
|
|
103
|
+
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
isValid: true,
|
|
109
|
+
resourceName: core.name,
|
|
110
|
+
resourceType: core.type,
|
|
111
|
+
schemaValidationErrors: [],
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async match(resource: ResourceJson, array: Array<ResourceJson>): Promise<ResourceJson | undefined> {
|
|
116
|
+
if (resource.core.type !== this.typeId) {
|
|
117
|
+
throw new Error(`Unknown type passed into match method: ${resource.core.type} for ${this.typeId}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!this.parsedSettings.allowMultiple) {
|
|
121
|
+
return array.find((r) => r.core.type === resource.core.type)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
const { name, type } = resource.core;
|
|
126
|
+
const parameterMatcher = this.parsedSettings.matcher;
|
|
127
|
+
|
|
128
|
+
for (const resourceToMatch of array) {
|
|
129
|
+
if (type !== resourceToMatch.core.type) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource
|
|
134
|
+
if (name === resourceToMatch.core.name
|
|
135
|
+
&& name
|
|
136
|
+
&& Number.isInteger(Number.parseInt(name, 10))
|
|
137
|
+
) {
|
|
138
|
+
return resourceToMatch;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const originalParams = structuredClone(resource.parameters) as Partial<T>;
|
|
142
|
+
const paramsToMatch = structuredClone(resourceToMatch.parameters) as Partial<T>;
|
|
143
|
+
|
|
144
|
+
this.addDefaultValues(originalParams);
|
|
145
|
+
await this.applyTransformations(originalParams);
|
|
146
|
+
|
|
147
|
+
this.addDefaultValues(paramsToMatch);
|
|
148
|
+
await this.applyTransformations(paramsToMatch);
|
|
149
|
+
|
|
150
|
+
const match = parameterMatcher(originalParams, paramsToMatch);
|
|
151
|
+
if (match) {
|
|
152
|
+
return resourceToMatch;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async plan(
|
|
158
|
+
core: ResourceConfig,
|
|
159
|
+
desired: Partial<T> | null,
|
|
160
|
+
state: Partial<T> | null,
|
|
161
|
+
isStateful = false,
|
|
162
|
+
commandType = 'plan',
|
|
163
|
+
): Promise<Plan<T>> {
|
|
164
|
+
this.validatePlanInputs(core, desired, state, isStateful);
|
|
165
|
+
const context: RefreshContext<T> = {
|
|
166
|
+
commandType: commandType as 'plan' | 'validationPlan',
|
|
167
|
+
isStateful,
|
|
168
|
+
originalDesiredConfig: structuredClone(desired),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
this.addDefaultValues(desired);
|
|
172
|
+
await this.applyTransformations(desired);
|
|
173
|
+
|
|
174
|
+
this.addDefaultValues(state);
|
|
175
|
+
await this.applyTransformations(state);
|
|
176
|
+
|
|
177
|
+
// Parse data from the user supplied config
|
|
178
|
+
const parsedConfig = new ConfigParser(desired, state, this.parsedSettings.statefulParameters)
|
|
179
|
+
const {
|
|
180
|
+
allParameters,
|
|
181
|
+
allNonStatefulParameters,
|
|
182
|
+
allStatefulParameters,
|
|
183
|
+
} = parsedConfig;
|
|
184
|
+
|
|
185
|
+
// Refresh resource parameters. This refreshes the parameters that configure the resource itself
|
|
186
|
+
const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context);
|
|
187
|
+
|
|
188
|
+
// Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
|
|
189
|
+
if (currentArray === null
|
|
190
|
+
|| currentArray === undefined
|
|
191
|
+
|| currentArray.length === 0
|
|
192
|
+
|| currentArray.filter(Boolean).length === 0
|
|
193
|
+
) {
|
|
194
|
+
return Plan.calculate({
|
|
195
|
+
desired,
|
|
196
|
+
currentArray,
|
|
197
|
+
state,
|
|
198
|
+
core,
|
|
199
|
+
settings: this.parsedSettings,
|
|
200
|
+
isStateful,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Refresh stateful parameters. These parameters have state external to the resource. Each variation of the
|
|
205
|
+
// current parameters (each array element) is passed into the stateful parameter refresh.
|
|
206
|
+
const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentArray, allParameters);
|
|
207
|
+
|
|
208
|
+
return Plan.calculate({
|
|
209
|
+
desired,
|
|
210
|
+
currentArray: currentArray.map((c, idx) => ({ ...c, ...statefulCurrentParameters[idx] })),
|
|
211
|
+
state,
|
|
212
|
+
core,
|
|
213
|
+
settings: this.parsedSettings,
|
|
214
|
+
isStateful
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async planDestroy(
|
|
219
|
+
core: ResourceConfig,
|
|
220
|
+
parameters: Partial<T>
|
|
221
|
+
): Promise<Plan<T>> {
|
|
222
|
+
this.addDefaultValues(parameters);
|
|
223
|
+
await this.applyTransformations(parameters);
|
|
224
|
+
|
|
225
|
+
// Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
|
|
226
|
+
const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys
|
|
227
|
+
? {
|
|
228
|
+
...Object.fromEntries(
|
|
229
|
+
this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])
|
|
230
|
+
),
|
|
231
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
232
|
+
...parameters,
|
|
233
|
+
}
|
|
234
|
+
: {
|
|
235
|
+
...Object.fromEntries(
|
|
236
|
+
this.getAllParameterKeys().map((k) => [k, null])
|
|
237
|
+
),
|
|
238
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
239
|
+
...parameters,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return this.plan(core, null, parametersToRefresh, true);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async apply(plan: Plan<T>): Promise<void> {
|
|
246
|
+
if (plan.getResourceType() !== this.typeId) {
|
|
247
|
+
throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
switch (plan.changeSet.operation) {
|
|
251
|
+
case ResourceOperation.CREATE: {
|
|
252
|
+
return this.applyCreate(plan);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
case ResourceOperation.MODIFY: {
|
|
256
|
+
return this.applyModify(plan);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case ResourceOperation.RECREATE: {
|
|
260
|
+
await this.applyDestroy(plan);
|
|
261
|
+
return this.applyCreate(plan);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case ResourceOperation.DESTROY: {
|
|
265
|
+
return this.applyDestroy(plan);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async import(
|
|
271
|
+
core: ResourceConfig,
|
|
272
|
+
parameters: Partial<T>,
|
|
273
|
+
autoSearchAll = false,
|
|
274
|
+
): Promise<Array<ResourceJson> | null> {
|
|
275
|
+
if (this.settings.importAndDestroy?.preventImport) {
|
|
276
|
+
throw new Error(`Type: ${this.typeId} cannot be imported`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const context: RefreshContext<T> = {
|
|
280
|
+
commandType: 'import',
|
|
281
|
+
isStateful: true,
|
|
282
|
+
originalDesiredConfig: structuredClone(parameters),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Auto search means that no required parameters will be provided. We will try to generate it ourselves or return an
|
|
286
|
+
// empty array if they can't be.
|
|
287
|
+
if (autoSearchAll && this.settings.allowMultiple) {
|
|
288
|
+
if (this.settings.allowMultiple === true || !this.settings.allowMultiple.findAllParameters?.()) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const parametersToImport = await this.settings.allowMultiple.findAllParameters?.();
|
|
293
|
+
const results = await Promise.all(parametersToImport.map((p) =>
|
|
294
|
+
this.import(core, p).catch(() => null))
|
|
295
|
+
);
|
|
296
|
+
return results.filter(Boolean).flat() as ResourceJson[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.addDefaultValues(parameters);
|
|
300
|
+
await this.applyTransformations(parameters);
|
|
301
|
+
|
|
302
|
+
// Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
|
|
303
|
+
const parametersToRefresh = this.getParametersToRefreshForImport(parameters, context);
|
|
304
|
+
|
|
305
|
+
// Parse data from the user supplied config
|
|
306
|
+
const parsedConfig = new ConfigParser(parametersToRefresh, null, this.parsedSettings.statefulParameters)
|
|
307
|
+
const {
|
|
308
|
+
allParameters,
|
|
309
|
+
allNonStatefulParameters,
|
|
310
|
+
allStatefulParameters,
|
|
311
|
+
} = parsedConfig;
|
|
312
|
+
|
|
313
|
+
const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context);
|
|
314
|
+
|
|
315
|
+
if (currentParametersArray === null
|
|
316
|
+
|| currentParametersArray === undefined
|
|
317
|
+
|| currentParametersArray.filter(Boolean).length === 0
|
|
318
|
+
) {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray, allParameters);
|
|
323
|
+
const resultParametersArray = currentParametersArray
|
|
324
|
+
?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] }))
|
|
325
|
+
|
|
326
|
+
for (const result of resultParametersArray) {
|
|
327
|
+
await this.applyTransformations(result, { original: context.originalDesiredConfig });
|
|
328
|
+
this.removeDefaultValues(result, parameters);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return resultParametersArray?.map((r) => ({ core, parameters: r }))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private async applyCreate(plan: Plan<T>): Promise<void> {
|
|
335
|
+
await this.resource.create(plan as CreatePlan<T>);
|
|
336
|
+
|
|
337
|
+
const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
|
|
338
|
+
|
|
339
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
340
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
|
|
341
|
+
await statefulParameter.add(parameterChange.newValue, plan);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async applyModify(plan: Plan<T>): Promise<void> {
|
|
346
|
+
const parameterChanges = plan
|
|
347
|
+
.changeSet
|
|
348
|
+
.parameterChanges
|
|
349
|
+
.filter((c: ParameterChange<T>) => c.operation !== ParameterOperation.NOOP);
|
|
350
|
+
|
|
351
|
+
const statelessParameterChanges = parameterChanges
|
|
352
|
+
.filter((pc: ParameterChange<T>) => !this.parsedSettings.statefulParameters.has(pc.name))
|
|
353
|
+
|
|
354
|
+
for (const pc of statelessParameterChanges) {
|
|
355
|
+
await this.resource.modify(pc, plan as ModifyPlan<T>);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
|
|
359
|
+
|
|
360
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
361
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
|
|
362
|
+
|
|
363
|
+
switch (parameterChange.operation) {
|
|
364
|
+
case ParameterOperation.ADD: {
|
|
365
|
+
await statefulParameter.add(parameterChange.newValue, plan);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case ParameterOperation.MODIFY: {
|
|
370
|
+
await statefulParameter.modify(parameterChange.newValue, parameterChange.previousValue, plan);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case ParameterOperation.REMOVE: {
|
|
375
|
+
await statefulParameter.remove(parameterChange.previousValue, plan);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private async applyDestroy(plan: Plan<T>): Promise<void> {
|
|
383
|
+
// If this option is set (defaults to false), then stateful parameters need to be destroyed
|
|
384
|
+
// as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
|
|
385
|
+
if (this.settings.removeStatefulParametersBeforeDestroy) {
|
|
386
|
+
const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
|
|
387
|
+
|
|
388
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
389
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
|
|
390
|
+
await statefulParameter.remove(parameterChange.previousValue, plan);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
await this.resource.destroy(plan as DestroyPlan<T>);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private validateRefreshResults(refresh: Array<Partial<T>> | null) {
|
|
398
|
+
if (!refresh) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!this.settings.allowMultiple && refresh.length > 1) {
|
|
403
|
+
throw new Error(`Resource: ${this.settings.id}. Allow multiple was set to false but multiple refresh results were returned.
|
|
404
|
+
|
|
405
|
+
${JSON.stringify(refresh, null, 2)}
|
|
406
|
+
`)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private async applyTransformations(config: Partial<T> | null, reverse?: {
|
|
411
|
+
original: Partial<T> | null
|
|
412
|
+
}, skipConfigTransformation = false): Promise<void> {
|
|
413
|
+
if (!config) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
for (const [key, inputTransformation] of Object.entries(this.parsedSettings.inputTransformations)) {
|
|
418
|
+
if (config[key] === undefined || !inputTransformation) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
(config as Record<string, unknown>)[key] = reverse
|
|
423
|
+
? await inputTransformation.from(config[key], reverse.original?.[key])
|
|
424
|
+
: await inputTransformation.to(config[key]);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (this.settings.transformation && !skipConfigTransformation) {
|
|
428
|
+
const transformed = reverse
|
|
429
|
+
? await this.settings.transformation.from({ ...config }, reverse.original)
|
|
430
|
+
: await this.settings.transformation.to({ ...config })
|
|
431
|
+
|
|
432
|
+
Object.keys(config).forEach((k) => delete config[k])
|
|
433
|
+
Object.assign(config, transformed);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private addDefaultValues(config: Partial<T> | null): void {
|
|
438
|
+
if (!config) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const [key, defaultValue] of Object.entries(this.parsedSettings.defaultValues)) {
|
|
443
|
+
if (defaultValue !== undefined && (config[key] === undefined || config[key] === null)) {
|
|
444
|
+
(config as Record<string, unknown>)[key] = defaultValue;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private removeDefaultValues(newConfig: Partial<T> | null, originalConfig: Partial<T>): void {
|
|
450
|
+
if (!newConfig) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const [key, defaultValue] of Object.entries(this.parsedSettings.defaultValues)) {
|
|
455
|
+
if (defaultValue !== undefined && (newConfig[key] === defaultValue || originalConfig[key] === undefined || originalConfig[key] === null)) {
|
|
456
|
+
delete newConfig[key];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private async refreshNonStatefulParameters(resourceParameters: Partial<T>, context: RefreshContext<T>): Promise<Array<Partial<T>> | null> {
|
|
463
|
+
const result = await this.resource.refresh(resourceParameters, context);
|
|
464
|
+
|
|
465
|
+
const currentParametersArray = Array.isArray(result) || result === null
|
|
466
|
+
? result
|
|
467
|
+
: [result]
|
|
468
|
+
|
|
469
|
+
this.validateRefreshResults(currentParametersArray);
|
|
470
|
+
return currentParametersArray;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Refresh stateful parameters
|
|
474
|
+
// This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
|
|
475
|
+
private async refreshStatefulParameters(
|
|
476
|
+
statefulParametersConfig: Partial<T>,
|
|
477
|
+
currentArray: Array<Partial<T>>,
|
|
478
|
+
allParameters: Partial<T>
|
|
479
|
+
): Promise<Array<Partial<T>>> {
|
|
480
|
+
const result: Array<Partial<T>> = Array.from({ length: currentArray.length }, () => ({}))
|
|
481
|
+
const sortedEntries = Object.entries(statefulParametersConfig)
|
|
482
|
+
.sort(
|
|
483
|
+
([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)!
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
for (const [idx, refreshedParams] of currentArray.entries()) {
|
|
487
|
+
await Promise.all(sortedEntries.map(async ([key, desiredValue]) => {
|
|
488
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(key);
|
|
489
|
+
if (!statefulParameter) {
|
|
490
|
+
throw new Error(`Stateful parameter ${key} was not found`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
(result[idx][key] as T[keyof T] | null) = await statefulParameter.refresh(desiredValue ?? null, { ...allParameters, ...refreshedParams })
|
|
494
|
+
}))
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private validatePlanInputs(
|
|
501
|
+
core: ResourceConfig,
|
|
502
|
+
desired: Partial<T> | null,
|
|
503
|
+
current: Partial<T> | null,
|
|
504
|
+
isStateful: boolean,
|
|
505
|
+
) {
|
|
506
|
+
if (!core || !core.type) {
|
|
507
|
+
throw new Error('Core parameters type must be defined');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!desired && !current) {
|
|
511
|
+
throw new Error('Desired config and current config cannot both be missing')
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (!isStateful && !desired) {
|
|
515
|
+
throw new Error('Desired config must be provided in non-stateful mode')
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private getSortedStatefulParameterChanges(parameterChanges: ParameterChange<T>[]) {
|
|
520
|
+
return parameterChanges
|
|
521
|
+
.filter((pc: ParameterChange<T>) => this.parsedSettings.statefulParameters.has(pc.name))
|
|
522
|
+
.sort((a, b) =>
|
|
523
|
+
this.parsedSettings.statefulParameterOrder.get(a.name)! - this.parsedSettings.statefulParameterOrder.get(b.name)!
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private getAllParameterKeys(): string[] {
|
|
528
|
+
return this.parsedSettings.schema
|
|
529
|
+
? Object.keys((this.parsedSettings.schema as any)?.properties)
|
|
530
|
+
: Object.keys(this.parsedSettings.parameterSettings);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private getParametersToRefreshForImport(parameters: Partial<T>, context: RefreshContext<T>): Partial<T> {
|
|
534
|
+
if (this.settings.importAndDestroy?.refreshMapper) {
|
|
535
|
+
return this.settings.importAndDestroy?.refreshMapper(parameters, context);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return this.settings.importAndDestroy?.refreshKeys
|
|
539
|
+
? {
|
|
540
|
+
...Object.fromEntries(
|
|
541
|
+
this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])
|
|
542
|
+
),
|
|
543
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
544
|
+
...parameters,
|
|
545
|
+
...(Object.fromEntries( // If a default value was used, but it was also declared in the defaultRefreshValues, prefer the defaultRefreshValue instead
|
|
546
|
+
Object.entries(parameters).filter(([k, v]) =>
|
|
547
|
+
this.parsedSettings.defaultValues[k] !== undefined
|
|
548
|
+
&& v === this.parsedSettings.defaultValues[k]
|
|
549
|
+
&& context.originalDesiredConfig?.[k] === undefined
|
|
550
|
+
&& this.settings.importAndDestroy?.defaultRefreshValues?.[k] !== undefined
|
|
551
|
+
).map(([k]) => [k, this.settings.importAndDestroy!.defaultRefreshValues![k]])
|
|
552
|
+
))
|
|
553
|
+
}
|
|
554
|
+
: {
|
|
555
|
+
...Object.fromEntries(
|
|
556
|
+
this.getAllParameterKeys().map((k) => [k, null])
|
|
557
|
+
),
|
|
558
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
559
|
+
...parameters,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|