codify-plugin-lib 1.0.146 → 1.0.148

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)
@@ -79,13 +79,12 @@ export class ResourceController {
79
79
  await this.applyTransformParameters(state);
80
80
  // Parse data from the user supplied config
81
81
  const parsedConfig = new ConfigParser(desired, state, this.parsedSettings.statefulParameters);
82
- const { allParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig;
82
+ const { allNonStatefulParameters, allStatefulParameters, } = parsedConfig;
83
83
  // Refresh resource parameters. This refreshes the parameters that configure the resource itself
84
84
  const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
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);
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,
@@ -170,17 +169,17 @@ export class ResourceController {
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
- return currentParametersArray
176
- ?.map((r) => ({ core, parameters: r }))
177
- ?? null;
173
+ return [];
174
+ }
175
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray);
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);
178
181
  }
179
- const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, parametersToRefresh);
180
- const resultParameters = { ...currentParametersArray[0], ...statefulCurrentParameters };
181
- await this.applyTransformParameters(resultParameters, true);
182
- this.removeDefaultValues(resultParameters, parameters);
183
- return [{ core, parameters: resultParameters }];
182
+ return resultParametersArray?.map((r) => ({ core, parameters: r }));
184
183
  }
185
184
  async applyCreate(plan) {
186
185
  await this.resource.create(plan);
@@ -293,16 +292,18 @@ ${JSON.stringify(refresh, null, 2)}
293
292
  // Refresh stateful parameters
294
293
  // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
295
294
  async refreshStatefulParameters(statefulParametersConfig, allParameters) {
296
- const result = {};
295
+ const result = Array.from({ length: allParameters.length }, () => ({}));
297
296
  const sortedEntries = Object.entries(statefulParametersConfig)
298
297
  .sort(([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1) - this.parsedSettings.statefulParameterOrder.get(key2));
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[key] = await statefulParameter.refresh(desiredValue ?? null, allParameters);
305
- }));
298
+ for (const [idx, refreshedParams] of allParameters.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, refreshedParams);
305
+ }));
306
+ }
306
307
  return result;
307
308
  }
308
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.146",
3
+ "version": "1.0.148",
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
@@ -119,7 +119,6 @@ export class ResourceController<T extends StringIndexedObject> {
119
119
  // Parse data from the user supplied config
120
120
  const parsedConfig = new ConfigParser(desired, state, this.parsedSettings.statefulParameters)
121
121
  const {
122
- allParameters,
123
122
  allNonStatefulParameters,
124
123
  allStatefulParameters,
125
124
  } = parsedConfig;
@@ -130,7 +129,6 @@ export class ResourceController<T extends StringIndexedObject> {
130
129
  // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
131
130
  if (currentArray === null
132
131
  || currentArray === undefined
133
- || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
134
132
  || currentArray.length === 0
135
133
  || currentArray.filter(Boolean).length === 0
136
134
  ) {
@@ -144,13 +142,13 @@ export class ResourceController<T extends StringIndexedObject> {
144
142
  });
145
143
  }
146
144
 
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);
145
+ // Refresh stateful parameters. These parameters have state external to the resource. Each variation of the
146
+ // current parameters (each array element) is passed into the stateful parameter refresh.
147
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentArray);
150
148
 
151
149
  return Plan.calculate({
152
150
  desired,
153
- currentArray: [{ ...currentArray[0], ...statefulCurrentParameters }] as Partial<T>[],
151
+ currentArray: currentArray.map((c, idx) => ({ ...c, ...statefulCurrentParameters[idx] })),
154
152
  state,
155
153
  core,
156
154
  settings: this.parsedSettings,
@@ -249,21 +247,21 @@ export class ResourceController<T extends StringIndexedObject> {
249
247
 
250
248
  if (currentParametersArray === null
251
249
  || currentParametersArray === undefined
252
- || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
253
250
  || currentParametersArray.filter(Boolean).length === 0
254
251
  ) {
255
- return currentParametersArray
256
- ?.map((r) => ({ core, parameters: r }))
257
- ?? null;
252
+ return [];
258
253
  }
259
254
 
260
- const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, parametersToRefresh);
261
- const resultParameters = { ...currentParametersArray[0], ...statefulCurrentParameters };
255
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray);
256
+ const resultParametersArray = currentParametersArray
257
+ ?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] }))
262
258
 
263
- await this.applyTransformParameters(resultParameters, true);
264
- this.removeDefaultValues(resultParameters, parameters)
259
+ for (const result of resultParametersArray) {
260
+ await this.applyTransformParameters(result, true);
261
+ this.removeDefaultValues(result, parameters);
262
+ }
265
263
 
266
- return [{ core, parameters: resultParameters }];
264
+ return resultParametersArray?.map((r) => ({ core, parameters: r }))
267
265
  }
268
266
 
269
267
  private async applyCreate(plan: Plan<T>): Promise<void> {
@@ -405,21 +403,23 @@ ${JSON.stringify(refresh, null, 2)}
405
403
 
406
404
  // Refresh stateful parameters
407
405
  // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
408
- private async refreshStatefulParameters(statefulParametersConfig: Partial<T>, allParameters: Partial<T>): Promise<Partial<T>> {
409
- const result: Partial<T> = {}
406
+ private async refreshStatefulParameters(statefulParametersConfig: Partial<T>, allParameters: Array<Partial<T>>): Promise<Array<Partial<T>>> {
407
+ const result: Array<Partial<T>> = Array.from({ length: allParameters.length }, () => ({}))
410
408
  const sortedEntries = Object.entries(statefulParametersConfig)
411
409
  .sort(
412
410
  ([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)!
413
411
  )
414
412
 
415
- await Promise.all(sortedEntries.map(async ([key, desiredValue]) => {
416
- const statefulParameter = this.parsedSettings.statefulParameters.get(key);
417
- if (!statefulParameter) {
418
- throw new Error(`Stateful parameter ${key} was not found`);
419
- }
413
+ for (const [idx, refreshedParams] of allParameters.entries()) {
414
+ await Promise.all(sortedEntries.map(async ([key, desiredValue]) => {
415
+ const statefulParameter = this.parsedSettings.statefulParameters.get(key);
416
+ if (!statefulParameter) {
417
+ throw new Error(`Stateful parameter ${key} was not found`);
418
+ }
420
419
 
421
- (result as Record<string, unknown>)[key] = await statefulParameter.refresh(desiredValue ?? null, allParameters)
422
- }))
420
+ (result[idx][key] as T[keyof T] | null) = await statefulParameter.refresh(desiredValue ?? null, refreshedParams)
421
+ }))
422
+ }
423
423
 
424
424
  return result;
425
425
  }