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
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Plan } from '../plan/plan.js';
|
|
3
|
+
import { spy } from 'sinon';
|
|
4
|
+
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
5
|
+
import {
|
|
6
|
+
TestArrayStatefulParameter,
|
|
7
|
+
TestConfig,
|
|
8
|
+
testPlan,
|
|
9
|
+
TestResource,
|
|
10
|
+
TestStatefulParameter
|
|
11
|
+
} from '../utils/test-utils.test.js';
|
|
12
|
+
import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
|
|
13
|
+
import { ResourceController } from './resource-controller.js';
|
|
14
|
+
|
|
15
|
+
describe('Resource parameter tests', () => {
|
|
16
|
+
it('Generates a resource plan that includes stateful parameters (create)', async () => {
|
|
17
|
+
const statefulParameter = spy(new class extends TestStatefulParameter {
|
|
18
|
+
async refresh(): Promise<string | null> {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const resource = new class extends TestResource {
|
|
24
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
25
|
+
return {
|
|
26
|
+
id: 'type',
|
|
27
|
+
parameterSettings: {
|
|
28
|
+
propA: { type: 'stateful', definition: statefulParameter }
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async refresh(): Promise<any> {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const controller = new ResourceController(resource);
|
|
39
|
+
const plan = await controller.plan({
|
|
40
|
+
type: 'type',
|
|
41
|
+
propA: 'a',
|
|
42
|
+
propB: 10
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(statefulParameter.refresh.notCalled).to.be.true;
|
|
46
|
+
expect(plan.currentConfig).to.be.null;
|
|
47
|
+
expect(plan.desiredConfig).toMatchObject({
|
|
48
|
+
type: 'type',
|
|
49
|
+
propA: 'a',
|
|
50
|
+
propB: 10
|
|
51
|
+
})
|
|
52
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('supports the creation of stateful parameters', async () => {
|
|
56
|
+
|
|
57
|
+
const statefulParameter = new class extends TestStatefulParameter {
|
|
58
|
+
async refresh(): Promise<string | null> {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
64
|
+
|
|
65
|
+
const resource = new class extends TestResource {
|
|
66
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
67
|
+
return {
|
|
68
|
+
id: 'type',
|
|
69
|
+
parameterSettings: {
|
|
70
|
+
propA: { type: 'stateful', definition: statefulParameterSpy }
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const controller = new ResourceController(resource);
|
|
77
|
+
const resourceSpy = spy(resource);
|
|
78
|
+
|
|
79
|
+
await controller.apply(
|
|
80
|
+
testPlan<TestConfig>({
|
|
81
|
+
desired: { propA: 'a', propB: 0, propC: 'c' }
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(statefulParameterSpy.add.calledOnce).to.be.true;
|
|
86
|
+
expect(resourceSpy.create.calledOnce).to.be.true;
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('supports the modification of stateful parameters', async () => {
|
|
90
|
+
const statefulParameter = new class extends TestStatefulParameter {
|
|
91
|
+
async refresh(): Promise<string | null> {
|
|
92
|
+
return 'b';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
97
|
+
|
|
98
|
+
const resource = new class extends TestResource {
|
|
99
|
+
|
|
100
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
101
|
+
return {
|
|
102
|
+
id: 'type',
|
|
103
|
+
parameterSettings: {
|
|
104
|
+
propA: { type: 'stateful', definition: statefulParameterSpy },
|
|
105
|
+
propB: { canModify: true },
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
111
|
+
return { propB: -1, propC: 'b' }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const controller = new ResourceController(resource);
|
|
116
|
+
|
|
117
|
+
const plan = await controller.plan({ type: 'type', propA: 'a', propB: 0, propC: 'b' })
|
|
118
|
+
|
|
119
|
+
const resourceSpy = spy(resource);
|
|
120
|
+
await controller.apply(plan);
|
|
121
|
+
|
|
122
|
+
expect(statefulParameterSpy.modify.calledOnce).to.be.true;
|
|
123
|
+
expect(resourceSpy.modify.calledOnce).to.be.true;
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('Allows stateful parameters to have default values', async () => {
|
|
127
|
+
const statefulParameter = spy(new class extends TestStatefulParameter {
|
|
128
|
+
getSettings(): ParameterSetting {
|
|
129
|
+
return {
|
|
130
|
+
default: 'abc'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async refresh(): Promise<string | null> {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const resource = new class extends TestResource {
|
|
140
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
141
|
+
return {
|
|
142
|
+
id: 'type',
|
|
143
|
+
parameterSettings: {
|
|
144
|
+
propA: { type: 'stateful', definition: statefulParameter }
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async refresh(): Promise<any> {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const controller = new ResourceController(resource);
|
|
155
|
+
const plan = await controller.plan({
|
|
156
|
+
type: 'type',
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
expect(statefulParameter.refresh.notCalled).to.be.true;
|
|
160
|
+
expect(plan.currentConfig).to.be.null;
|
|
161
|
+
expect(plan.desiredConfig).toMatchObject({
|
|
162
|
+
type: 'type',
|
|
163
|
+
propA: 'abc',
|
|
164
|
+
})
|
|
165
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('Filters array results in stateless mode to prevent modify from being called', async () => {
|
|
169
|
+
const statefulParameter = new class extends TestStatefulParameter {
|
|
170
|
+
getSettings(): ParameterSetting {
|
|
171
|
+
return { type: 'array' }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async refresh(): Promise<any | null> {
|
|
175
|
+
return ['a', 'b', 'c', 'd']
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
180
|
+
|
|
181
|
+
const resource = new class extends TestResource {
|
|
182
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
183
|
+
return {
|
|
184
|
+
id: 'type',
|
|
185
|
+
parameterSettings: {
|
|
186
|
+
propA: { type: 'stateful', definition: statefulParameterSpy },
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
192
|
+
return {};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const controller = new ResourceController(resource);
|
|
197
|
+
const plan = await controller.plan({ type: 'type', propA: ['a', 'b'] } as any)
|
|
198
|
+
|
|
199
|
+
expect(plan).toMatchObject({
|
|
200
|
+
changeSet: {
|
|
201
|
+
operation: ResourceOperation.NOOP,
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('Filters array results in stateless mode to prevent modify from being called 2', async () => {
|
|
207
|
+
const statefulParameter = new class extends TestStatefulParameter {
|
|
208
|
+
async refresh(): Promise<any | null> {
|
|
209
|
+
return ['a', 'b']
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
214
|
+
|
|
215
|
+
const resource = new class extends TestResource {
|
|
216
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
217
|
+
return {
|
|
218
|
+
id: 'type',
|
|
219
|
+
parameterSettings: {
|
|
220
|
+
propA: { type: 'stateful', definition: statefulParameterSpy }
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const controller = new ResourceController(resource);
|
|
231
|
+
const plan = await controller.plan({ type: 'type', propA: ['a', 'b', 'c', 'd'] } as any)
|
|
232
|
+
|
|
233
|
+
expect(plan).toMatchObject({
|
|
234
|
+
changeSet: {
|
|
235
|
+
operation: ResourceOperation.MODIFY,
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('Uses isElementEqual for stateless mode filtering if available', async () => {
|
|
241
|
+
const statefulParameter = new class extends TestArrayStatefulParameter {
|
|
242
|
+
getSettings(): ArrayParameterSetting {
|
|
243
|
+
return {
|
|
244
|
+
type: 'array',
|
|
245
|
+
isElementEqual: (desired, current) => {
|
|
246
|
+
return current.includes(desired)
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async refresh(): Promise<any | null> {
|
|
252
|
+
return ['3.11.9']
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
257
|
+
|
|
258
|
+
const resource = new class extends TestResource {
|
|
259
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
260
|
+
return {
|
|
261
|
+
id: 'type',
|
|
262
|
+
parameterSettings: {
|
|
263
|
+
propA: { type: 'stateful', definition: statefulParameterSpy }
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
269
|
+
return {};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const controller = new ResourceController(resource);
|
|
274
|
+
const plan = await controller.plan({ type: 'type', propA: ['3.11'] } as any)
|
|
275
|
+
|
|
276
|
+
expect(plan).toMatchObject({
|
|
277
|
+
changeSet: {
|
|
278
|
+
operation: ResourceOperation.NOOP,
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('Plans stateful parameters in the order specified', async () => {
|
|
284
|
+
const statefulParameterA = spy(new class extends TestStatefulParameter {
|
|
285
|
+
async refresh(): Promise<any | null> {
|
|
286
|
+
return performance.now()
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const statefulParameterB = spy(new class extends TestStatefulParameter {
|
|
291
|
+
async refresh(): Promise<any | null> {
|
|
292
|
+
return performance.now()
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const statefulParameterC = spy(new class extends TestStatefulParameter {
|
|
297
|
+
async refresh(): Promise<any | null> {
|
|
298
|
+
return performance.now()
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const statefulParameterD = spy(new class extends TestStatefulParameter {
|
|
303
|
+
async refresh(): Promise<any | null> {
|
|
304
|
+
return performance.now()
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const statefulParameterE = spy(new class extends TestStatefulParameter {
|
|
309
|
+
async refresh(): Promise<any | null> {
|
|
310
|
+
return performance.now()
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const resource = spy(new class extends TestResource {
|
|
315
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
316
|
+
return {
|
|
317
|
+
id: 'resourceType',
|
|
318
|
+
parameterSettings: {
|
|
319
|
+
propA: { type: 'stateful', definition: statefulParameterA, order: 3 },
|
|
320
|
+
propB: { type: 'stateful', definition: statefulParameterB, order: 1 },
|
|
321
|
+
propC: { type: 'stateful', definition: statefulParameterC, order: 2 },
|
|
322
|
+
propD: { type: 'stateful', definition: statefulParameterD },
|
|
323
|
+
propE: { type: 'stateful', definition: statefulParameterE }
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
329
|
+
return {};
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const controller = new ResourceController(resource)
|
|
334
|
+
const plan = await controller.plan({
|
|
335
|
+
type: 'resourceType',
|
|
336
|
+
propA: 'propA',
|
|
337
|
+
propB: 10,
|
|
338
|
+
propC: 'propC',
|
|
339
|
+
propD: 'propD',
|
|
340
|
+
propE: 'propE',
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(plan.currentConfig?.propB).to.be.lessThan(plan.currentConfig?.propC as any);
|
|
344
|
+
expect(plan.currentConfig?.propC).to.be.lessThan(plan.currentConfig?.propA as any);
|
|
345
|
+
expect(plan.currentConfig?.propA).to.be.lessThan(plan.currentConfig?.propD as any);
|
|
346
|
+
expect(plan.currentConfig?.propD).to.be.lessThan(plan.currentConfig?.propE as any);
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('Applies stateful parameters in the order specified', async () => {
|
|
350
|
+
let timestampA;
|
|
351
|
+
const statefulParameterA = spy(new class extends TestStatefulParameter {
|
|
352
|
+
add = async (): Promise<void> => {
|
|
353
|
+
timestampA = performance.now();
|
|
354
|
+
}
|
|
355
|
+
modify = async (): Promise<void> => {
|
|
356
|
+
timestampA = performance.now();
|
|
357
|
+
}
|
|
358
|
+
remove = async (): Promise<void> => {
|
|
359
|
+
timestampA = performance.now();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
let timestampB
|
|
364
|
+
const statefulParameterB = spy(new class extends TestStatefulParameter {
|
|
365
|
+
add = async (): Promise<void> => {
|
|
366
|
+
timestampB = performance.now();
|
|
367
|
+
}
|
|
368
|
+
modify = async (): Promise<void> => {
|
|
369
|
+
timestampB = performance.now();
|
|
370
|
+
}
|
|
371
|
+
remove = async (): Promise<void> => {
|
|
372
|
+
timestampB = performance.now();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
let timestampC
|
|
377
|
+
const statefulParameterC = spy(new class extends TestStatefulParameter {
|
|
378
|
+
add = async (): Promise<void> => {
|
|
379
|
+
timestampC = performance.now();
|
|
380
|
+
}
|
|
381
|
+
modify = async (): Promise<void> => {
|
|
382
|
+
timestampC = performance.now();
|
|
383
|
+
}
|
|
384
|
+
remove = async (): Promise<void> => {
|
|
385
|
+
timestampC = performance.now();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const resource = spy(new class extends TestResource {
|
|
390
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
391
|
+
return {
|
|
392
|
+
id: 'resourceType',
|
|
393
|
+
parameterSettings: {
|
|
394
|
+
propA: { type: 'stateful', definition: statefulParameterA, order: 3 },
|
|
395
|
+
propB: { type: 'stateful', definition: statefulParameterB, order: 1 },
|
|
396
|
+
propC: { type: 'stateful', definition: statefulParameterC, order: 2 },
|
|
397
|
+
},
|
|
398
|
+
removeStatefulParametersBeforeDestroy: true,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const controller = new ResourceController(resource);
|
|
404
|
+
await controller.apply(
|
|
405
|
+
Plan.fromResponse({
|
|
406
|
+
resourceType: 'resourceType',
|
|
407
|
+
operation: ResourceOperation.CREATE,
|
|
408
|
+
parameters: [
|
|
409
|
+
{ name: 'propA', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
|
|
410
|
+
{ name: 'propB', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
|
|
411
|
+
{ name: 'propC', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
|
|
412
|
+
]
|
|
413
|
+
}, {}) as any
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
if (!timestampB || !timestampC || !timestampA) {
|
|
417
|
+
throw new Error('Variable not initialized')
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
expect(timestampB).to.be.lessThan(timestampC as any);
|
|
421
|
+
expect(timestampC).to.be.lessThan(timestampA as any);
|
|
422
|
+
timestampA = 0;
|
|
423
|
+
timestampB = 0;
|
|
424
|
+
timestampC = 0;
|
|
425
|
+
|
|
426
|
+
await controller.apply(
|
|
427
|
+
Plan.fromResponse({
|
|
428
|
+
resourceType: 'resourceType',
|
|
429
|
+
operation: ResourceOperation.MODIFY,
|
|
430
|
+
parameters: [
|
|
431
|
+
{ name: 'propA', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
|
|
432
|
+
{ name: 'propB', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
|
|
433
|
+
{ name: 'propC', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
|
|
434
|
+
]
|
|
435
|
+
}, {}) as any
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
expect(timestampB).to.be.lessThan(timestampC as any);
|
|
439
|
+
expect(timestampC).to.be.lessThan(timestampA as any);
|
|
440
|
+
timestampA = 0;
|
|
441
|
+
timestampB = 0;
|
|
442
|
+
timestampC = 0;
|
|
443
|
+
|
|
444
|
+
await controller.apply(
|
|
445
|
+
Plan.fromResponse({
|
|
446
|
+
resourceType: 'resourceType',
|
|
447
|
+
operation: ResourceOperation.DESTROY,
|
|
448
|
+
parameters: [
|
|
449
|
+
{ name: 'propA', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
|
|
450
|
+
{ name: 'propB', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
|
|
451
|
+
{ name: 'propC', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
|
|
452
|
+
]
|
|
453
|
+
}, {}) as any
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
expect(timestampB).to.be.lessThan(timestampC as any);
|
|
457
|
+
expect(timestampC).to.be.lessThan(timestampA as any);
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('Supports transform parameters', async () => {
|
|
461
|
+
const resource = spy(new class extends TestResource {
|
|
462
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
463
|
+
return {
|
|
464
|
+
id: 'resourceType',
|
|
465
|
+
inputTransformation: (desired) => ({
|
|
466
|
+
propA: 'propA',
|
|
467
|
+
propB: 10,
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
473
|
+
return {
|
|
474
|
+
propA: 'propA',
|
|
475
|
+
propB: 10,
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const controller = new ResourceController(resource);
|
|
481
|
+
const plan = await controller.plan({ type: 'resourceType', propC: 'abc' } as any);
|
|
482
|
+
|
|
483
|
+
expect(resource.refresh.called).to.be.true;
|
|
484
|
+
expect(resource.refresh.getCall(0).firstArg['propA']).to.exist;
|
|
485
|
+
expect(resource.refresh.getCall(0).firstArg['propB']).to.exist;
|
|
486
|
+
expect(resource.refresh.getCall(0).firstArg['propC']).to.not.exist;
|
|
487
|
+
|
|
488
|
+
expect(plan.desiredConfig?.propA).to.eq('propA');
|
|
489
|
+
expect(plan.desiredConfig?.propB).to.eq(10);
|
|
490
|
+
expect(plan.desiredConfig?.propC).to.be.undefined;
|
|
491
|
+
|
|
492
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
493
|
+
})
|
|
494
|
+
})
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { untildify } from '../utils/utils.js';
|
|
5
|
+
import { StatefulParameter } from './stateful-parameter.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The configuration and settings for a resource.
|
|
9
|
+
*/
|
|
10
|
+
export interface ResourceSettings<T extends StringIndexedObject> {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The typeId of the resource.
|
|
14
|
+
*/
|
|
15
|
+
id: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Schema to validate user configs with. Must be in the format JSON Schema draft07
|
|
19
|
+
*/
|
|
20
|
+
schema?: unknown;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Allow multiple of the same resource to unique. Set truthy if
|
|
24
|
+
* multiples are allowed, for example for applications, there can be multiple copy of the same application installed
|
|
25
|
+
* on the system. Or there can be multiple git repos. Defaults to false.
|
|
26
|
+
*/
|
|
27
|
+
allowMultiple?: {
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* If multiple copies are allowed then a matcher must be defined to match the desired
|
|
31
|
+
* config with one of the resources currently existing on the system. Return null if there is no match.
|
|
32
|
+
*
|
|
33
|
+
* @param current An array of resources found installed on the system
|
|
34
|
+
* @param desired The desired config to match.
|
|
35
|
+
*
|
|
36
|
+
* @return The matched resource.
|
|
37
|
+
*/
|
|
38
|
+
matcher: (desired: Partial<T>, current: Partial<T>[],) => Partial<T>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* If true, {@link StatefulParameter} remove() will be called before resource destruction. This is useful
|
|
43
|
+
* if the stateful parameter needs to be first uninstalled (cleanup) before the overall resource can be
|
|
44
|
+
* uninstalled. Defaults to false.
|
|
45
|
+
*/
|
|
46
|
+
removeStatefulParametersBeforeDestroy?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* An array of type ids of resources that this resource depends on. This affects the order in which multiple resources are
|
|
50
|
+
* planned and applied.
|
|
51
|
+
*/
|
|
52
|
+
dependencies?: string[];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Options for configuring parameters operations including overriding the equals function, adding default values
|
|
56
|
+
* and applying any input transformations. Use parameter settings to define stateful parameters as well.
|
|
57
|
+
*/
|
|
58
|
+
parameterSettings?: Partial<Record<keyof T, ParameterSetting>>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A config level transformation that is only applied to the user supplied desired config. This transformation is allowed
|
|
62
|
+
* to add, remove or modify keys as well as values. Changing this transformation for existing libraries will mess up existing states.
|
|
63
|
+
*
|
|
64
|
+
* @param desired
|
|
65
|
+
*/
|
|
66
|
+
inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The type of parameter. This value is mainly used to determine a pre-set equality method for comparing the current
|
|
71
|
+
* config with desired config. Certain types will have additional options to help support it. For example the type
|
|
72
|
+
* stateful requires a stateful parameter definition and type array takes an isElementEqual method.
|
|
73
|
+
*/
|
|
74
|
+
export type ParameterSettingType =
|
|
75
|
+
'any'
|
|
76
|
+
| 'array'
|
|
77
|
+
| 'boolean'
|
|
78
|
+
| 'directory'
|
|
79
|
+
| 'number'
|
|
80
|
+
| 'stateful'
|
|
81
|
+
| 'string'
|
|
82
|
+
| 'version';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Typing information for the parameter setting. This represents a setting on a specific parameter within a
|
|
86
|
+
* resource. Options for configuring parameters operations including overriding the equals function, adding default values
|
|
87
|
+
* and applying any input transformations. See {@link DefaultParameterSetting } for more information.
|
|
88
|
+
* Use parameter settings to define stateful parameters as well.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
export type ParameterSetting =
|
|
92
|
+
ArrayParameterSetting
|
|
93
|
+
| DefaultParameterSetting
|
|
94
|
+
| StatefulParameterSetting
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The parent class for parameter settings. The options are applicable to array parameter settings
|
|
98
|
+
* as well.
|
|
99
|
+
*/
|
|
100
|
+
export interface DefaultParameterSetting {
|
|
101
|
+
/**
|
|
102
|
+
* The type of the value of this parameter. See {@link ParameterSettingType} for the available options. This value
|
|
103
|
+
* is mainly used to determine the equality method when performing diffing.
|
|
104
|
+
*/
|
|
105
|
+
type?: ParameterSettingType;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Default value for the parameter. If a value is not provided in the config, then this value will be used.
|
|
109
|
+
*/
|
|
110
|
+
default?: unknown;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A transformation of the input value for this parameter. This transformation is only applied to the desired parameter
|
|
114
|
+
* value supplied by the user.
|
|
115
|
+
*
|
|
116
|
+
* @param input The original parameter value from the desired config.
|
|
117
|
+
*/
|
|
118
|
+
inputTransformation?: (input: any) => Promise<any> | unknown;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan.
|
|
122
|
+
* This value will override the pre-set equality function from the type. Return true if the desired value is
|
|
123
|
+
* equivalent to the current value.
|
|
124
|
+
*
|
|
125
|
+
* @param desired The desired value.
|
|
126
|
+
* @param current The current value.
|
|
127
|
+
*
|
|
128
|
+
* @return Return true if equal
|
|
129
|
+
*/
|
|
130
|
+
isEqual?: (desired: any, current: any) => boolean;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Chose if the resource can be modified instead of re-created when there is a change to this parameter.
|
|
134
|
+
* Defaults to false (re-create).
|
|
135
|
+
*
|
|
136
|
+
* Examples:
|
|
137
|
+
* 1. Settings like git user name and git user email that have setter calls and don't require the re-installation of git
|
|
138
|
+
* 2. AWS profile secret keys that can be updated without the re-installation of AWS CLI
|
|
139
|
+
*/
|
|
140
|
+
canModify?: boolean
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Array type specific settings. See {@link DefaultParameterSetting } for a full list of options.
|
|
145
|
+
*/
|
|
146
|
+
export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
147
|
+
type: 'array'
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* An element level equality function for arrays. The diffing algorithm will take isElementEqual and use it in a
|
|
151
|
+
* O(n^2) equality comparison to determine if the overall array is equal. This value will override the pre-set equality
|
|
152
|
+
* function for arrays (desired === current). Return true if the desired element is equivalent to the current element.
|
|
153
|
+
*
|
|
154
|
+
* @param desired An element of the desired array
|
|
155
|
+
* @param current An element of the current array
|
|
156
|
+
*
|
|
157
|
+
* @return Return true if desired is equivalent to current.
|
|
158
|
+
*/
|
|
159
|
+
isElementEqual?: (desired: any, current: any) => boolean
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
|
|
164
|
+
* state but is still tied to the overall state of the resource. For example 'homebrew' is represented
|
|
165
|
+
* as a resource and taps, formulas and casks are represented as a stateful parameter. A formula can be installed,
|
|
166
|
+
* modified and removed (has state) but it is still tied to the overall lifecycle of homebrew.
|
|
167
|
+
*
|
|
168
|
+
*/
|
|
169
|
+
export interface StatefulParameterSetting extends DefaultParameterSetting {
|
|
170
|
+
type: 'stateful',
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The stateful parameter definition. A stateful parameter is a sub-resource that can hold its own
|
|
174
|
+
* state but is still tied to the overall state of the resource. For example 'homebrew' is represented
|
|
175
|
+
* as a resource and taps, formulas and casks are represented as a stateful parameter. A formula can be installed,
|
|
176
|
+
* modified and removed (has state) but it is still tied to the overall lifecycle of homebrew.
|
|
177
|
+
*/
|
|
178
|
+
definition: StatefulParameter<any, unknown>,
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The order multiple stateful parameters should be applied in. The order is applied in ascending order (1, 2, 3...).
|
|
182
|
+
*/
|
|
183
|
+
order?: number,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>> = {
|
|
187
|
+
'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b),
|
|
188
|
+
'directory': (a: unknown, b: unknown) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
|
|
189
|
+
'number': (a: unknown, b: unknown) => Number(a) === Number(b),
|
|
190
|
+
'string': (a: unknown, b: unknown) => String(a) === String(b),
|
|
191
|
+
'version': (desired: unknown, current: unknown) => String(current).includes(String(desired))
|
|
192
|
+
}
|