Skip to content

Commit 59fdf3a

Browse files
committed
feat: Allow resources with stateful parameters to allow multple as well
1 parent 8a7fabe commit 59fdf3a

File tree

3 files changed

+78
-66
lines changed

3 files changed

+78
-66
lines changed

src/plan/plan.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,56 @@ export class Plan<T extends StringIndexedObject> {
229229
return this.coreParameters.type
230230
}
231231

232+
/**
233+
* When multiples of the same resource are allowed, this matching function will match a given config with one of the
234+
* existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
235+
* the application name and location to match it to our desired configs name and location.
236+
*
237+
* @param params
238+
* @private
239+
*/
240+
private static matchCurrentParameters<T extends StringIndexedObject>(params: {
241+
desired: Partial<T> | null,
242+
currentArray: Partial<T>[] | null,
243+
state: Partial<T> | null,
244+
settings: ParsedResourceSettings<T>,
245+
isStateful: boolean,
246+
}): Partial<T> | null {
247+
const {
248+
desired,
249+
currentArray,
250+
state,
251+
settings,
252+
isStateful
253+
} = params;
254+
255+
if (!settings.allowMultiple) {
256+
return currentArray?.[0] ?? null;
257+
}
258+
259+
if (!currentArray) {
260+
return null;
261+
}
262+
263+
const { matcher: parameterMatcher, id } = settings;
264+
const matcher = (desired: Partial<T>, currentArray: Partial<T>[]): Partial<T> | undefined => {
265+
const matched = currentArray.filter((c) => parameterMatcher(desired, c))
266+
if (matched.length > 0) {
267+
console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`)
268+
}
269+
270+
return matched[0];
271+
}
272+
273+
if (isStateful) {
274+
return state
275+
? matcher(state, currentArray) ?? null
276+
: null
277+
}
278+
279+
return matcher(desired!, currentArray) ?? null;
280+
}
281+
232282
/**
233283
* Only keep relevant params for the plan. We don't want to change settings that were not already
234284
* defined.

src/resource/parsed-resource-settings.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,10 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
188188
}
189189
}
190190

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`)
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.`)
194195
}
195196

196197
const schema = this.settings.schema as JSONSchemaType<any>;

src/resource/resource-controller.ts

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ export class ResourceController<T extends StringIndexedObject> {
119119
// Parse data from the user supplied config
120120
const parsedConfig = new ConfigParser(desired, state, this.parsedSettings.statefulParameters)
121121
const {
122-
allParameters,
123122
allNonStatefulParameters,
124123
allStatefulParameters,
125124
} = parsedConfig;
@@ -130,7 +129,6 @@ export class ResourceController<T extends StringIndexedObject> {
130129
// Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
131130
if (currentArray === null
132131
|| currentArray === undefined
133-
|| this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
134132
|| currentArray.length === 0
135133
|| currentArray.filter(Boolean).length === 0
136134
) {
@@ -144,13 +142,13 @@ export class ResourceController<T extends StringIndexedObject> {
144142
});
145143
}
146144

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);
150148

151149
return Plan.calculate({
152150
desired,
153-
currentArray: [{ ...currentArray[0], ...statefulCurrentParameters }] as Partial<T>[],
151+
currentArray: currentArray.map((c, idx) => ({ ...c, ...statefulCurrentParameters[idx] })),
154152
state,
155153
core,
156154
settings: this.parsedSettings,
@@ -249,26 +247,21 @@ export class ResourceController<T extends StringIndexedObject> {
249247

250248
if (currentParametersArray === null
251249
|| currentParametersArray === undefined
252-
|| this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
253250
|| currentParametersArray.filter(Boolean).length === 0
254251
) {
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;
252+
return [];
263253
}
264254

265-
const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, parametersToRefresh);
266-
const resultParameters = { ...currentParametersArray[0], ...statefulCurrentParameters };
255+
const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray);
256+
const resultParametersArray = currentParametersArray
257+
?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] }))
267258

268-
await this.applyTransformParameters(resultParameters, true);
269-
this.removeDefaultValues(resultParameters, parameters)
259+
for (const result of resultParametersArray) {
260+
await this.applyTransformParameters(result, true);
261+
this.removeDefaultValues(result, parameters);
262+
}
270263

271-
return [{ core, parameters: resultParameters }];
264+
return resultParametersArray?.map((r) => ({ core, parameters: r }))
272265
}
273266

274267
private async applyCreate(plan: Plan<T>): Promise<void> {
@@ -410,21 +403,23 @@ ${JSON.stringify(refresh, null, 2)}
410403

411404
// Refresh stateful parameters
412405
// 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> = {}
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 }, () => ({}))
415408
const sortedEntries = Object.entries(statefulParametersConfig)
416409
.sort(
417410
([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)!
418411
)
419412

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-
}
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+
}
425419

426-
(result as Record<string, unknown>)[key] = await statefulParameter.refresh(desiredValue ?? null, allParameters)
427-
}))
420+
(result[idx][key] as T[keyof T] | null) = await statefulParameter.refresh(desiredValue ?? null, refreshedParams)
421+
}))
422+
}
428423

429424
return result;
430425
}
@@ -461,39 +456,5 @@ ${JSON.stringify(refresh, null, 2)}
461456
? Object.keys((this.settings.schema as any)?.properties)
462457
: Object.keys(this.parsedSettings.parameterSettings);
463458
}
464-
465-
/**
466-
* When multiples of the same resource are allowed, this matching function will match a given config with one of the
467-
* existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
468-
* the application name and location to match it to our desired configs name and location.
469-
*
470-
* @param params
471-
* @private
472-
*/
473-
private matchParameters(
474-
desired: Partial<T> | null,
475-
currentArray: Partial<T>[] | null
476-
): Partial<T> | null {
477-
if (!this.parsedSettings.allowMultiple) {
478-
return currentArray?.[0] ?? null;
479-
}
480-
481-
if (!currentArray) {
482-
return null;
483-
}
484-
485-
const { matcher: parameterMatcher, id } = this.parsedSettings;
486-
const matcher = (desired: Partial<T>, currentArray: Partial<T>[]): Partial<T> | undefined => {
487-
const matched = currentArray.filter((c) => parameterMatcher(desired, c))
488-
if (matched.length > 0) {
489-
console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`)
490-
}
491-
492-
return matched[0];
493-
}
494-
495-
return matcher(desired!, currentArray) ?? null;
496-
}
497-
498459
}
499460

0 commit comments

Comments
 (0)