codify-plugin-lib 1.0.43 → 1.0.45

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.
@@ -14,7 +14,7 @@ export class Plugin {
14
14
  resourceDefinitions: [...this.resources.values()]
15
15
  .map((r) => ({
16
16
  type: r.typeId,
17
- dependencies: r.getDependencyTypeIds(),
17
+ dependencies: r.dependencies,
18
18
  }))
19
19
  };
20
20
  }
@@ -1,6 +1,5 @@
1
1
  import { StatefulParameter } from './stateful-parameter.js';
2
2
  import { ResourceOperation, StringIndexedObject } from 'codify-schemas';
3
- import { Resource } from './resource.js';
4
3
  export type ErrorMessage = string;
5
4
  export interface ResourceParameterConfiguration {
6
5
  planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
@@ -9,7 +8,7 @@ export interface ResourceParameterConfiguration {
9
8
  export interface ResourceConfiguration<T extends StringIndexedObject> {
10
9
  type: string;
11
10
  callStatefulParameterRemoveOnDestroy?: boolean;
12
- dependencies?: Resource<any>[];
11
+ dependencies?: string[];
13
12
  statefulParameters?: Array<StatefulParameter<T, T[keyof T]>>;
14
13
  parameterConfigurations?: Partial<Record<keyof T, ResourceParameterConfiguration>>;
15
14
  }
@@ -6,11 +6,10 @@ import { ParameterConfiguration } from './plan-types.js';
6
6
  export declare abstract class Resource<T extends StringIndexedObject> {
7
7
  readonly typeId: string;
8
8
  readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
9
- readonly dependencies: Resource<any>[];
9
+ readonly dependencies: string[];
10
10
  readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>;
11
- private readonly options;
11
+ readonly configuration: ResourceConfiguration<T>;
12
12
  protected constructor(configuration: ResourceConfiguration<T>);
13
- getDependencyTypeIds(): string[];
14
13
  onInitialize(): Promise<void>;
15
14
  plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>>;
16
15
  apply(plan: Plan<T>): Promise<void>;
@@ -6,17 +6,14 @@ export class Resource {
6
6
  statefulParameters;
7
7
  dependencies;
8
8
  parameterConfigurations;
9
- options;
9
+ configuration;
10
10
  constructor(configuration) {
11
11
  this.validateResourceConfiguration(configuration);
12
12
  this.typeId = configuration.type;
13
13
  this.statefulParameters = new Map(configuration.statefulParameters?.map((sp) => [sp.name, sp]));
14
14
  this.parameterConfigurations = this.generateParameterConfigurations(configuration);
15
15
  this.dependencies = configuration.dependencies ?? [];
16
- this.options = configuration;
17
- }
18
- getDependencyTypeIds() {
19
- return this.dependencies.map((d) => d.typeId);
16
+ this.configuration = configuration;
20
17
  }
21
18
  async onInitialize() { }
22
19
  async plan(desiredConfig) {
@@ -39,8 +36,17 @@ export class Resource {
39
36
  for (const statefulParameter of statefulParameters) {
40
37
  const desiredValue = desiredParameters[statefulParameter.name];
41
38
  let currentValue = await statefulParameter.refresh() ?? undefined;
42
- if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
43
- currentValue = currentValue.filter((p) => desiredValue?.includes(p));
39
+ if (Array.isArray(currentValue)
40
+ && Array.isArray(desiredValue)
41
+ && !planConfiguration.statefulMode
42
+ && !statefulParameter.configuration.disableStatelessModeArrayFiltering) {
43
+ currentValue = currentValue.filter((c) => desiredValue?.some((d) => {
44
+ const pc = planConfiguration?.parameterConfigurations?.[statefulParameter.name];
45
+ if (pc && pc.isElementEqual) {
46
+ return pc.isElementEqual(d, c);
47
+ }
48
+ return d === c;
49
+ }));
44
50
  }
45
51
  currentParameters[statefulParameter.name] = currentValue;
46
52
  }
@@ -106,7 +112,7 @@ export class Resource {
106
112
  }
107
113
  }
108
114
  async _applyDestroy(plan) {
109
- if (this.options.callStatefulParameterRemoveOnDestroy) {
115
+ if (this.configuration.callStatefulParameterRemoveOnDestroy) {
110
116
  const statefulParameterChanges = plan.changeSet.parameterChanges
111
117
  .filter((pc) => this.statefulParameters.has(pc.name));
112
118
  for (const parameterChange of statefulParameterChanges) {
@@ -137,7 +143,7 @@ export class Resource {
137
143
  validateResourceConfiguration(data) {
138
144
  if (data.parameterConfigurations && data.statefulParameters) {
139
145
  const parameters = [...Object.keys(data.parameterConfigurations)];
140
- const statefulParameterSet = new Set(Object.keys(data.statefulParameters));
146
+ const statefulParameterSet = new Set(data.statefulParameters.map((sp) => sp.name));
141
147
  const intersection = parameters.some((p) => statefulParameterSet.has(p));
142
148
  if (intersection) {
143
149
  throw new Error(`Resource ${this.typeId} cannot declare a parameter as both stateful and non-stateful`);
@@ -150,7 +156,7 @@ export class Resource {
150
156
  }
151
157
  const refreshKeys = new Set(Object.keys(refresh));
152
158
  if (!setsEqual(desiredKeys, refreshKeys)) {
153
- throw new Error(`Resource ${this.options.type}
159
+ throw new Error(`Resource ${this.configuration.type}
154
160
  refresh() must return back exactly the keys that were provided
155
161
  Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
156
162
  Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
@@ -3,6 +3,7 @@ import { StringIndexedObject } from 'codify-schemas';
3
3
  export interface StatefulParameterConfiguration<T> {
4
4
  name: keyof T;
5
5
  isEqual?: (desired: any, current: any) => boolean;
6
+ disableStatelessModeArrayFiltering?: boolean;
6
7
  }
7
8
  export interface ArrayStatefulParameterConfiguration<T> extends StatefulParameterConfiguration<T> {
8
9
  isEqual?: (desired: any[], current: any[]) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -126,7 +126,6 @@ export class ChangeSet<T extends StringIndexedObject> {
126
126
  const _desired = { ...desired };
127
127
  const _current = { ...current };
128
128
 
129
-
130
129
  for (const [k, v] of Object.entries(_current)) {
131
130
  if (_desired[k] == null) {
132
131
  parameterChangeSet.push({
@@ -34,10 +34,6 @@ export class Plan<T extends StringIndexedObject> {
34
34
  .map(([k, v]) => k)
35
35
  );
36
36
 
37
-
38
- // TODO: After adding in state files, need to calculate deletes here
39
- // Where current config exists and state config exists but desired config doesn't
40
-
41
37
  // Explanation: This calculates the change set of the parameters between the
42
38
  // two configs and then passes it to ChangeSet to calculate the overall
43
39
  // operation for the resource
@@ -28,7 +28,7 @@ export class Plugin {
28
28
  resourceDefinitions: [...this.resources.values()]
29
29
  .map((r) => ({
30
30
  type: r.typeId,
31
- dependencies: r.getDependencyTypeIds(),
31
+ dependencies: r.dependencies,
32
32
  }))
33
33
  }
34
34
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { StatefulParameter, StatefulParameterConfiguration } from './stateful-parameter.js';
2
+ import { ArrayStatefulParameter, StatefulParameter, StatefulParameterConfiguration } from './stateful-parameter.js';
3
3
  import { Plan } from './plan.js';
4
4
  import { spy } from 'sinon';
5
5
  import { ResourceOperation } from 'codify-schemas';
@@ -157,4 +157,45 @@ describe('Resource parameters tests', () => {
157
157
  }
158
158
  })
159
159
  })
160
+
161
+ it('Uses isElementEqual for stateless mode filtering if available', async () => {
162
+ const statefulParameter = new class extends ArrayStatefulParameter<TestConfig, string> {
163
+ constructor() {
164
+ super({
165
+ name: 'propA',
166
+ isElementEqual: (desired, current) => current.includes(desired),
167
+ });
168
+ }
169
+
170
+ async refresh(): Promise<any | null> {
171
+ return ['3.11.9']
172
+ }
173
+
174
+ async applyAddItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
175
+ async applyRemoveItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
176
+ }
177
+
178
+ const statefulParameterSpy = spy(statefulParameter);
179
+
180
+ const resource = new class extends TestResource {
181
+ constructor() {
182
+ super({
183
+ type: 'resource',
184
+ statefulParameters: [statefulParameterSpy],
185
+ });
186
+ }
187
+
188
+ async refresh(): Promise<Partial<TestConfig> | null> {
189
+ return {};
190
+ }
191
+ }
192
+
193
+ const plan = await resource.plan({ type: 'resource', propA: ['3.11'] } as any)
194
+
195
+ expect(plan).toMatchObject({
196
+ changeSet: {
197
+ operation: ResourceOperation.NOOP,
198
+ }
199
+ })
200
+ })
160
201
  })
@@ -1,6 +1,5 @@
1
1
  import { StatefulParameter } from './stateful-parameter.js';
2
2
  import { ResourceOperation, StringIndexedObject } from 'codify-schemas';
3
- import { Resource } from './resource.js';
4
3
 
5
4
  export type ErrorMessage = string;
6
5
 
@@ -30,7 +29,7 @@ export interface ResourceConfiguration<T extends StringIndexedObject> {
30
29
  * Defaults to false.
31
30
  */
32
31
  callStatefulParameterRemoveOnDestroy?: boolean,
33
- dependencies?: Resource<any>[];
32
+ dependencies?: string[];
34
33
  statefulParameters?: Array<StatefulParameter<T, T[keyof T]>>;
35
34
  parameterConfigurations?: Partial<Record<keyof T, ResourceParameterConfiguration>>
36
35
  }
@@ -4,6 +4,7 @@ import { spy } from 'sinon';
4
4
  import { Plan } from './plan.js';
5
5
  import { describe, expect, it } from 'vitest'
6
6
  import { ResourceConfiguration, ValidationResult } from './resource-types.js';
7
+ import { StatefulParameter } from './stateful-parameter.js';
7
8
 
8
9
  export interface TestConfig extends StringIndexedObject {
9
10
  propA: string;
@@ -40,7 +41,8 @@ export class TestResource extends Resource<TestConfig> {
40
41
  }
41
42
 
42
43
  describe('Resource tests', () => {
43
- it('plans correctly', async () => {
44
+
45
+ it('Plans successfully', async () => {
44
46
  const resource = new class extends TestResource {
45
47
  constructor() {
46
48
  super({ type: 'type' });
@@ -212,4 +214,83 @@ describe('Resource tests', () => {
212
214
 
213
215
  expect(resourceSpy.applyModify.calledTwice).to.be.true;
214
216
  })
217
+
218
+ it('Validates the resource configuration correct (pass)', () => {
219
+ const parameter = new class extends StatefulParameter<TestConfig, string> {
220
+ constructor() {
221
+ super({
222
+ name: 'propC',
223
+ });
224
+ }
225
+
226
+ async refresh(): Promise<string | null> {
227
+ return null;
228
+ }
229
+ applyAdd(valueToAdd: string, plan: Plan<TestConfig>): Promise<void> {
230
+ throw new Error('Method not implemented.');
231
+ }
232
+ applyModify(newValue: string, previousValue: string, allowDeletes: boolean, plan: Plan<TestConfig>): Promise<void> {
233
+ throw new Error('Method not implemented.');
234
+ }
235
+ applyRemove(valueToRemove: string, plan: Plan<TestConfig>): Promise<void> {
236
+ throw new Error('Method not implemented.');
237
+ }
238
+ }
239
+
240
+ expect(() => new class extends TestResource {
241
+ constructor() {
242
+ super({
243
+ type: 'type',
244
+ dependencies: ['homebrew', 'python'],
245
+ statefulParameters: [
246
+ parameter
247
+ ],
248
+ parameterConfigurations: {
249
+ propA: { planOperation: ResourceOperation.MODIFY },
250
+ propC: { isEqual: (a, b) => true },
251
+ }
252
+ });
253
+ }
254
+ }).to.not.throw;
255
+ })
256
+
257
+ it('Validates the resource configuration correct (fail)', () => {
258
+ const parameter = new class extends StatefulParameter<TestConfig, string> {
259
+ constructor() {
260
+ super({
261
+ name: 'propC',
262
+ });
263
+ }
264
+
265
+ async refresh(): Promise<string | null> {
266
+ return null;
267
+ }
268
+ applyAdd(valueToAdd: string, plan: Plan<TestConfig>): Promise<void> {
269
+ throw new Error('Method not implemented.');
270
+ }
271
+ applyModify(newValue: string, previousValue: string, allowDeletes: boolean, plan: Plan<TestConfig>): Promise<void> {
272
+ throw new Error('Method not implemented.');
273
+ }
274
+ applyRemove(valueToRemove: string, plan: Plan<TestConfig>): Promise<void> {
275
+ throw new Error('Method not implemented.');
276
+ }
277
+ }
278
+
279
+ expect(() => new class extends TestResource {
280
+ constructor() {
281
+ super({
282
+ type: 'type',
283
+ dependencies: ['homebrew', 'python'],
284
+ statefulParameters: [
285
+ parameter
286
+ ],
287
+ parameterConfigurations: {
288
+ propA: { planOperation: ResourceOperation.MODIFY },
289
+ propC: { isEqual: (a, b) => true },
290
+ }
291
+ });
292
+ }
293
+ }).to.not.throw;
294
+ })
295
+
215
296
  });
@@ -17,10 +17,9 @@ export abstract class Resource<T extends StringIndexedObject> {
17
17
 
18
18
  readonly typeId: string;
19
19
  readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
20
- readonly dependencies: Resource<any>[]; // TODO: Change this to a string
20
+ readonly dependencies: string[]; // TODO: Change this to a string
21
21
  readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>
22
-
23
- private readonly options: ResourceConfiguration<T>;
22
+ readonly configuration: ResourceConfiguration<T>;
24
23
 
25
24
  protected constructor(configuration: ResourceConfiguration<T>) {
26
25
  this.validateResourceConfiguration(configuration);
@@ -30,11 +29,7 @@ export abstract class Resource<T extends StringIndexedObject> {
30
29
  this.parameterConfigurations = this.generateParameterConfigurations(configuration);
31
30
 
32
31
  this.dependencies = configuration.dependencies ?? [];
33
- this.options = configuration;
34
- }
35
-
36
- getDependencyTypeIds(): string[] {
37
- return this.dependencies.map((d) => d.typeId)
32
+ this.configuration = configuration;
38
33
  }
39
34
 
40
35
  async onInitialize(): Promise<void> {}
@@ -79,8 +74,18 @@ export abstract class Resource<T extends StringIndexedObject> {
79
74
  let currentValue = await statefulParameter.refresh() ?? undefined;
80
75
 
81
76
  // In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
82
- if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
83
- currentValue = currentValue.filter((p) => desiredValue?.includes(p)) as any;
77
+ if (Array.isArray(currentValue)
78
+ && Array.isArray(desiredValue)
79
+ && !planConfiguration.statefulMode
80
+ && !statefulParameter.configuration.disableStatelessModeArrayFiltering
81
+ ) {
82
+ currentValue = currentValue.filter((c) => desiredValue?.some((d) => {
83
+ const pc = planConfiguration?.parameterConfigurations?.[statefulParameter.name];
84
+ if (pc && pc.isElementEqual) {
85
+ return pc.isElementEqual(d, c);
86
+ }
87
+ return d === c;
88
+ })) as any;
84
89
  }
85
90
 
86
91
  currentParameters[statefulParameter.name] = currentValue;
@@ -166,7 +171,7 @@ export abstract class Resource<T extends StringIndexedObject> {
166
171
  private async _applyDestroy(plan: Plan<T>): Promise<void> {
167
172
  // If this option is set (defaults to false), then stateful parameters need to be destroyed
168
173
  // as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
169
- if (this.options.callStatefulParameterRemoveOnDestroy) {
174
+ if (this.configuration.callStatefulParameterRemoveOnDestroy) {
170
175
  const statefulParameterChanges = plan.changeSet.parameterChanges
171
176
  .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
172
177
  for (const parameterChange of statefulParameterChanges) {
@@ -205,16 +210,18 @@ export abstract class Resource<T extends StringIndexedObject> {
205
210
  }
206
211
 
207
212
  private validateResourceConfiguration(data: ResourceConfiguration<T>) {
208
- // A parameter cannot be both stateful and stateless
213
+ // Stateful parameters are configured within the object not in the resource.
209
214
  if (data.parameterConfigurations && data.statefulParameters) {
210
215
  const parameters = [...Object.keys(data.parameterConfigurations)];
211
- const statefulParameterSet = new Set(Object.keys(data.statefulParameters));
216
+ const statefulParameterSet = new Set(data.statefulParameters.map((sp) => sp.name));
212
217
 
213
218
  const intersection = parameters.some((p) => statefulParameterSet.has(p));
214
219
  if (intersection) {
215
220
  throw new Error(`Resource ${this.typeId} cannot declare a parameter as both stateful and non-stateful`);
216
221
  }
217
222
  }
223
+
224
+
218
225
  }
219
226
 
220
227
  private validateRefreshResults(refresh: Partial<T> | null, desiredKeys: Set<keyof T>) {
@@ -226,7 +233,7 @@ export abstract class Resource<T extends StringIndexedObject> {
226
233
 
227
234
  if (!setsEqual(desiredKeys, refreshKeys)) {
228
235
  throw new Error(
229
- `Resource ${this.options.type}
236
+ `Resource ${this.configuration.type}
230
237
  refresh() must return back exactly the keys that were provided
231
238
  Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
232
239
  Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
@@ -4,6 +4,17 @@ import { StringIndexedObject } from 'codify-schemas';
4
4
  export interface StatefulParameterConfiguration<T> {
5
5
  name: keyof T;
6
6
  isEqual?: (desired: any, current: any) => boolean;
7
+
8
+ /**
9
+ * In stateless mode, array refresh results (current) will be automatically filtered by the user config (desired).
10
+ * This is done to ensure that for modify operations, stateless mode will not try to delete existing resources.
11
+ *
12
+ * Ex: System has python 3.11.9 and 3.12.7 installed (current). Desired is 3.11. Without filtering 3.12.7 will be deleted
13
+ * in the next modify
14
+ *
15
+ * Set this flag to true to disable this behaviour
16
+ */
17
+ disableStatelessModeArrayFiltering?: boolean;
7
18
  }
8
19
 
9
20
  export interface ArrayStatefulParameterConfiguration<T> extends StatefulParameterConfiguration<T> {