codify-plugin-lib 1.0.99 → 1.0.101
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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plan/change-set.d.ts +2 -2
- package/dist/plan/plan.js +16 -8
- package/dist/resource/config-parser.d.ts +2 -2
- package/dist/resource/parsed-resource-settings.d.ts +17 -4
- package/dist/resource/parsed-resource-settings.js +25 -3
- package/dist/resource/resource-settings.d.ts +70 -6
- package/dist/resource/resource-settings.js +21 -5
- package/dist/stateful-parameter/stateful-parameter-controller.d.ts +21 -0
- package/dist/stateful-parameter/stateful-parameter-controller.js +82 -0
- package/dist/stateful-parameter/stateful-parameter.d.ts +144 -0
- package/dist/stateful-parameter/stateful-parameter.js +43 -0
- package/dist/utils/utils.d.ts +1 -2
- package/dist/utils/utils.js +3 -2
- package/package.json +4 -2
- package/src/index.ts +1 -1
- package/src/plan/change-set.ts +5 -5
- package/src/plan/plan.test.ts +78 -0
- package/src/plan/plan.ts +24 -11
- package/src/resource/config-parser.ts +3 -3
- package/src/resource/parsed-resource-settings.ts +56 -9
- package/src/resource/resource-settings.test.ts +101 -0
- package/src/resource/resource-settings.ts +99 -9
- package/src/{resource/stateful-parameter.test.ts → stateful-parameter/stateful-parameter-controller.test.ts} +90 -13
- package/src/stateful-parameter/stateful-parameter-controller.ts +112 -0
- package/src/{resource → stateful-parameter}/stateful-parameter.ts +9 -67
- package/src/utils/test-utils.test.ts +1 -1
- package/src/utils/utils.ts +9 -4
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
TestResource,
|
|
9
9
|
TestStatefulParameter
|
|
10
10
|
} from '../utils/test-utils.test.js';
|
|
11
|
-
import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from '
|
|
12
|
-
import { ResourceController } from '
|
|
11
|
+
import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from '../resource/resource-settings.js';
|
|
12
|
+
import { ResourceController } from '../resource/resource-controller.js';
|
|
13
|
+
import { StatefulParameterController } from './stateful-parameter-controller.js';
|
|
13
14
|
|
|
14
15
|
describe('Stateful parameter tests', () => {
|
|
15
16
|
it('addItem is called the correct number of times', async () => {
|
|
@@ -20,11 +21,12 @@ describe('Stateful parameter tests', () => {
|
|
|
20
21
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
|
|
21
22
|
expect(plan.changeSet.parameterChanges.length).to.eq(1);
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
-
|
|
24
|
+
const parameter = spy(new TestArrayStatefulParameter());
|
|
25
|
+
const controller = new StatefulParameterController(parameter);
|
|
26
|
+
await controller.add((plan.desiredConfig! as any).propZ, plan);
|
|
25
27
|
|
|
26
|
-
expect(
|
|
27
|
-
expect(
|
|
28
|
+
expect(parameter.addItem.callCount).to.eq(3);
|
|
29
|
+
expect(parameter.removeItem.called).to.be.false;
|
|
28
30
|
})
|
|
29
31
|
|
|
30
32
|
it('applyRemoveItem is called the correct number of times', async () => {
|
|
@@ -38,11 +40,12 @@ describe('Stateful parameter tests', () => {
|
|
|
38
40
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY);
|
|
39
41
|
expect(plan.changeSet.parameterChanges.length).to.eq(1);
|
|
40
42
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
+
const parameter = spy(new TestArrayStatefulParameter());
|
|
44
|
+
const controller = new StatefulParameterController(parameter);
|
|
45
|
+
await controller.remove((plan.currentConfig as any).propZ, plan);
|
|
43
46
|
|
|
44
|
-
expect(
|
|
45
|
-
expect(
|
|
47
|
+
expect(parameter.addItem.called).to.be.false;
|
|
48
|
+
expect(parameter.removeItem.callCount).to.eq(3);
|
|
46
49
|
})
|
|
47
50
|
|
|
48
51
|
it('In stateless mode only applyAddItem is called only for modifies', async () => {
|
|
@@ -61,8 +64,10 @@ describe('Stateful parameter tests', () => {
|
|
|
61
64
|
operation: ParameterOperation.MODIFY,
|
|
62
65
|
})
|
|
63
66
|
|
|
67
|
+
|
|
64
68
|
const testParameter = spy(parameter);
|
|
65
|
-
|
|
69
|
+
const controller = new StatefulParameterController(testParameter);
|
|
70
|
+
await controller.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
|
|
66
71
|
|
|
67
72
|
expect(testParameter.addItem.calledThrice).to.be.true;
|
|
68
73
|
expect(testParameter.removeItem.called).to.be.false;
|
|
@@ -92,7 +97,8 @@ describe('Stateful parameter tests', () => {
|
|
|
92
97
|
operation: ParameterOperation.MODIFY,
|
|
93
98
|
})
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
const controller = new StatefulParameterController(testParameter);
|
|
101
|
+
await controller.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
|
|
96
102
|
|
|
97
103
|
expect(testParameter.addItem.calledOnce).to.be.true;
|
|
98
104
|
expect(testParameter.removeItem.called).to.be.false;
|
|
@@ -151,7 +157,78 @@ describe('Stateful parameter tests', () => {
|
|
|
151
157
|
nodeVersions: ['20.15'],
|
|
152
158
|
} as any)
|
|
153
159
|
|
|
154
|
-
|
|
160
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('Accepts a string equals value', async () => {
|
|
164
|
+
const testParameter = spy(new class extends TestStatefulParameter {
|
|
165
|
+
getSettings(): ParameterSetting {
|
|
166
|
+
return {
|
|
167
|
+
type: 'string',
|
|
168
|
+
isEqual: 'version'
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async refresh(): Promise<any> {
|
|
173
|
+
return '20.15.0';
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const resource = new class extends TestResource {
|
|
178
|
+
getSettings(): ResourceSettings<any> {
|
|
179
|
+
return {
|
|
180
|
+
id: 'type',
|
|
181
|
+
parameterSettings: { propA: { type: 'stateful', definition: testParameter } }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async refresh(): Promise<Partial<any> | null> {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const controller = new ResourceController(resource);
|
|
191
|
+
const plan = await controller.plan({
|
|
192
|
+
propA: '20.15',
|
|
193
|
+
} as any)
|
|
194
|
+
|
|
195
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('Accepts a string isElementEquals value', async () => {
|
|
199
|
+
const testParameter = spy(new class extends TestStatefulParameter {
|
|
200
|
+
getSettings(): ParameterSetting {
|
|
201
|
+
return {
|
|
202
|
+
type: 'array',
|
|
203
|
+
isElementEqual: 'version'
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async refresh(): Promise<any> {
|
|
208
|
+
return [
|
|
209
|
+
'20.15.0',
|
|
210
|
+
'20.18.0'
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const resource = new class extends TestResource {
|
|
216
|
+
getSettings(): ResourceSettings<any> {
|
|
217
|
+
return {
|
|
218
|
+
id: 'type',
|
|
219
|
+
parameterSettings: { propA: { type: 'stateful', definition: testParameter } }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async refresh(): Promise<Partial<any> | null> {
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const controller = new ResourceController(resource);
|
|
229
|
+
const plan = await controller.plan({
|
|
230
|
+
propA: ['20.15', '20.18'],
|
|
231
|
+
} as any)
|
|
155
232
|
|
|
156
233
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
157
234
|
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
|
|
3
|
+
import { Plan } from '../plan/plan.js';
|
|
4
|
+
import { ParsedArrayParameterSetting, ParsedParameterSetting, } from '../resource/parsed-resource-settings.js';
|
|
5
|
+
import {
|
|
6
|
+
ArrayParameterSetting,
|
|
7
|
+
ParameterSetting,
|
|
8
|
+
resolveEqualsFn,
|
|
9
|
+
resolveFnFromEqualsFnOrString
|
|
10
|
+
} from '../resource/resource-settings.js';
|
|
11
|
+
import { ArrayStatefulParameter, StatefulParameter } from './stateful-parameter.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This class is analogous to what {@link ResourceController} is for {@link Resource}
|
|
15
|
+
* It's a bit messy because this class supports both {@link StatefulParameter} and {@link ArrayStatefulParameter}
|
|
16
|
+
*/
|
|
17
|
+
export class StatefulParameterController<T extends StringIndexedObject, V extends T[keyof T]> {
|
|
18
|
+
readonly sp: ArrayStatefulParameter<T, V> | StatefulParameter<T, V>
|
|
19
|
+
readonly settings: ParameterSetting;
|
|
20
|
+
readonly parsedSettings: ParsedParameterSetting
|
|
21
|
+
|
|
22
|
+
private readonly isArrayStatefulParameter: boolean;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
statefulParameter: ArrayStatefulParameter<T, V> | StatefulParameter<T, V>
|
|
26
|
+
) {
|
|
27
|
+
this.sp = statefulParameter;
|
|
28
|
+
this.settings = statefulParameter.getSettings();
|
|
29
|
+
this.isArrayStatefulParameter = this.calculateIsArrayStatefulParameter();
|
|
30
|
+
|
|
31
|
+
this.parsedSettings = (this.isArrayStatefulParameter || this.settings.type === 'array') ? {
|
|
32
|
+
...this.settings,
|
|
33
|
+
isEqual: resolveEqualsFn(this.settings),
|
|
34
|
+
isElementEqual: resolveFnFromEqualsFnOrString((this.settings as ArrayParameterSetting).isElementEqual)
|
|
35
|
+
?? ((a: unknown, b: unknown) => a === b)
|
|
36
|
+
} as ParsedParameterSetting : {
|
|
37
|
+
...this.settings,
|
|
38
|
+
isEqual: resolveEqualsFn(this.settings),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async refresh(desired: V | null, config: Partial<T>): Promise<V | null> {
|
|
43
|
+
return await this.sp.refresh(desired as any, config) as V | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async add(valueToAdd: V, plan: Plan<T>): Promise<void> {
|
|
47
|
+
if (!this.isArrayStatefulParameter) {
|
|
48
|
+
const sp = this.sp as StatefulParameter<T, V>;
|
|
49
|
+
return sp.add(valueToAdd, plan);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sp = this.sp as ArrayStatefulParameter<any, any>;
|
|
53
|
+
const valuesToAdd = valueToAdd as Array<any>;
|
|
54
|
+
for (const value of valuesToAdd) {
|
|
55
|
+
await sp.addItem(value, plan);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async modify(newValue: V, previousValue: V, plan: Plan<T>): Promise<void> {
|
|
60
|
+
if (!this.isArrayStatefulParameter) {
|
|
61
|
+
const sp = this.sp as StatefulParameter<T, V>;
|
|
62
|
+
return sp.modify(newValue, previousValue, plan);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sp = this.sp as ArrayStatefulParameter<any, any>;
|
|
66
|
+
const settings = this.parsedSettings as ParsedArrayParameterSetting;
|
|
67
|
+
const newValues = newValue as Array<unknown>[];
|
|
68
|
+
const previousValues = previousValue as Array<unknown>[];
|
|
69
|
+
|
|
70
|
+
// TODO: I don't think this works with duplicate elements. Solve at another time
|
|
71
|
+
const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
|
|
72
|
+
if (settings.isElementEqual) {
|
|
73
|
+
return settings.isElementEqual!(n, p);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return n === p;
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
|
|
80
|
+
if (settings.isElementEqual) {
|
|
81
|
+
return settings.isElementEqual!(n, p);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return n === p;
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
for (const value of valuesToAdd) {
|
|
88
|
+
await sp.addItem(value, plan)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const value of valuesToRemove) {
|
|
92
|
+
await sp.removeItem(value, plan)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async remove(valueToRemove: V, plan: Plan<T>): Promise<void> {
|
|
97
|
+
if (!this.isArrayStatefulParameter) {
|
|
98
|
+
const sp = this.sp as StatefulParameter<T, V>;
|
|
99
|
+
return sp.remove(valueToRemove, plan);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sp = this.sp as ArrayStatefulParameter<any, any>;
|
|
103
|
+
const valuesToRemove = valueToRemove as Array<any>;
|
|
104
|
+
for (const value of valuesToRemove) {
|
|
105
|
+
await sp.removeItem(value as V, plan);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private calculateIsArrayStatefulParameter() {
|
|
110
|
+
return Object.hasOwn(this.sp, 'addItem') && Object.hasOwn(this.sp, 'removeItem');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StringIndexedObject } from 'codify-schemas';
|
|
2
2
|
|
|
3
3
|
import { Plan } from '../plan/plan.js';
|
|
4
|
-
import { ArrayParameterSetting, ParameterSetting } from '
|
|
4
|
+
import { ArrayParameterSetting, ParameterSetting } from '../resource/resource-settings.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
|
|
@@ -101,83 +101,25 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
|
|
|
101
101
|
* - Homebrew formulas are arrays
|
|
102
102
|
* - Pyenv python versions are arrays
|
|
103
103
|
*/
|
|
104
|
-
export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V>
|
|
104
|
+
export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> {
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
* Parameter
|
|
107
|
+
* Parameter settings for the stateful parameter. Stateful parameters share the same parameter settings as
|
|
108
|
+
* regular parameters except that they cannot be of type 'stateful'. See {@link ParameterSetting} for more
|
|
109
|
+
* information on available settings. Type must be 'array'.
|
|
110
|
+
*
|
|
111
|
+
* @return The parameter settings
|
|
108
112
|
*/
|
|
109
113
|
getSettings(): ArrayParameterSetting {
|
|
110
114
|
return { type: 'array' }
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
/**
|
|
114
|
-
* It is not recommended to override the `add` method. A addItem helper method is available to operate on
|
|
115
|
-
* individual elements of the desired array. See {@link StatefulParameter.add} for more info.
|
|
116
|
-
*
|
|
117
|
-
* @param valuesToAdd The array of values to add
|
|
118
|
-
* @param plan The overall plan
|
|
119
|
-
*
|
|
120
|
-
*/
|
|
121
|
-
async add(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
|
|
122
|
-
for (const value of valuesToAdd) {
|
|
123
|
-
await this.addItem(value, plan);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* It is not recommended to override the `modify` method. `addItem` and `removeItem` will be called accordingly based
|
|
129
|
-
* on the modifications. See {@link StatefulParameter.modify} for more info.
|
|
130
|
-
*
|
|
131
|
-
* @param newValues The new array value
|
|
132
|
-
* @param previousValues The previous array value
|
|
133
|
-
* @param plan The overall plan
|
|
134
|
-
*/
|
|
135
|
-
async modify(newValues: V[], previousValues: V[], plan: Plan<T>): Promise<void> {
|
|
136
|
-
|
|
137
|
-
// TODO: I don't think this works with duplicate elements. Solve at another time
|
|
138
|
-
const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
|
|
139
|
-
if (this.getSettings()?.isElementEqual) {
|
|
140
|
-
return this.getSettings().isElementEqual!(n, p);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return n === p;
|
|
144
|
-
}));
|
|
145
|
-
|
|
146
|
-
const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
|
|
147
|
-
if (this.getSettings().isElementEqual) {
|
|
148
|
-
return this.getSettings().isElementEqual!(n, p);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return n === p;
|
|
152
|
-
}));
|
|
153
|
-
|
|
154
|
-
for (const value of valuesToAdd) {
|
|
155
|
-
await this.addItem(value, plan)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (const value of valuesToRemove) {
|
|
159
|
-
await this.removeItem(value, plan)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* It is not recommended to override the `remove` method. A removeItem helper method is available to operate on
|
|
165
|
-
* individual elements of the desired array. See {@link StatefulParameter.remove} for more info.
|
|
166
|
-
*
|
|
167
|
-
* @param valuesToAdd The array of values to add
|
|
168
|
-
* @param plan The overall plan
|
|
169
|
-
*
|
|
170
|
-
*/
|
|
171
|
-
async remove(valuesToRemove: V[], plan: Plan<T>): Promise<void> {
|
|
172
|
-
for (const value of valuesToRemove) {
|
|
173
|
-
await this.removeItem(value as V, plan);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
117
|
/**
|
|
178
118
|
* See {@link StatefulParameter.refresh} for more info.
|
|
179
119
|
*
|
|
180
120
|
* @param desired The desired value to refresh
|
|
121
|
+
* @param config The desired config
|
|
122
|
+
*
|
|
181
123
|
* @return The current value on the system or null if not found.
|
|
182
124
|
*/
|
|
183
125
|
abstract refresh(desired: V[] | null, config: Partial<T>): Promise<V[] | null>;
|
|
@@ -3,7 +3,7 @@ import { ResourceSettings } from '../resource/resource-settings.js';
|
|
|
3
3
|
import { Plan } from '../plan/plan.js';
|
|
4
4
|
import { Resource } from '../resource/resource.js';
|
|
5
5
|
import { CreatePlan, DestroyPlan } from '../plan/plan-types.js';
|
|
6
|
-
import { ArrayStatefulParameter, StatefulParameter } from '../
|
|
6
|
+
import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
|
|
7
7
|
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
8
8
|
|
|
9
9
|
export function testPlan<T extends StringIndexedObject>(params: {
|
package/src/utils/utils.ts
CHANGED
|
@@ -3,8 +3,6 @@ import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
|
|
|
3
3
|
import { SpawnOptions } from 'node:child_process';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
|
|
6
|
-
import { ArrayParameterSetting } from '../resource/resource-settings.js';
|
|
7
|
-
|
|
8
6
|
export enum SpawnStatus {
|
|
9
7
|
SUCCESS = 'success',
|
|
10
8
|
ERROR = 'error',
|
|
@@ -111,7 +109,11 @@ export function untildify(pathWithTilde: string) {
|
|
|
111
109
|
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
112
110
|
}
|
|
113
111
|
|
|
114
|
-
export function areArraysEqual(
|
|
112
|
+
export function areArraysEqual(
|
|
113
|
+
isElementEqual: ((desired: unknown, current: unknown) => boolean) | undefined,
|
|
114
|
+
desired: unknown,
|
|
115
|
+
current: unknown
|
|
116
|
+
): boolean {
|
|
115
117
|
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
116
118
|
throw new Error(`A non-array value:
|
|
117
119
|
|
|
@@ -133,7 +135,10 @@ Was provided even though type array was specified.
|
|
|
133
135
|
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
134
136
|
// an isElementEqual method. Time: O(n^2)
|
|
135
137
|
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
136
|
-
const idx = currentCopy.findIndex((e2) => (
|
|
138
|
+
const idx = currentCopy.findIndex((e2) => (
|
|
139
|
+
isElementEqual
|
|
140
|
+
?? ((a, b) => a === b))(desiredCopy[counter], e2
|
|
141
|
+
))
|
|
137
142
|
|
|
138
143
|
if (idx === -1) {
|
|
139
144
|
return false;
|