codify-plugin-lib 1.0.76 → 1.0.77
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/.eslintrc.json +11 -4
- package/.github/workflows/release.yaml +19 -0
- package/.github/workflows/unit-test-ci.yaml +19 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +10 -10
- package/dist/index.js +9 -9
- package/dist/messages/handlers.d.ts +1 -1
- package/dist/messages/handlers.js +2 -1
- package/dist/plan/change-set.d.ts +37 -0
- package/dist/plan/change-set.js +146 -0
- package/dist/plan/plan-types.d.ts +23 -0
- package/dist/plan/plan-types.js +1 -0
- package/dist/plan/plan.d.ts +59 -0
- package/dist/plan/plan.js +228 -0
- package/dist/plugin/plugin.d.ts +17 -0
- package/dist/plugin/plugin.js +83 -0
- package/dist/resource/config-parser.d.ts +14 -0
- package/dist/resource/config-parser.js +48 -0
- package/dist/resource/parsed-resource-settings.d.ts +26 -0
- package/dist/resource/parsed-resource-settings.js +126 -0
- package/dist/resource/resource-controller.d.ts +30 -0
- package/dist/resource/resource-controller.js +247 -0
- package/dist/resource/resource-settings.d.ts +149 -0
- package/dist/resource/resource-settings.js +9 -0
- package/dist/resource/resource.d.ts +137 -0
- package/dist/resource/resource.js +44 -0
- package/dist/resource/stateful-parameter.d.ts +164 -0
- package/dist/resource/stateful-parameter.js +94 -0
- package/dist/utils/utils.d.ts +19 -3
- package/dist/utils/utils.js +52 -3
- package/package.json +5 -3
- package/src/index.ts +10 -11
- package/src/messages/handlers.test.ts +10 -37
- package/src/messages/handlers.ts +2 -2
- package/src/plan/change-set.test.ts +220 -0
- package/src/plan/change-set.ts +225 -0
- package/src/plan/plan-types.ts +27 -0
- package/src/{entities → plan}/plan.test.ts +35 -29
- package/src/plan/plan.ts +353 -0
- package/src/{entities → plugin}/plugin.test.ts +14 -13
- package/src/{entities → plugin}/plugin.ts +28 -24
- package/src/resource/config-parser.ts +77 -0
- package/src/{entities/resource-options.test.ts → resource/parsed-resource-settings.test.ts} +8 -7
- package/src/resource/parsed-resource-settings.ts +179 -0
- package/src/{entities/resource-stateful-mode.test.ts → resource/resource-controller-stateful-mode.test.ts} +36 -39
- package/src/{entities/resource.test.ts → resource/resource-controller.test.ts} +116 -176
- package/src/resource/resource-controller.ts +340 -0
- package/src/resource/resource-settings.test.ts +494 -0
- package/src/resource/resource-settings.ts +192 -0
- package/src/resource/resource.ts +149 -0
- package/src/resource/stateful-parameter.test.ts +93 -0
- package/src/resource/stateful-parameter.ts +217 -0
- package/src/utils/test-utils.test.ts +87 -0
- package/src/utils/utils.test.ts +2 -2
- package/src/utils/utils.ts +51 -5
- package/tsconfig.json +0 -1
- package/vitest.config.ts +10 -0
- package/src/entities/change-set.test.ts +0 -155
- package/src/entities/change-set.ts +0 -244
- package/src/entities/plan-types.ts +0 -44
- package/src/entities/plan.ts +0 -178
- package/src/entities/resource-options.ts +0 -155
- package/src/entities/resource-parameters.test.ts +0 -604
- package/src/entities/resource-types.ts +0 -31
- package/src/entities/resource.ts +0 -470
- package/src/entities/stateful-parameter.test.ts +0 -114
- package/src/entities/stateful-parameter.ts +0 -92
- package/src/entities/transform-parameter.ts +0 -13
- /package/src/{entities/errors.ts → errors.ts} +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { StringIndexedObject, } from 'codify-schemas';
|
|
2
|
+
|
|
3
|
+
import { ParameterChange } from '../plan/change-set.js';
|
|
4
|
+
import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
|
|
5
|
+
import { ResourceSettings } from './resource-settings.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A resource represents an object on the system (application, CLI tool, or setting)
|
|
9
|
+
* that has state and can be created and destroyed. Examples of resources include CLI tools
|
|
10
|
+
* like homebrew, docker, and xcode-tools; applications like Google Chrome, Zoom, and OpenVPN;
|
|
11
|
+
* and settings like AWS profiles, git configs and system preference settings.
|
|
12
|
+
*/
|
|
13
|
+
export abstract class Resource<T extends StringIndexedObject> {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Return the settings for the resource. Consult the typing for {@link ResourceSettings} for
|
|
17
|
+
* a description of the options.
|
|
18
|
+
*
|
|
19
|
+
* **Parameters**:
|
|
20
|
+
* - id: The id of the resource. This translates to the `type` id parameter in codify.json configs
|
|
21
|
+
* - schema: A JSON schema used to validate user input
|
|
22
|
+
* - allowMultiple: Allow multiple copies of the resource to exist at the same time. If true then,
|
|
23
|
+
* a matcher must be defined that matches a user defined config and a single resource on the system.
|
|
24
|
+
* - removeStatefulParametersBeforeDestory: Call the delete methods of stateful parameters before destorying
|
|
25
|
+
* the base resource. Defaults to false.
|
|
26
|
+
* - dependencies: Specify the ids of any resources that this resource depends on
|
|
27
|
+
* - parameterSettings: Parameter specific settings. Use this to define custom equals functions, default values
|
|
28
|
+
* and input transformations
|
|
29
|
+
* - inputTransformation: Transform the input value.
|
|
30
|
+
*
|
|
31
|
+
* @return ResourceSettings The resource settings
|
|
32
|
+
*/
|
|
33
|
+
abstract getSettings(): ResourceSettings<T>;
|
|
34
|
+
|
|
35
|
+
async initialize(): Promise<void> {
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add custom validation logic in-addition to the default schema validation.
|
|
40
|
+
* In this method throw an error if the object did not validate. The message of the
|
|
41
|
+
* error will be shown to the user.
|
|
42
|
+
* @param parameters
|
|
43
|
+
*/
|
|
44
|
+
async validate(parameters: Partial<T>): Promise<void> {
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return the status of the resource on the system. If multiple resources exist, then return all instances of
|
|
49
|
+
* the resource back. Query for the individual parameters specified in the parameter param.
|
|
50
|
+
* Return null if the resource does not exist.
|
|
51
|
+
*
|
|
52
|
+
* Example (Android Studios Resource):
|
|
53
|
+
* 1. Receive Input:
|
|
54
|
+
* ```
|
|
55
|
+
* {
|
|
56
|
+
* name: 'Android Studios.app'
|
|
57
|
+
* directory: '/Application',
|
|
58
|
+
* version: '2023.2'
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
* 2. Query the system for any installed Android studio versions.
|
|
62
|
+
* 3. In this example we find that there is an 2023.2 version installed and an
|
|
63
|
+
* additional 2024.3-beta version installed as well.
|
|
64
|
+
* 4. We would return:
|
|
65
|
+
* ```
|
|
66
|
+
* [
|
|
67
|
+
* { name: 'Android Studios.app', directory: '/Application', version: '2023.2' },
|
|
68
|
+
* { name: 'Android Studios Preview.app', directory: '/Application', version: '2024.3' },
|
|
69
|
+
* ]
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @param parameters The parameters to refresh. In stateless mode this will be the parameters
|
|
73
|
+
* of the desired config. In stateful mode, this will be parameters of the state config + the desired
|
|
74
|
+
* config of any new parameters.
|
|
75
|
+
*
|
|
76
|
+
* @return A config or an array of configs representing the status of the resource on the
|
|
77
|
+
* system currently
|
|
78
|
+
*/
|
|
79
|
+
abstract refresh(parameters: Partial<T>): Promise<Array<Partial<T>> | Partial<T> | null>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create the resource (install) based on the parameters passed in. Only the desired parameters will
|
|
83
|
+
* be non-null because in a CREATE plan, the current value is null.
|
|
84
|
+
*
|
|
85
|
+
* Example (Android Studios Resource):
|
|
86
|
+
* 1. We receive a plan of:
|
|
87
|
+
* ```
|
|
88
|
+
* Plan {
|
|
89
|
+
* desiredConfig: {
|
|
90
|
+
* name: 'Android Studios.app',
|
|
91
|
+
* directory: '/Application',
|
|
92
|
+
* version: '2023.2'
|
|
93
|
+
* }
|
|
94
|
+
* currentConfig: null,
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
* 2. Install version Android Studios 2023.2 and then return.
|
|
98
|
+
*
|
|
99
|
+
* @param plan The plan of what to install. Use only the desiredConfig because currentConfig is null.
|
|
100
|
+
*/
|
|
101
|
+
abstract create(plan: CreatePlan<T>): Promise<void>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Modify a single parameter of a resource. Modify is optional to override and is only called
|
|
105
|
+
* when a resourceSetting was set to `canModify = true`. This method should only modify
|
|
106
|
+
* a single parameter at a time as specified by the first parameter: ParameterChange.
|
|
107
|
+
*
|
|
108
|
+
* Example (AWS Profile Resource):
|
|
109
|
+
* 1. We receive a parameter change of:
|
|
110
|
+
* ```
|
|
111
|
+
* {
|
|
112
|
+
* name: 'awsAccessKeyId',
|
|
113
|
+
* operation: ParameterOperation.MODIFY,
|
|
114
|
+
* newValue: '123456',
|
|
115
|
+
* previousValue: 'abcdef'
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
* 2. Use an if statement to only apply this operation for the parameter `awsAccessKeyId`
|
|
119
|
+
* 3. Update the value of the `aws_access_key_id` to the `newValue` specified in the parameter change
|
|
120
|
+
*
|
|
121
|
+
* @param pc ParameterChange, the parameter name and values to modify on the resource
|
|
122
|
+
* @param plan The overall plan that triggered the modify operation
|
|
123
|
+
*/
|
|
124
|
+
async modify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void> {
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Destroy the resource (uninstall) based on the parameters passed in. Only the current parameters will
|
|
129
|
+
* be non-null because in a DESTROY plan, the desired value is null. This method will only be called in
|
|
130
|
+
* stateful mode.
|
|
131
|
+
*
|
|
132
|
+
* Example (Android Studios Resource):
|
|
133
|
+
* 1. We receive a plan of:
|
|
134
|
+
* ```
|
|
135
|
+
* Plan {
|
|
136
|
+
* currentConfig: {
|
|
137
|
+
* name: 'Android Studios.app',
|
|
138
|
+
* directory: '/Application',
|
|
139
|
+
* version: '2022.4'
|
|
140
|
+
* },
|
|
141
|
+
* desiredConfig: null
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
* 2. Uninstall version Android Studios 2022.4 and then return.
|
|
145
|
+
*
|
|
146
|
+
* @param plan The plan of what to uninstall. Use only the currentConfig because desiredConfig is null.
|
|
147
|
+
*/
|
|
148
|
+
abstract destroy(plan: DestroyPlan<T>): Promise<void>;
|
|
149
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { spy } from 'sinon';
|
|
3
|
+
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
4
|
+
import { TestArrayStatefulParameter, TestConfig, testPlan } from '../utils/test-utils.test.js';
|
|
5
|
+
import { ArrayParameterSetting } from './resource-settings.js';
|
|
6
|
+
|
|
7
|
+
describe('Stateful parameter tests', () => {
|
|
8
|
+
it('addItem is called the correct number of times', async () => {
|
|
9
|
+
const plan = testPlan<TestConfig>({
|
|
10
|
+
desired: { propZ: ['a', 'b', 'c'] },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
|
|
14
|
+
expect(plan.changeSet.parameterChanges.length).to.eq(1);
|
|
15
|
+
|
|
16
|
+
const testParameter = spy(new TestArrayStatefulParameter());
|
|
17
|
+
await testParameter.add((plan.desiredConfig! as any).propZ, plan);
|
|
18
|
+
|
|
19
|
+
expect(testParameter.addItem.callCount).to.eq(3);
|
|
20
|
+
expect(testParameter.removeItem.called).to.be.false;
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('applyRemoveItem is called the correct number of times', async () => {
|
|
24
|
+
const plan = testPlan<TestConfig>({
|
|
25
|
+
desired: null,
|
|
26
|
+
current: [{ propZ: ['a', 'b', 'c'] }],
|
|
27
|
+
state: { propZ: ['a', 'b', 'c'] },
|
|
28
|
+
statefulMode: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY);
|
|
32
|
+
expect(plan.changeSet.parameterChanges.length).to.eq(1);
|
|
33
|
+
|
|
34
|
+
const testParameter = spy(new TestArrayStatefulParameter());
|
|
35
|
+
await testParameter.remove((plan.currentConfig as any).propZ, plan);
|
|
36
|
+
|
|
37
|
+
expect(testParameter.addItem.called).to.be.false;
|
|
38
|
+
expect(testParameter.removeItem.callCount).to.eq(3);
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('In stateless mode only applyAddItem is called only for modifies', async () => {
|
|
42
|
+
const parameter = new TestArrayStatefulParameter()
|
|
43
|
+
const plan = testPlan<TestConfig>({
|
|
44
|
+
desired: { propZ: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
|
|
45
|
+
current: [{ propZ: ['a', 'b', 'c'] }],
|
|
46
|
+
settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: parameter } } },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
|
|
50
|
+
expect(plan.changeSet.parameterChanges[0]).toMatchObject({
|
|
51
|
+
name: 'propZ',
|
|
52
|
+
previousValue: ['a', 'c'], // In stateless mode the previous value gets filtered to prevent deletes
|
|
53
|
+
newValue: ['a', 'c', 'd', 'e', 'f'],
|
|
54
|
+
operation: ParameterOperation.MODIFY,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const testParameter = spy(parameter);
|
|
58
|
+
await testParameter.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
|
|
59
|
+
|
|
60
|
+
expect(testParameter.addItem.calledThrice).to.be.true;
|
|
61
|
+
expect(testParameter.removeItem.called).to.be.false;
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('isElementEqual is called for modifies', async () => {
|
|
65
|
+
const testParameter = spy(new class extends TestArrayStatefulParameter {
|
|
66
|
+
getSettings(): ArrayParameterSetting {
|
|
67
|
+
return {
|
|
68
|
+
type: 'array',
|
|
69
|
+
isElementEqual: (desired, current) => current.includes(desired),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const plan = testPlan<TestConfig>({
|
|
75
|
+
desired: { propZ: ['9.12', '9.13'] }, // b to remove, d, e, f to add
|
|
76
|
+
current: [{ propZ: ['9.12.9'] }],
|
|
77
|
+
settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: testParameter } } }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
|
|
81
|
+
expect(plan.changeSet.parameterChanges[0]).toMatchObject({
|
|
82
|
+
name: 'propZ',
|
|
83
|
+
previousValue: ['9.12.9'],
|
|
84
|
+
newValue: ['9.12', '9.13'],
|
|
85
|
+
operation: ParameterOperation.MODIFY,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await testParameter.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
|
|
89
|
+
|
|
90
|
+
expect(testParameter.addItem.calledOnce).to.be.true;
|
|
91
|
+
expect(testParameter.removeItem.called).to.be.false;
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
|
|
3
|
+
import { Plan } from '../plan/plan.js';
|
|
4
|
+
import { ArrayParameterSetting, ParameterSetting } from './resource-settings.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
|
|
8
|
+
* is still tied to the overall lifecycle of a resource.
|
|
9
|
+
*
|
|
10
|
+
* **Examples include:**
|
|
11
|
+
* 1. Homebrew formulas are stateful parameters. They can be installed and uninstalled but they are still tied to the
|
|
12
|
+
* overall lifecycle of homebrew
|
|
13
|
+
* 2. Nvm installed node versions are stateful parameters. Nvm can install and uninstall different versions of Node but
|
|
14
|
+
* these versions are tied to the lifecycle of nvm. If nvm is uninstalled then so are the Node versions.
|
|
15
|
+
*/
|
|
16
|
+
export abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parameter settings for the stateful parameter. Stateful parameters share the same parameter settings as
|
|
20
|
+
* regular parameters except that they cannot be of type 'stateful'. See {@link ParameterSetting} for more
|
|
21
|
+
* information on available settings.
|
|
22
|
+
*
|
|
23
|
+
* @return The parameter settings
|
|
24
|
+
*/
|
|
25
|
+
getSettings(): ParameterSetting {
|
|
26
|
+
return {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Refresh the status of the stateful parameter on the system. This method works similarly to {@link Resource.refresh}.
|
|
31
|
+
* Return the value of the stateful parameter or null if not found.
|
|
32
|
+
*
|
|
33
|
+
* @param desired The desired value of the user.
|
|
34
|
+
*
|
|
35
|
+
* @return The value of the stateful parameter currently on the system or null if not found
|
|
36
|
+
*/
|
|
37
|
+
abstract refresh(desired: V | null): Promise<V | null>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create the stateful parameter on the system. This method is similar {@link Resource.create} except that its only
|
|
41
|
+
* applicable to the stateful parameter. For resource `CREATE` operations, this method will be called after the
|
|
42
|
+
* resource is successfully created. The add method is called when a ParameterChange is ADD in a plan. The add
|
|
43
|
+
* method is only called when the stateful parameter does not currently exist.
|
|
44
|
+
*
|
|
45
|
+
* **Example (Homebrew formula):**
|
|
46
|
+
* 1. Add is called with a value of:
|
|
47
|
+
* ```
|
|
48
|
+
* ['jq', 'jenv']
|
|
49
|
+
* ```
|
|
50
|
+
* 2. Add handles the request by calling `brew install --formulae jq jenv`
|
|
51
|
+
*
|
|
52
|
+
* @param valueToAdd The desired value of the stateful parameter.
|
|
53
|
+
* @param plan The overall plan that contains the ADD
|
|
54
|
+
*/
|
|
55
|
+
abstract add(valueToAdd: V, plan: Plan<T>): Promise<void>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Modify the state of a stateful parameter on the system. This method is similar to {@link Resource.modify} except that its only
|
|
59
|
+
* applicable to the stateful parameter.
|
|
60
|
+
*
|
|
61
|
+
* **Example (Git email parameter):**
|
|
62
|
+
* 1. Add is called with a value of:
|
|
63
|
+
* ```
|
|
64
|
+
* newValue: 'email+new@gmail.com', previousValue: 'email+old@gmail.com'
|
|
65
|
+
* ```
|
|
66
|
+
* 2. Modify handles the request by calling `git config --global user.email email+new@gmail.com`
|
|
67
|
+
*
|
|
68
|
+
* @param newValue The desired value of the stateful parameter
|
|
69
|
+
* @param previousValue The current value of the stateful parameter
|
|
70
|
+
* @param plan The overall plan
|
|
71
|
+
*/
|
|
72
|
+
abstract modify(newValue: V, previousValue: V, plan: Plan<T>): Promise<void>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create the stateful parameter on the system. This method is similar {@link Resource.destroy} except that its only
|
|
76
|
+
* applicable to the stateful parameter. The remove method is only called when the stateful parameter already currently exist.
|
|
77
|
+
* This method corresponds to REMOVE parameter operations in a plan.
|
|
78
|
+
* For resource `DESTORY`, this method is only called if the {@link ResourceSettings.removeStatefulParametersBeforeDestroy}
|
|
79
|
+
* is set to true. This method will be called before the resource is destroyed.
|
|
80
|
+
*
|
|
81
|
+
* **Example (Homebrew formula):**
|
|
82
|
+
* 1. Remove is called with a value of:
|
|
83
|
+
* ```
|
|
84
|
+
* ['jq', 'jenv']
|
|
85
|
+
* ```
|
|
86
|
+
* 2. Remove handles the request by calling `brew uninstall --formulae jq jenv`
|
|
87
|
+
*
|
|
88
|
+
* @param valueToRemove The value to remove from the stateful parameter.
|
|
89
|
+
* @param plan The overall plan that contains the REMOVE
|
|
90
|
+
*/
|
|
91
|
+
abstract remove(valueToRemove: V, plan: Plan<T>): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A specialized version of {@link StatefulParameter } that is used for stateful parameters which are arrays.
|
|
96
|
+
* A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
|
|
97
|
+
* is still tied to the overall lifecycle of a resource.
|
|
98
|
+
*
|
|
99
|
+
* **Examples:**
|
|
100
|
+
* - Homebrew formulas are arrays
|
|
101
|
+
* - Pyenv python versions are arrays
|
|
102
|
+
*/
|
|
103
|
+
export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parameter level settings. Type must be 'array'.
|
|
107
|
+
*/
|
|
108
|
+
getSettings(): ArrayParameterSetting {
|
|
109
|
+
return { type: 'array' }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* It is not recommended to override the `add` method. A addItem helper method is available to operate on
|
|
114
|
+
* individual elements of the desired array. See {@link StatefulParameter.add} for more info.
|
|
115
|
+
*
|
|
116
|
+
* @param valuesToAdd The array of values to add
|
|
117
|
+
* @param plan The overall plan
|
|
118
|
+
*
|
|
119
|
+
*/
|
|
120
|
+
async add(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
|
|
121
|
+
for (const value of valuesToAdd) {
|
|
122
|
+
await this.addItem(value, plan);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* It is not recommended to override the `modify` method. `addItem` and `removeItem` will be called accordingly based
|
|
128
|
+
* on the modifications. See {@link StatefulParameter.modify} for more info.
|
|
129
|
+
*
|
|
130
|
+
* @param newValues The new array value
|
|
131
|
+
* @param previousValues The previous array value
|
|
132
|
+
* @param plan The overall plan
|
|
133
|
+
*/
|
|
134
|
+
async modify(newValues: V[], previousValues: V[], plan: Plan<T>): Promise<void> {
|
|
135
|
+
|
|
136
|
+
// TODO: I don't think this works with duplicate elements. Solve at another time
|
|
137
|
+
const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
|
|
138
|
+
if (this.getSettings()?.isElementEqual) {
|
|
139
|
+
return this.getSettings().isElementEqual!(n, p);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return n === p;
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
|
|
146
|
+
if (this.getSettings().isElementEqual) {
|
|
147
|
+
return this.getSettings().isElementEqual!(n, p);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return n === p;
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
for (const value of valuesToAdd) {
|
|
154
|
+
await this.addItem(value, plan)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const value of valuesToRemove) {
|
|
158
|
+
await this.removeItem(value, plan)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* It is not recommended to override the `remove` method. A removeItem helper method is available to operate on
|
|
164
|
+
* individual elements of the desired array. See {@link StatefulParameter.remove} for more info.
|
|
165
|
+
*
|
|
166
|
+
* @param valuesToAdd The array of values to add
|
|
167
|
+
* @param plan The overall plan
|
|
168
|
+
*
|
|
169
|
+
*/
|
|
170
|
+
async remove(valuesToRemove: V[], plan: Plan<T>): Promise<void> {
|
|
171
|
+
for (const value of valuesToRemove) {
|
|
172
|
+
await this.removeItem(value as V, plan);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* See {@link StatefulParameter.refresh} for more info.
|
|
178
|
+
*
|
|
179
|
+
* @param desired The desired value to refresh
|
|
180
|
+
* @return The current value on the system or null if not found.
|
|
181
|
+
*/
|
|
182
|
+
abstract refresh(desired: V[] | null): Promise<V[] | null>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Helper method that gets called when individual elements of the array need to be added. See {@link StatefulParameter.add}
|
|
186
|
+
* for more information.
|
|
187
|
+
*
|
|
188
|
+
* Example (Homebrew formula):
|
|
189
|
+
* 1. The stateful parameter receives an input of:
|
|
190
|
+
* ```
|
|
191
|
+
* ['jq', 'jenv', 'docker']
|
|
192
|
+
* ```
|
|
193
|
+
* 2. Internally the stateful parameter will iterate the array and call `addItem` for each element
|
|
194
|
+
* 3. Override addItem and install each formula using `brew install --formula jq`
|
|
195
|
+
*
|
|
196
|
+
* @param item The item to add (install)
|
|
197
|
+
* @param plan The overall plan
|
|
198
|
+
*/
|
|
199
|
+
abstract addItem(item: V, plan: Plan<T>): Promise<void>;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Helper method that gets called when individual elements of the array need to be removed. See {@link StatefulParameter.remove}
|
|
203
|
+
* for more information.
|
|
204
|
+
*
|
|
205
|
+
* Example (Homebrew formula):
|
|
206
|
+
* 1. The stateful parameter receives an input of:
|
|
207
|
+
* ```
|
|
208
|
+
* ['jq', 'jenv', 'docker']
|
|
209
|
+
* ```
|
|
210
|
+
* 2. Internally the stateful parameter will iterate the array and call `removeItem` for each element
|
|
211
|
+
* 3. Override removeItem and uninstall each formula using `brew uninstall --formula jq`
|
|
212
|
+
*
|
|
213
|
+
* @param item The item to remove (uninstall)
|
|
214
|
+
* @param plan The overall plan
|
|
215
|
+
*/
|
|
216
|
+
abstract removeItem(item: V, plan: Plan<T>): Promise<void>;
|
|
217
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
3
|
+
import { Plan } from '../plan/plan.js';
|
|
4
|
+
import { Resource } from '../resource/resource.js';
|
|
5
|
+
import { CreatePlan, DestroyPlan } from '../plan/plan-types.js';
|
|
6
|
+
import { ArrayStatefulParameter, StatefulParameter } from '../resource/stateful-parameter.js';
|
|
7
|
+
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
8
|
+
|
|
9
|
+
export function testPlan<T extends StringIndexedObject>(params: {
|
|
10
|
+
desired?: Partial<T> | null;
|
|
11
|
+
current?: Partial<T>[] | null;
|
|
12
|
+
state?: Partial<T> | null;
|
|
13
|
+
core?: ResourceConfig;
|
|
14
|
+
settings?: ResourceSettings<T>;
|
|
15
|
+
statefulMode?: boolean;
|
|
16
|
+
}) {
|
|
17
|
+
return Plan.calculate({
|
|
18
|
+
desiredParameters: params.desired ?? null,
|
|
19
|
+
currentParametersArray: params.current ?? null,
|
|
20
|
+
stateParameters: params.state ?? null,
|
|
21
|
+
coreParameters: params.core ?? { type: 'type' },
|
|
22
|
+
settings: params.settings ?
|
|
23
|
+
new ParsedResourceSettings<T>(params.settings)
|
|
24
|
+
: new ParsedResourceSettings<T>({ id: 'type' }),
|
|
25
|
+
statefulMode: params.statefulMode ?? false,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TestConfig extends StringIndexedObject {
|
|
30
|
+
propA: string;
|
|
31
|
+
propB: number;
|
|
32
|
+
propC?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class TestResource extends Resource<TestConfig> {
|
|
36
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
37
|
+
return { id: 'type' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
create(plan: CreatePlan<TestConfig>): Promise<void> {
|
|
41
|
+
return Promise.resolve(undefined);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
destroy(plan: DestroyPlan<TestConfig>): Promise<void> {
|
|
45
|
+
return Promise.resolve(undefined);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
|
|
49
|
+
return {
|
|
50
|
+
propA: 'a',
|
|
51
|
+
propB: 10,
|
|
52
|
+
propC: 'c',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class TestStatefulParameter extends StatefulParameter<TestConfig, string> {
|
|
58
|
+
async refresh(desired: string | null): Promise<string | null> {
|
|
59
|
+
return 'd';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async add(valueToAdd: string, plan: Plan<TestConfig>): Promise<void> {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async modify(newValue: string, previousValue: string, plan: Plan<TestConfig>): Promise<void> {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async remove(valueToRemove: string, plan: Plan<TestConfig>): Promise<void> {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class TestArrayStatefulParameter extends ArrayStatefulParameter<TestConfig, string> {
|
|
76
|
+
async refresh(): Promise<any | null> {
|
|
77
|
+
return ['3.11.9']
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
addItem(item: string, plan: Plan<TestConfig>): Promise<void> {
|
|
81
|
+
return Promise.resolve(undefined);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
removeItem(item: string, plan: Plan<TestConfig>): Promise<void> {
|
|
85
|
+
return Promise.resolve(undefined);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/utils/utils.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { splitUserConfig } from './utils.js';
|
|
|
3
3
|
|
|
4
4
|
describe('Utils tests', () => {
|
|
5
5
|
it('Can split a config correctly', () => {
|
|
6
|
-
const { parameters,
|
|
6
|
+
const { parameters, coreParameters } = splitUserConfig({
|
|
7
7
|
type: 'type',
|
|
8
8
|
name: 'name',
|
|
9
9
|
dependsOn: ['a', 'b', 'c'],
|
|
@@ -13,7 +13,7 @@ describe('Utils tests', () => {
|
|
|
13
13
|
propD: 'propD',
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
expect(
|
|
16
|
+
expect(coreParameters).toMatchObject({
|
|
17
17
|
type: 'type',
|
|
18
18
|
name: 'name',
|
|
19
19
|
dependsOn: ['a', 'b', 'c'],
|
package/src/utils/utils.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import promiseSpawn from '@npmcli/promise-spawn';
|
|
2
|
-
import { SpawnOptions } from 'child_process';
|
|
3
2
|
import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
|
|
3
|
+
import { SpawnOptions } from 'node:child_process';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
|
|
6
|
+
import { ArrayParameterSetting } from '../resource/resource-settings.js';
|
|
4
7
|
|
|
5
8
|
export enum SpawnStatus {
|
|
6
9
|
SUCCESS = 'success',
|
|
@@ -81,21 +84,64 @@ export function isDebug(): boolean {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
export function splitUserConfig<T extends StringIndexedObject>(
|
|
84
|
-
config:
|
|
85
|
-
): { parameters: T;
|
|
86
|
-
const
|
|
87
|
+
config: ResourceConfig & T
|
|
88
|
+
): { parameters: T; coreParameters: ResourceConfig } {
|
|
89
|
+
const coreParameters = {
|
|
87
90
|
type: config.type,
|
|
88
91
|
...(config.name ? { name: config.name } : {}),
|
|
89
92
|
...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
|
|
90
93
|
};
|
|
91
94
|
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
92
96
|
const { type, name, dependsOn, ...parameters } = config;
|
|
97
|
+
|
|
93
98
|
return {
|
|
94
99
|
parameters: parameters as T,
|
|
95
|
-
|
|
100
|
+
coreParameters,
|
|
96
101
|
};
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
export function setsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean {
|
|
100
105
|
return set1.size === set2.size && [...set1].every((v) => set2.has(v));
|
|
101
106
|
}
|
|
107
|
+
|
|
108
|
+
const homeDirectory = os.homedir();
|
|
109
|
+
|
|
110
|
+
export function untildify(pathWithTilde: string) {
|
|
111
|
+
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown) {
|
|
115
|
+
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
116
|
+
throw new Error(`A non-array value:
|
|
117
|
+
|
|
118
|
+
Desired: ${JSON.stringify(desired, null, 2)}
|
|
119
|
+
|
|
120
|
+
Current: ${JSON.stringify(desired, null, 2)}
|
|
121
|
+
|
|
122
|
+
Was provided even though type array was specified.
|
|
123
|
+
`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (desired.length !== current.length) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const desiredCopy = [...desired];
|
|
131
|
+
const currentCopy = [...current];
|
|
132
|
+
|
|
133
|
+
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
134
|
+
// an isElementEqual method. Time: O(n^2)
|
|
135
|
+
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
136
|
+
const idx = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2))
|
|
137
|
+
|
|
138
|
+
if (idx === -1) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
desiredCopy.splice(counter, 1)
|
|
143
|
+
currentCopy.splice(idx, 1)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return currentCopy.length === 0;
|
|
147
|
+
}
|
package/tsconfig.json
CHANGED