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.
- package/dist/entities/plugin.js +1 -1
- package/dist/entities/resource-types.d.ts +1 -2
- package/dist/entities/resource.d.ts +2 -3
- package/dist/entities/resource.js +16 -10
- package/dist/entities/stateful-parameter.d.ts +1 -0
- package/package.json +1 -1
- package/src/entities/change-set.ts +0 -1
- package/src/entities/plan.ts +0 -4
- package/src/entities/plugin.ts +1 -1
- package/src/entities/resource-parameters.test.ts +42 -1
- package/src/entities/resource-types.ts +1 -2
- package/src/entities/resource.test.ts +82 -1
- package/src/entities/resource.ts +21 -14
- package/src/entities/stateful-parameter.ts +11 -0
package/dist/entities/plugin.js
CHANGED
|
@@ -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?:
|
|
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:
|
|
9
|
+
readonly dependencies: string[];
|
|
10
10
|
readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>;
|
|
11
|
-
|
|
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
|
-
|
|
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.
|
|
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)
|
|
43
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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
package/src/entities/plan.ts
CHANGED
|
@@ -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
|
package/src/entities/plugin.ts
CHANGED
|
@@ -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?:
|
|
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
|
-
|
|
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
|
});
|
package/src/entities/resource.ts
CHANGED
|
@@ -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:
|
|
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.
|
|
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)
|
|
83
|
-
|
|
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.
|
|
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
|
-
//
|
|
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(
|
|
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.
|
|
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> {
|