codify-plugin-lib 1.0.103 → 1.0.105
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/dist/plan/plan.js +31 -12
- package/dist/resource/resource-settings.d.ts +1 -1
- package/package.json +1 -1
- package/src/plan/plan.ts +38 -12
- package/src/resource/resource-controller.test.ts +115 -1
- package/src/resource/resource-settings.test.ts +88 -0
- package/src/resource/resource-settings.ts +1 -1
package/dist/plan/plan.js
CHANGED
|
@@ -143,15 +143,28 @@ export class Plan {
|
|
|
143
143
|
return Object.fromEntries(Object.entries(current)
|
|
144
144
|
.filter(([k]) => keys.has(k)));
|
|
145
145
|
}
|
|
146
|
+
function getFilterParameter(k) {
|
|
147
|
+
if (settings.parameterSettings?.[k]?.type === 'stateful') {
|
|
148
|
+
const statefulSetting = settings.parameterSettings[k];
|
|
149
|
+
if (statefulSetting.nestedSettings.type === 'array') {
|
|
150
|
+
return statefulSetting.nestedSettings.filterInStatelessMode;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (settings.parameterSettings?.[k]?.type === 'array') {
|
|
154
|
+
return (settings.parameterSettings?.[k]).filterInStatelessMode;
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
146
158
|
function isArrayParameterWithFiltering(k, v) {
|
|
159
|
+
const filterParameter = getFilterParameter(k);
|
|
147
160
|
if (settings.parameterSettings?.[k]?.type === 'stateful') {
|
|
148
161
|
const statefulSetting = settings.parameterSettings[k];
|
|
149
162
|
return statefulSetting.nestedSettings.type === 'array' &&
|
|
150
|
-
(
|
|
163
|
+
(filterParameter ?? true)
|
|
151
164
|
&& Array.isArray(v);
|
|
152
165
|
}
|
|
153
166
|
return settings.parameterSettings?.[k]?.type === 'array'
|
|
154
|
-
&& (
|
|
167
|
+
&& (filterParameter ?? true)
|
|
155
168
|
&& Array.isArray(v);
|
|
156
169
|
}
|
|
157
170
|
// For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
|
|
@@ -165,17 +178,23 @@ export class Plan {
|
|
|
165
178
|
.isElementEqual;
|
|
166
179
|
const desiredCopy = [...desiredArray];
|
|
167
180
|
const currentCopy = [...v];
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
const defaultFilterMethod = ((desired, current) => {
|
|
182
|
+
const result = [];
|
|
183
|
+
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
184
|
+
const idx = currentCopy.findIndex((e2) => matcher(desiredCopy[counter], e2));
|
|
185
|
+
if (idx === -1) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
desiredCopy.splice(counter, 1);
|
|
189
|
+
const [element] = currentCopy.splice(idx, 1);
|
|
190
|
+
result.push(element);
|
|
173
191
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
return result;
|
|
193
|
+
});
|
|
194
|
+
const filterParameter = getFilterParameter(k);
|
|
195
|
+
return typeof filterParameter === 'function'
|
|
196
|
+
? filterParameter(desiredCopy, currentCopy)
|
|
197
|
+
: defaultFilterMethod(desiredCopy, currentCopy);
|
|
179
198
|
}
|
|
180
199
|
}
|
|
181
200
|
// TODO: This needs to be revisited. I don't think this is valid anymore.
|
|
@@ -191,7 +191,7 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
|
191
191
|
*
|
|
192
192
|
* Defaults to true.
|
|
193
193
|
*/
|
|
194
|
-
filterInStatelessMode?: boolean;
|
|
194
|
+
filterInStatelessMode?: ((desired: any[], current: any[]) => any[]) | boolean;
|
|
195
195
|
}
|
|
196
196
|
/**
|
|
197
197
|
* Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
|
package/package.json
CHANGED
package/src/plan/plan.ts
CHANGED
|
@@ -251,16 +251,34 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
251
251
|
) as Partial<T>;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
function getFilterParameter(k: string): ((desired: any[], current: any[]) => any[]) | boolean | undefined {
|
|
255
|
+
if (settings.parameterSettings?.[k]?.type === 'stateful') {
|
|
256
|
+
const statefulSetting = settings.parameterSettings[k] as ParsedStatefulParameterSetting;
|
|
257
|
+
|
|
258
|
+
if (statefulSetting.nestedSettings.type === 'array') {
|
|
259
|
+
return (statefulSetting.nestedSettings as ArrayParameterSetting).filterInStatelessMode
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (settings.parameterSettings?.[k]?.type === 'array') {
|
|
264
|
+
return (settings.parameterSettings?.[k] as ArrayParameterSetting).filterInStatelessMode;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
254
270
|
function isArrayParameterWithFiltering(k: string, v: T[keyof T]): boolean {
|
|
271
|
+
const filterParameter = getFilterParameter(k);
|
|
272
|
+
|
|
255
273
|
if (settings.parameterSettings?.[k]?.type === 'stateful') {
|
|
256
274
|
const statefulSetting = settings.parameterSettings[k] as ParsedStatefulParameterSetting;
|
|
257
275
|
return statefulSetting.nestedSettings.type === 'array' &&
|
|
258
|
-
(
|
|
276
|
+
(filterParameter ?? true)
|
|
259
277
|
&& Array.isArray(v);
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
return settings.parameterSettings?.[k]?.type === 'array'
|
|
263
|
-
&& (
|
|
281
|
+
&& (filterParameter ?? true)
|
|
264
282
|
&& Array.isArray(v);
|
|
265
283
|
}
|
|
266
284
|
|
|
@@ -276,21 +294,29 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
276
294
|
|
|
277
295
|
const desiredCopy = [...desiredArray];
|
|
278
296
|
const currentCopy = [...v];
|
|
279
|
-
const result = [];
|
|
280
297
|
|
|
281
|
-
|
|
282
|
-
const
|
|
298
|
+
const defaultFilterMethod = ((desired: any[], current: any[]) => {
|
|
299
|
+
const result = [];
|
|
300
|
+
|
|
301
|
+
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
302
|
+
const idx = currentCopy.findIndex((e2) => matcher(desiredCopy[counter], e2))
|
|
283
303
|
|
|
284
|
-
|
|
285
|
-
|
|
304
|
+
if (idx === -1) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
desiredCopy.splice(counter, 1)
|
|
309
|
+
const [element] = currentCopy.splice(idx, 1)
|
|
310
|
+
result.push(element)
|
|
286
311
|
}
|
|
287
312
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
result.push(element)
|
|
291
|
-
}
|
|
313
|
+
return result;
|
|
314
|
+
})
|
|
292
315
|
|
|
293
|
-
|
|
316
|
+
const filterParameter = getFilterParameter(k);
|
|
317
|
+
return typeof filterParameter === 'function'
|
|
318
|
+
? filterParameter(desiredCopy, currentCopy)
|
|
319
|
+
: defaultFilterMethod(desiredCopy, currentCopy);
|
|
294
320
|
}
|
|
295
321
|
}
|
|
296
322
|
|
|
@@ -2,12 +2,14 @@ import { Resource } from './resource.js';
|
|
|
2
2
|
import { ResourceOperation } from 'codify-schemas';
|
|
3
3
|
import { spy } from 'sinon';
|
|
4
4
|
import { describe, expect, it } from 'vitest'
|
|
5
|
-
import { ResourceSettings } from './resource-settings.js';
|
|
5
|
+
import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
|
|
6
6
|
import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
|
|
7
7
|
import { ParameterChange } from '../plan/change-set.js';
|
|
8
8
|
import { ResourceController } from './resource-controller.js';
|
|
9
9
|
import { TestConfig, testPlan, TestResource, TestStatefulParameter } from '../utils/test-utils.test.js';
|
|
10
10
|
import { untildify } from '../utils/utils.js';
|
|
11
|
+
import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
|
|
12
|
+
import { Plan } from '../plan/plan.js';
|
|
11
13
|
|
|
12
14
|
describe('Resource tests', () => {
|
|
13
15
|
|
|
@@ -391,4 +393,116 @@ describe('Resource tests', () => {
|
|
|
391
393
|
}
|
|
392
394
|
}
|
|
393
395
|
})
|
|
396
|
+
|
|
397
|
+
it('Can use multiple stateful parameters (create)', async () => {
|
|
398
|
+
const parameter1 = new class extends StatefulParameter<any, any> {
|
|
399
|
+
getSettings(): ParameterSetting {
|
|
400
|
+
return {
|
|
401
|
+
type: 'version'
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
override async refresh(desired: any, config: Partial<any>): Promise<any> {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
override async add(valueToAdd: any, plan: Plan<any>): Promise<void> {}
|
|
408
|
+
override async modify(newValue: any, previousValue: any, plan: Plan<any>): Promise<void> {}
|
|
409
|
+
override async remove(valueToRemove: any, plan: Plan<any>): Promise<void> {}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const parameter2 = new class extends ArrayStatefulParameter<any, any> {
|
|
413
|
+
override getSettings(): ArrayParameterSetting {
|
|
414
|
+
return {
|
|
415
|
+
type: 'array',
|
|
416
|
+
isElementEqual: (desired, current) => current.includes(desired),
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
override async refresh(desired: any[] | null, config: Partial<any>): Promise<any[] | null> {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
override async addItem(item: any, plan: Plan<any>): Promise<void> {}
|
|
424
|
+
override async removeItem(item: any, plan: Plan<any>): Promise<void> {}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const p1Spy = spy(parameter1);
|
|
428
|
+
const p2Spy = spy(parameter2);
|
|
429
|
+
|
|
430
|
+
const resource = new class extends TestResource {
|
|
431
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
432
|
+
return {
|
|
433
|
+
id: 'nvm',
|
|
434
|
+
parameterSettings: {
|
|
435
|
+
global: { type: 'stateful', definition: parameter1, order: 2 },
|
|
436
|
+
nodeVersions: { type: 'stateful', definition: parameter2, order: 1 },
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const controller = new ResourceController(resource);
|
|
447
|
+
const plan = await controller.plan({ type: 'nvm', global: '20.12', nodeVersions: ['18', '20'] })
|
|
448
|
+
|
|
449
|
+
expect(plan).toMatchObject({
|
|
450
|
+
changeSet: {
|
|
451
|
+
operation: ResourceOperation.CREATE,
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
console.log(JSON.stringify(plan, null, 2))
|
|
456
|
+
|
|
457
|
+
await controller.apply(plan)
|
|
458
|
+
|
|
459
|
+
expect(p1Spy.add.calledOnce).to.be.true;
|
|
460
|
+
expect(p2Spy.addItem.calledTwice).to.be.true;
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('Can use multiple stateful parameters (modify)', async () => {
|
|
464
|
+
const parameter1 = spy(new class extends TestStatefulParameter {
|
|
465
|
+
async refresh(desired: string | null): Promise<string | null> {
|
|
466
|
+
return '16';
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
const parameter2 = spy(new class extends ArrayStatefulParameter<any, any> {
|
|
471
|
+
async refresh(desired: any[] | null, config: Partial<any>): Promise<any[] | null> {
|
|
472
|
+
return ['20']
|
|
473
|
+
}
|
|
474
|
+
async addItem(item: any, plan: Plan<any>): Promise<void> {}
|
|
475
|
+
async removeItem(item: any, plan: Plan<any>): Promise<void> {}
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
const resource = new class extends TestResource {
|
|
479
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
480
|
+
return {
|
|
481
|
+
id: 'nvm',
|
|
482
|
+
parameterSettings: {
|
|
483
|
+
global: { type: 'stateful', definition: parameter1, order: 2 },
|
|
484
|
+
nodeVersions: { type: 'stateful', definition: parameter2, order: 1 },
|
|
485
|
+
},
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
|
|
490
|
+
return {};
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const controller = new ResourceController(resource);
|
|
495
|
+
const plan = await controller.plan({ type: 'nvm', global: '20.12', nodeVersions: ['18', '20'] })
|
|
496
|
+
|
|
497
|
+
expect(plan).toMatchObject({
|
|
498
|
+
changeSet: {
|
|
499
|
+
operation: ResourceOperation.MODIFY,
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
await controller.apply(plan)
|
|
504
|
+
|
|
505
|
+
expect(parameter1.modify.calledOnce).to.be.true;
|
|
506
|
+
expect(parameter2.addItem.calledOnce).to.be.true;
|
|
507
|
+
});
|
|
394
508
|
});
|
|
@@ -239,6 +239,94 @@ describe('Resource parameter tests', () => {
|
|
|
239
239
|
})
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
+
it('Can accept a custom filter function to filter in stateless mode', async () => {
|
|
243
|
+
const resource = new class extends TestResource {
|
|
244
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
245
|
+
return {
|
|
246
|
+
id: 'type',
|
|
247
|
+
parameterSettings: {
|
|
248
|
+
hosts: {
|
|
249
|
+
type: 'array',
|
|
250
|
+
isElementEqual: 'object',
|
|
251
|
+
filterInStatelessMode: (desired, current) => {
|
|
252
|
+
return current.filter((d) => desired.some((c) => d.Host === c.Host))
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
|
|
260
|
+
return {
|
|
261
|
+
hosts: [
|
|
262
|
+
{
|
|
263
|
+
Host: '*',
|
|
264
|
+
AddKeysToAgent: 'yes',
|
|
265
|
+
IdentityFile: 'id_ed25519'
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
Host: 'github.com',
|
|
269
|
+
AddKeysToAgent: 'yes',
|
|
270
|
+
UseKeychain: 'yes',
|
|
271
|
+
IgnoreUnknown: 'UseKeychain',
|
|
272
|
+
IdentityFile: '~/.ssh/id_ed25519',
|
|
273
|
+
}
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const controller = new ResourceController(resource);
|
|
280
|
+
const plan = await controller.plan({
|
|
281
|
+
type: 'type',
|
|
282
|
+
hosts: [
|
|
283
|
+
{
|
|
284
|
+
Host: 'new.com',
|
|
285
|
+
AddKeysToAgent: 'yes',
|
|
286
|
+
IdentityFile: '~/.ssh/id_ed25519'
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
Host: 'github.com',
|
|
290
|
+
AddKeysToAgent: 'yes',
|
|
291
|
+
UseKeychain: 'yes',
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(plan).toMatchObject({
|
|
297
|
+
'changeSet': {
|
|
298
|
+
'operation': 'recreate',
|
|
299
|
+
'parameterChanges': [
|
|
300
|
+
{
|
|
301
|
+
'name': 'hosts',
|
|
302
|
+
'previousValue': [
|
|
303
|
+
{
|
|
304
|
+
'Host': 'github.com',
|
|
305
|
+
'AddKeysToAgent': 'yes',
|
|
306
|
+
'UseKeychain': 'yes',
|
|
307
|
+
'IgnoreUnknown': 'UseKeychain',
|
|
308
|
+
'IdentityFile': '~/.ssh/id_ed25519'
|
|
309
|
+
}
|
|
310
|
+
],
|
|
311
|
+
'newValue': [
|
|
312
|
+
{
|
|
313
|
+
'Host': 'new.com',
|
|
314
|
+
'AddKeysToAgent': 'yes',
|
|
315
|
+
'IdentityFile': '~/.ssh/id_ed25519'
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
'Host': 'github.com',
|
|
319
|
+
'AddKeysToAgent': 'yes',
|
|
320
|
+
'UseKeychain': 'yes'
|
|
321
|
+
}
|
|
322
|
+
],
|
|
323
|
+
'operation': 'modify'
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
242
330
|
it('Uses isElementEqual for stateless mode filtering if available', async () => {
|
|
243
331
|
const statefulParameter = new class extends TestArrayStatefulParameter {
|
|
244
332
|
getSettings(): ArrayParameterSetting {
|