codify-plugin-lib 1.0.147 → 1.0.149

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.
@@ -107,6 +107,27 @@ export class Plugin {
107
107
  .validate(core, parameters);
108
108
  validationResults.push(validation);
109
109
  }
110
+ // Validate that if allow multiple is false, then only 1 of each resource exists
111
+ const countMap = data.configs.reduce((map, resource) => {
112
+ if (!map.has(resource.core.type)) {
113
+ map.set(resource.core.type, 0);
114
+ }
115
+ const count = map.get(resource.core.type);
116
+ map.set(resource.core.type, count + 1);
117
+ return map;
118
+ }, new Map());
119
+ const invalidMultipleConfigs = [...countMap.entries()].filter(([k, v]) => {
120
+ const controller = this.resourceControllers.get(k);
121
+ return !controller.parsedSettings.allowMultiple && v > 1;
122
+ });
123
+ if (invalidMultipleConfigs.length > 0) {
124
+ validationResults.push(...invalidMultipleConfigs.map(([k, v]) => ({
125
+ resourceType: k,
126
+ schemaValidationErrors: [],
127
+ customValidationErrorMessage: `Multiple of resource type: ${k} found in configs. Only allowed 1.`,
128
+ isValid: false,
129
+ })));
130
+ }
110
131
  await this.crossValidateResources(data.configs);
111
132
  return {
112
133
  resourceValidations: validationResults
@@ -113,10 +113,10 @@ export class ParsedResourceSettings {
113
113
  this.validateParameterEqualsFn(v, k);
114
114
  }
115
115
  }
116
- // if (this.allowMultiple
117
- // && Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) {
118
- // throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`)
119
- // }
116
+ if (Object.entries(this.parameterSettings).some(([k, v]) => v.type === 'stateful'
117
+ && typeof this.settings.allowMultiple === 'object' && this.settings.allowMultiple?.identifyingParameters?.includes(k))) {
118
+ throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed to be identifying parameters for allowMultiple.`);
119
+ }
120
120
  const schema = this.settings.schema;
121
121
  if (!this.settings.importAndDestroy && (schema?.oneOf
122
122
  && Array.isArray(schema.oneOf)
@@ -85,7 +85,6 @@ export class ResourceController {
85
85
  // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
86
86
  if (currentArray === null
87
87
  || currentArray === undefined
88
- || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
89
88
  || currentArray.length === 0
90
89
  || currentArray.filter(Boolean).length === 0) {
91
90
  return Plan.calculate({
@@ -97,12 +96,12 @@ export class ResourceController {
97
96
  isStateful,
98
97
  });
99
98
  }
100
- // Refresh stateful parameters. These parameters have state external to the resource. allowMultiple
101
- // does not work together with stateful parameters
102
- const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, allParameters);
99
+ // Refresh stateful parameters. These parameters have state external to the resource. Each variation of the
100
+ // current parameters (each array element) is passed into the stateful parameter refresh.
101
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentArray, allParameters);
103
102
  return Plan.calculate({
104
103
  desired,
105
- currentArray: [{ ...currentArray[0], ...statefulCurrentParameters }],
104
+ currentArray: currentArray.map((c, idx) => ({ ...c, ...statefulCurrentParameters[idx] })),
106
105
  state,
107
106
  core,
108
107
  settings: this.parsedSettings,
@@ -166,25 +165,21 @@ export class ResourceController {
166
165
  };
167
166
  // Parse data from the user supplied config
168
167
  const parsedConfig = new ConfigParser(parametersToRefresh, null, this.parsedSettings.statefulParameters);
169
- const { allNonStatefulParameters, allStatefulParameters, } = parsedConfig;
168
+ const { allParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig;
170
169
  const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
171
170
  if (currentParametersArray === null
172
171
  || currentParametersArray === undefined
173
- || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
174
172
  || currentParametersArray.filter(Boolean).length === 0) {
175
- for (const result of currentParametersArray ?? []) {
176
- await this.applyTransformParameters(result, true);
177
- this.removeDefaultValues(result, parameters);
178
- }
179
- return currentParametersArray
180
- ?.map((r) => ({ core, parameters: r }))
181
- ?? null;
173
+ return [];
174
+ }
175
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray, allParameters);
176
+ const resultParametersArray = currentParametersArray
177
+ ?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] }));
178
+ for (const result of resultParametersArray) {
179
+ await this.applyTransformParameters(result, true);
180
+ this.removeDefaultValues(result, parameters);
182
181
  }
183
- const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, parametersToRefresh);
184
- const resultParameters = { ...currentParametersArray[0], ...statefulCurrentParameters };
185
- await this.applyTransformParameters(resultParameters, true);
186
- this.removeDefaultValues(resultParameters, parameters);
187
- return [{ core, parameters: resultParameters }];
182
+ return resultParametersArray?.map((r) => ({ core, parameters: r }));
188
183
  }
189
184
  async applyCreate(plan) {
190
185
  await this.resource.create(plan);
@@ -296,17 +291,19 @@ ${JSON.stringify(refresh, null, 2)}
296
291
  }
297
292
  // Refresh stateful parameters
298
293
  // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
299
- async refreshStatefulParameters(statefulParametersConfig, allParameters) {
300
- const result = {};
294
+ async refreshStatefulParameters(statefulParametersConfig, currentArray, allParameters) {
295
+ const result = Array.from({ length: currentArray.length }, () => ({}));
301
296
  const sortedEntries = Object.entries(statefulParametersConfig)
302
297
  .sort(([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1) - this.parsedSettings.statefulParameterOrder.get(key2));
303
- await Promise.all(sortedEntries.map(async ([key, desiredValue]) => {
304
- const statefulParameter = this.parsedSettings.statefulParameters.get(key);
305
- if (!statefulParameter) {
306
- throw new Error(`Stateful parameter ${key} was not found`);
307
- }
308
- result[key] = await statefulParameter.refresh(desiredValue ?? null, allParameters);
309
- }));
298
+ for (const [idx, refreshedParams] of currentArray.entries()) {
299
+ await Promise.all(sortedEntries.map(async ([key, desiredValue]) => {
300
+ const statefulParameter = this.parsedSettings.statefulParameters.get(key);
301
+ if (!statefulParameter) {
302
+ throw new Error(`Stateful parameter ${key} was not found`);
303
+ }
304
+ result[idx][key] = await statefulParameter.refresh(desiredValue ?? null, { ...allParameters, ...refreshedParams });
305
+ }));
306
+ }
310
307
  return result;
311
308
  }
312
309
  validatePlanInputs(core, desired, current, isStateful) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.147",
3
+ "version": "1.0.149",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -448,4 +448,47 @@ describe('Plugin tests', () => {
448
448
 
449
449
  console.log(match)
450
450
  })
451
+
452
+ it('Validates that a config correctly allows multiple of a config when allowMultiple is true', async () => {
453
+ const resource = spy(new class extends TestResource {
454
+ getSettings(): ResourceSettings<TestConfig> {
455
+ return {
456
+ id: 'ssh-config',
457
+ allowMultiple: true
458
+ }
459
+ }
460
+ })
461
+
462
+ const testPlugin = Plugin.create('testPlugin', [resource as any]);
463
+
464
+ const validate1 = await testPlugin.validate({
465
+ configs: [{ core: { type: 'ssh-config' }, parameters: { propA: 'a' } }, {
466
+ core: { type: 'ssh-config' },
467
+ parameters: { propB: 'b' }
468
+ }]
469
+ })
470
+
471
+ expect(validate1.resourceValidations.every((r) => r.isValid)).to.be.true;
472
+ })
473
+
474
+ it('Validates that a config correctly dis-allows multiple of a config when allowMultiple is false', async () => {
475
+ const resource = spy(new class extends TestResource {
476
+ getSettings(): ResourceSettings<TestConfig> {
477
+ return {
478
+ id: 'ssh-config',
479
+ }
480
+ }
481
+ })
482
+
483
+ const testPlugin = Plugin.create('testPlugin', [resource as any]);
484
+
485
+ const validate1 = await testPlugin.validate({
486
+ configs: [{ core: { type: 'ssh-config' }, parameters: { propA: 'a' } }, {
487
+ core: { type: 'ssh-config' },
488
+ parameters: { propB: 'b' }
489
+ }]
490
+ })
491
+
492
+ expect(validate1.resourceValidations.every((r) => r.isValid)).to.be.false;
493
+ })
451
494
  });
@@ -145,7 +145,7 @@ export class Plugin {
145
145
  }
146
146
 
147
147
  async validate(data: ValidateRequestData): Promise<ValidateResponseData> {
148
- const validationResults = [];
148
+ const validationResults: ValidateResponseData['resourceValidations'] = [];
149
149
  for (const config of data.configs) {
150
150
  const { core, parameters } = config;
151
151
 
@@ -160,6 +160,32 @@ export class Plugin {
160
160
  validationResults.push(validation);
161
161
  }
162
162
 
163
+ // Validate that if allow multiple is false, then only 1 of each resource exists
164
+ const countMap = data.configs.reduce((map, resource) => {
165
+ if (!map.has(resource.core.type)) {
166
+ map.set(resource.core.type, 0);
167
+ }
168
+
169
+ const count = map.get(resource.core.type)!;
170
+ map.set(resource.core.type, count + 1)
171
+
172
+ return map;
173
+ }, new Map<string, number>())
174
+
175
+ const invalidMultipleConfigs = [...countMap.entries()].filter(([k, v]) => {
176
+ const controller = this.resourceControllers.get(k)!;
177
+ return !controller.parsedSettings.allowMultiple && v > 1;
178
+ });
179
+
180
+ if (invalidMultipleConfigs.length > 0) {
181
+ validationResults.push(...invalidMultipleConfigs.map(([k, v]) => ({
182
+ resourceType: k,
183
+ schemaValidationErrors: [],
184
+ customValidationErrorMessage: `Multiple of resource type: ${k} found in configs. Only allowed 1.`,
185
+ isValid: false,
186
+ })));
187
+ }
188
+
163
189
  await this.crossValidateResources(data.configs);
164
190
  return {
165
191
  resourceValidations: validationResults
@@ -188,10 +188,11 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
188
188
  }
189
189
  }
190
190
 
191
- // if (this.allowMultiple
192
- // && Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) {
193
- // throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`)
194
- // }
191
+ if (Object.entries(this.parameterSettings).some(([k, v]) =>
192
+ v.type === 'stateful'
193
+ && typeof this.settings.allowMultiple === 'object' && this.settings.allowMultiple?.identifyingParameters?.includes(k))) {
194
+ throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed to be identifying parameters for allowMultiple.`)
195
+ }
195
196
 
196
197
  const schema = this.settings.schema as JSONSchemaType<any>;
197
198
  if (!this.settings.importAndDestroy && (schema?.oneOf
@@ -130,7 +130,6 @@ export class ResourceController<T extends StringIndexedObject> {
130
130
  // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
131
131
  if (currentArray === null
132
132
  || currentArray === undefined
133
- || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
134
133
  || currentArray.length === 0
135
134
  || currentArray.filter(Boolean).length === 0
136
135
  ) {
@@ -144,13 +143,13 @@ export class ResourceController<T extends StringIndexedObject> {
144
143
  });
145
144
  }
146
145
 
147
- // Refresh stateful parameters. These parameters have state external to the resource. allowMultiple
148
- // does not work together with stateful parameters
149
- const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, allParameters);
146
+ // Refresh stateful parameters. These parameters have state external to the resource. Each variation of the
147
+ // current parameters (each array element) is passed into the stateful parameter refresh.
148
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentArray, allParameters);
150
149
 
151
150
  return Plan.calculate({
152
151
  desired,
153
- currentArray: [{ ...currentArray[0], ...statefulCurrentParameters }] as Partial<T>[],
152
+ currentArray: currentArray.map((c, idx) => ({ ...c, ...statefulCurrentParameters[idx] })),
154
153
  state,
155
154
  core,
156
155
  settings: this.parsedSettings,
@@ -241,6 +240,7 @@ export class ResourceController<T extends StringIndexedObject> {
241
240
  // Parse data from the user supplied config
242
241
  const parsedConfig = new ConfigParser(parametersToRefresh, null, this.parsedSettings.statefulParameters)
243
242
  const {
243
+ allParameters,
244
244
  allNonStatefulParameters,
245
245
  allStatefulParameters,
246
246
  } = parsedConfig;
@@ -249,26 +249,21 @@ export class ResourceController<T extends StringIndexedObject> {
249
249
 
250
250
  if (currentParametersArray === null
251
251
  || currentParametersArray === undefined
252
- || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
253
252
  || currentParametersArray.filter(Boolean).length === 0
254
253
  ) {
255
- for (const result of currentParametersArray ?? []) {
256
- await this.applyTransformParameters(result, true);
257
- this.removeDefaultValues(result, parameters)
258
- }
259
-
260
- return currentParametersArray
261
- ?.map((r) => ({ core, parameters: r }))
262
- ?? null;
254
+ return [];
263
255
  }
264
256
 
265
- const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, parametersToRefresh);
266
- const resultParameters = { ...currentParametersArray[0], ...statefulCurrentParameters };
257
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray, allParameters);
258
+ const resultParametersArray = currentParametersArray
259
+ ?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] }))
267
260
 
268
- await this.applyTransformParameters(resultParameters, true);
269
- this.removeDefaultValues(resultParameters, parameters)
261
+ for (const result of resultParametersArray) {
262
+ await this.applyTransformParameters(result, true);
263
+ this.removeDefaultValues(result, parameters);
264
+ }
270
265
 
271
- return [{ core, parameters: resultParameters }];
266
+ return resultParametersArray?.map((r) => ({ core, parameters: r }))
272
267
  }
273
268
 
274
269
  private async applyCreate(plan: Plan<T>): Promise<void> {
@@ -410,21 +405,27 @@ ${JSON.stringify(refresh, null, 2)}
410
405
 
411
406
  // Refresh stateful parameters
412
407
  // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
413
- private async refreshStatefulParameters(statefulParametersConfig: Partial<T>, allParameters: Partial<T>): Promise<Partial<T>> {
414
- const result: Partial<T> = {}
408
+ private async refreshStatefulParameters(
409
+ statefulParametersConfig: Partial<T>,
410
+ currentArray: Array<Partial<T>>,
411
+ allParameters: Partial<T>
412
+ ): Promise<Array<Partial<T>>> {
413
+ const result: Array<Partial<T>> = Array.from({ length: currentArray.length }, () => ({}))
415
414
  const sortedEntries = Object.entries(statefulParametersConfig)
416
415
  .sort(
417
416
  ([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)!
418
417
  )
419
418
 
420
- await Promise.all(sortedEntries.map(async ([key, desiredValue]) => {
421
- const statefulParameter = this.parsedSettings.statefulParameters.get(key);
422
- if (!statefulParameter) {
423
- throw new Error(`Stateful parameter ${key} was not found`);
424
- }
419
+ for (const [idx, refreshedParams] of currentArray.entries()) {
420
+ await Promise.all(sortedEntries.map(async ([key, desiredValue]) => {
421
+ const statefulParameter = this.parsedSettings.statefulParameters.get(key);
422
+ if (!statefulParameter) {
423
+ throw new Error(`Stateful parameter ${key} was not found`);
424
+ }
425
425
 
426
- (result as Record<string, unknown>)[key] = await statefulParameter.refresh(desiredValue ?? null, allParameters)
427
- }))
426
+ (result[idx][key] as T[keyof T] | null) = await statefulParameter.refresh(desiredValue ?? null, { ...allParameters, ...refreshedParams })
427
+ }))
428
+ }
428
429
 
429
430
  return result;
430
431
  }