codify-plugin-lib 1.0.75 → 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/entities/plugin.d.ts +1 -1
- package/dist/entities/plugin.js +5 -5
- package/dist/entities/resource-options.d.ts +6 -6
- package/dist/entities/resource-options.js +7 -9
- package/dist/entities/resource.d.ts +2 -3
- package/dist/entities/resource.js +2 -2
- 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 +25 -24
- 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 +6 -4
- package/src/index.ts +10 -11
- package/src/messages/handlers.test.ts +21 -42
- package/src/messages/handlers.ts +28 -27
- 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 +32 -27
- 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 -156
- package/src/entities/resource-parameters.test.ts +0 -604
- package/src/entities/resource-types.ts +0 -31
- package/src/entities/resource.ts +0 -471
- 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,247 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
3
|
+
import { Plan } from '../plan/plan.js';
|
|
4
|
+
import { ConfigParser } from './config-parser.js';
|
|
5
|
+
import { ParsedResourceSettings } from './parsed-resource-settings.js';
|
|
6
|
+
export class ResourceController {
|
|
7
|
+
resource;
|
|
8
|
+
settings;
|
|
9
|
+
parsedSettings;
|
|
10
|
+
typeId;
|
|
11
|
+
dependencies;
|
|
12
|
+
ajv;
|
|
13
|
+
schemaValidator;
|
|
14
|
+
constructor(resource) {
|
|
15
|
+
this.resource = resource;
|
|
16
|
+
this.settings = resource.getSettings();
|
|
17
|
+
this.typeId = this.settings.id;
|
|
18
|
+
this.dependencies = this.settings.dependencies ?? [];
|
|
19
|
+
if (this.settings.schema) {
|
|
20
|
+
this.ajv = new Ajv({
|
|
21
|
+
allErrors: true,
|
|
22
|
+
strict: true,
|
|
23
|
+
strictRequired: false,
|
|
24
|
+
});
|
|
25
|
+
this.schemaValidator = this.ajv.compile(this.settings.schema);
|
|
26
|
+
}
|
|
27
|
+
this.parsedSettings = new ParsedResourceSettings(this.settings);
|
|
28
|
+
}
|
|
29
|
+
async initialize() {
|
|
30
|
+
return this.resource.initialize();
|
|
31
|
+
}
|
|
32
|
+
async validate(parameters, resourceMetaData) {
|
|
33
|
+
if (this.schemaValidator) {
|
|
34
|
+
const isValid = this.schemaValidator(parameters);
|
|
35
|
+
if (!isValid) {
|
|
36
|
+
return {
|
|
37
|
+
isValid: false,
|
|
38
|
+
resourceName: resourceMetaData.name,
|
|
39
|
+
resourceType: resourceMetaData.type,
|
|
40
|
+
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let isValid = true;
|
|
45
|
+
let customValidationErrorMessage;
|
|
46
|
+
try {
|
|
47
|
+
await this.resource.validate(parameters);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
isValid = false;
|
|
51
|
+
customValidationErrorMessage = error.message;
|
|
52
|
+
}
|
|
53
|
+
if (!isValid) {
|
|
54
|
+
return {
|
|
55
|
+
customValidationErrorMessage,
|
|
56
|
+
isValid: false,
|
|
57
|
+
resourceName: resourceMetaData.name,
|
|
58
|
+
resourceType: resourceMetaData.type,
|
|
59
|
+
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
isValid: true,
|
|
64
|
+
resourceName: resourceMetaData.name,
|
|
65
|
+
resourceType: resourceMetaData.type,
|
|
66
|
+
schemaValidationErrors: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async plan(desiredConfig, stateConfig = null, statefulMode = false) {
|
|
70
|
+
this.validatePlanInputs(desiredConfig, stateConfig, statefulMode);
|
|
71
|
+
this.addDefaultValues(desiredConfig);
|
|
72
|
+
await this.applyTransformParameters(desiredConfig);
|
|
73
|
+
// Parse data from the user supplied config
|
|
74
|
+
const parsedConfig = new ConfigParser(desiredConfig, stateConfig, this.parsedSettings.statefulParameters);
|
|
75
|
+
const { coreParameters, desiredParameters, stateParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig;
|
|
76
|
+
// Refresh resource parameters. This refreshes the parameters that configure the resource itself
|
|
77
|
+
const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
|
|
78
|
+
// Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
|
|
79
|
+
if (currentParametersArray === null
|
|
80
|
+
|| currentParametersArray === undefined
|
|
81
|
+
|| this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
|
|
82
|
+
|| currentParametersArray.length === 0
|
|
83
|
+
|| currentParametersArray.filter(Boolean).length === 0) {
|
|
84
|
+
return Plan.calculate({
|
|
85
|
+
desiredParameters,
|
|
86
|
+
currentParametersArray,
|
|
87
|
+
stateParameters,
|
|
88
|
+
coreParameters,
|
|
89
|
+
settings: this.parsedSettings,
|
|
90
|
+
statefulMode,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Refresh stateful parameters. These parameters have state external to the resource. allowMultiple
|
|
94
|
+
// does not work together with stateful parameters
|
|
95
|
+
const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters);
|
|
96
|
+
return Plan.calculate({
|
|
97
|
+
desiredParameters,
|
|
98
|
+
currentParametersArray: [{ ...currentParametersArray[0], ...statefulCurrentParameters }],
|
|
99
|
+
stateParameters,
|
|
100
|
+
coreParameters,
|
|
101
|
+
settings: this.parsedSettings,
|
|
102
|
+
statefulMode
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async apply(plan) {
|
|
106
|
+
if (plan.getResourceType() !== this.typeId) {
|
|
107
|
+
throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
|
|
108
|
+
}
|
|
109
|
+
switch (plan.changeSet.operation) {
|
|
110
|
+
case ResourceOperation.CREATE: {
|
|
111
|
+
return this.applyCreate(plan);
|
|
112
|
+
}
|
|
113
|
+
case ResourceOperation.MODIFY: {
|
|
114
|
+
return this.applyModify(plan);
|
|
115
|
+
}
|
|
116
|
+
case ResourceOperation.RECREATE: {
|
|
117
|
+
await this.applyDestroy(plan);
|
|
118
|
+
return this.applyCreate(plan);
|
|
119
|
+
}
|
|
120
|
+
case ResourceOperation.DESTROY: {
|
|
121
|
+
return this.applyDestroy(plan);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async applyCreate(plan) {
|
|
126
|
+
await this.resource.create(plan);
|
|
127
|
+
const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges);
|
|
128
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
129
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name);
|
|
130
|
+
await statefulParameter.add(parameterChange.newValue, plan);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async applyModify(plan) {
|
|
134
|
+
const parameterChanges = plan
|
|
135
|
+
.changeSet
|
|
136
|
+
.parameterChanges
|
|
137
|
+
.filter((c) => c.operation !== ParameterOperation.NOOP);
|
|
138
|
+
const statelessParameterChanges = parameterChanges
|
|
139
|
+
.filter((pc) => !this.parsedSettings.statefulParameters.has(pc.name));
|
|
140
|
+
for (const pc of statelessParameterChanges) {
|
|
141
|
+
await this.resource.modify(pc, plan);
|
|
142
|
+
}
|
|
143
|
+
const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges);
|
|
144
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
145
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name);
|
|
146
|
+
switch (parameterChange.operation) {
|
|
147
|
+
case ParameterOperation.ADD: {
|
|
148
|
+
await statefulParameter.add(parameterChange.newValue, plan);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case ParameterOperation.MODIFY: {
|
|
152
|
+
await statefulParameter.modify(parameterChange.newValue, parameterChange.previousValue, plan);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case ParameterOperation.REMOVE: {
|
|
156
|
+
await statefulParameter.remove(parameterChange.previousValue, plan);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async applyDestroy(plan) {
|
|
163
|
+
// If this option is set (defaults to false), then stateful parameters need to be destroyed
|
|
164
|
+
// as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
|
|
165
|
+
if (this.settings.removeStatefulParametersBeforeDestroy) {
|
|
166
|
+
const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges);
|
|
167
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
168
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name);
|
|
169
|
+
await statefulParameter.remove(parameterChange.previousValue, plan);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await this.resource.destroy(plan);
|
|
173
|
+
}
|
|
174
|
+
validateRefreshResults(refresh) {
|
|
175
|
+
if (!refresh) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!this.settings.allowMultiple && refresh.length > 1) {
|
|
179
|
+
throw new Error(`Resource: ${this.settings.id}. Allow multiple was set to false but multiple refresh results were returned.
|
|
180
|
+
|
|
181
|
+
${JSON.stringify(refresh, null, 2)}
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async applyTransformParameters(desired) {
|
|
186
|
+
if (!desired) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
for (const [key, inputTransformation] of Object.entries(this.parsedSettings.inputTransformations)) {
|
|
190
|
+
if (desired[key] === undefined || !inputTransformation) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
desired[key] = await inputTransformation(desired[key]);
|
|
194
|
+
}
|
|
195
|
+
if (this.settings.inputTransformation) {
|
|
196
|
+
const transformed = await this.settings.inputTransformation(desired);
|
|
197
|
+
Object.keys(desired).forEach((k) => delete desired[k]);
|
|
198
|
+
Object.assign(desired, transformed);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
addDefaultValues(desired) {
|
|
202
|
+
if (!desired) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const [key, defaultValue] of Object.entries(this.parsedSettings.defaultValues)) {
|
|
206
|
+
if (defaultValue !== undefined && (desired[key] === undefined || desired[key] === null)) {
|
|
207
|
+
desired[key] = defaultValue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async refreshNonStatefulParameters(resourceParameters) {
|
|
212
|
+
const result = await this.resource.refresh(resourceParameters);
|
|
213
|
+
const currentParametersArray = Array.isArray(result) || result === null
|
|
214
|
+
? result
|
|
215
|
+
: [result];
|
|
216
|
+
this.validateRefreshResults(currentParametersArray);
|
|
217
|
+
return currentParametersArray;
|
|
218
|
+
}
|
|
219
|
+
// Refresh stateful parameters
|
|
220
|
+
// This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
|
|
221
|
+
async refreshStatefulParameters(statefulParametersConfig) {
|
|
222
|
+
const result = {};
|
|
223
|
+
const sortedEntries = Object.entries(statefulParametersConfig)
|
|
224
|
+
.sort(([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1) - this.parsedSettings.statefulParameterOrder.get(key2));
|
|
225
|
+
for (const [key, desiredValue] of sortedEntries) {
|
|
226
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(key);
|
|
227
|
+
if (!statefulParameter) {
|
|
228
|
+
throw new Error(`Stateful parameter ${key} was not found`);
|
|
229
|
+
}
|
|
230
|
+
result[key] = await statefulParameter.refresh(desiredValue ?? null);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
validatePlanInputs(desired, current, statefulMode) {
|
|
235
|
+
if (!desired && !current) {
|
|
236
|
+
throw new Error('Desired config and current config cannot both be missing');
|
|
237
|
+
}
|
|
238
|
+
if (!statefulMode && !desired) {
|
|
239
|
+
throw new Error('Desired config must be provided in non-stateful mode');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
getSortedStatefulParameterChanges(parameterChanges) {
|
|
243
|
+
return parameterChanges
|
|
244
|
+
.filter((pc) => this.parsedSettings.statefulParameters.has(pc.name))
|
|
245
|
+
.sort((a, b) => this.parsedSettings.statefulParameterOrder.get(a.name) - this.parsedSettings.statefulParameterOrder.get(b.name));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { StatefulParameter } from './stateful-parameter.js';
|
|
3
|
+
/**
|
|
4
|
+
* The configuration and settings for a resource.
|
|
5
|
+
*/
|
|
6
|
+
export interface ResourceSettings<T extends StringIndexedObject> {
|
|
7
|
+
/**
|
|
8
|
+
* The typeId of the resource.
|
|
9
|
+
*/
|
|
10
|
+
id: string;
|
|
11
|
+
/**
|
|
12
|
+
* Schema to validate user configs with. Must be in the format JSON Schema draft07
|
|
13
|
+
*/
|
|
14
|
+
schema?: unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Allow multiple of the same resource to unique. Set truthy if
|
|
17
|
+
* multiples are allowed, for example for applications, there can be multiple copy of the same application installed
|
|
18
|
+
* on the system. Or there can be multiple git repos. Defaults to false.
|
|
19
|
+
*/
|
|
20
|
+
allowMultiple?: {
|
|
21
|
+
/**
|
|
22
|
+
* If multiple copies are allowed then a matcher must be defined to match the desired
|
|
23
|
+
* config with one of the resources currently existing on the system. Return null if there is no match.
|
|
24
|
+
*
|
|
25
|
+
* @param current An array of resources found installed on the system
|
|
26
|
+
* @param desired The desired config to match.
|
|
27
|
+
*
|
|
28
|
+
* @return The matched resource.
|
|
29
|
+
*/
|
|
30
|
+
matcher: (desired: Partial<T>, current: Partial<T>[]) => Partial<T>;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* If true, {@link StatefulParameter} remove() will be called before resource destruction. This is useful
|
|
34
|
+
* if the stateful parameter needs to be first uninstalled (cleanup) before the overall resource can be
|
|
35
|
+
* uninstalled. Defaults to false.
|
|
36
|
+
*/
|
|
37
|
+
removeStatefulParametersBeforeDestroy?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* An array of type ids of resources that this resource depends on. This affects the order in which multiple resources are
|
|
40
|
+
* planned and applied.
|
|
41
|
+
*/
|
|
42
|
+
dependencies?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Options for configuring parameters operations including overriding the equals function, adding default values
|
|
45
|
+
* and applying any input transformations. Use parameter settings to define stateful parameters as well.
|
|
46
|
+
*/
|
|
47
|
+
parameterSettings?: Partial<Record<keyof T, ParameterSetting>>;
|
|
48
|
+
/**
|
|
49
|
+
* A config level transformation that is only applied to the user supplied desired config. This transformation is allowed
|
|
50
|
+
* to add, remove or modify keys as well as values. Changing this transformation for existing libraries will mess up existing states.
|
|
51
|
+
*
|
|
52
|
+
* @param desired
|
|
53
|
+
*/
|
|
54
|
+
inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The type of parameter. This value is mainly used to determine a pre-set equality method for comparing the current
|
|
58
|
+
* config with desired config. Certain types will have additional options to help support it. For example the type
|
|
59
|
+
* stateful requires a stateful parameter definition and type array takes an isElementEqual method.
|
|
60
|
+
*/
|
|
61
|
+
export type ParameterSettingType = 'any' | 'array' | 'boolean' | 'directory' | 'number' | 'stateful' | 'string' | 'version';
|
|
62
|
+
/**
|
|
63
|
+
* Typing information for the parameter setting. This represents a setting on a specific parameter within a
|
|
64
|
+
* resource. Options for configuring parameters operations including overriding the equals function, adding default values
|
|
65
|
+
* and applying any input transformations. See {@link DefaultParameterSetting } for more information.
|
|
66
|
+
* Use parameter settings to define stateful parameters as well.
|
|
67
|
+
*/
|
|
68
|
+
export type ParameterSetting = ArrayParameterSetting | DefaultParameterSetting | StatefulParameterSetting;
|
|
69
|
+
/**
|
|
70
|
+
* The parent class for parameter settings. The options are applicable to array parameter settings
|
|
71
|
+
* as well.
|
|
72
|
+
*/
|
|
73
|
+
export interface DefaultParameterSetting {
|
|
74
|
+
/**
|
|
75
|
+
* The type of the value of this parameter. See {@link ParameterSettingType} for the available options. This value
|
|
76
|
+
* is mainly used to determine the equality method when performing diffing.
|
|
77
|
+
*/
|
|
78
|
+
type?: ParameterSettingType;
|
|
79
|
+
/**
|
|
80
|
+
* Default value for the parameter. If a value is not provided in the config, then this value will be used.
|
|
81
|
+
*/
|
|
82
|
+
default?: unknown;
|
|
83
|
+
/**
|
|
84
|
+
* A transformation of the input value for this parameter. This transformation is only applied to the desired parameter
|
|
85
|
+
* value supplied by the user.
|
|
86
|
+
*
|
|
87
|
+
* @param input The original parameter value from the desired config.
|
|
88
|
+
*/
|
|
89
|
+
inputTransformation?: (input: any) => Promise<any> | unknown;
|
|
90
|
+
/**
|
|
91
|
+
* Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan.
|
|
92
|
+
* This value will override the pre-set equality function from the type. Return true if the desired value is
|
|
93
|
+
* equivalent to the current value.
|
|
94
|
+
*
|
|
95
|
+
* @param desired The desired value.
|
|
96
|
+
* @param current The current value.
|
|
97
|
+
*
|
|
98
|
+
* @return Return true if equal
|
|
99
|
+
*/
|
|
100
|
+
isEqual?: (desired: any, current: any) => boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Chose if the resource can be modified instead of re-created when there is a change to this parameter.
|
|
103
|
+
* Defaults to false (re-create).
|
|
104
|
+
*
|
|
105
|
+
* Examples:
|
|
106
|
+
* 1. Settings like git user name and git user email that have setter calls and don't require the re-installation of git
|
|
107
|
+
* 2. AWS profile secret keys that can be updated without the re-installation of AWS CLI
|
|
108
|
+
*/
|
|
109
|
+
canModify?: boolean;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Array type specific settings. See {@link DefaultParameterSetting } for a full list of options.
|
|
113
|
+
*/
|
|
114
|
+
export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
115
|
+
type: 'array';
|
|
116
|
+
/**
|
|
117
|
+
* An element level equality function for arrays. The diffing algorithm will take isElementEqual and use it in a
|
|
118
|
+
* O(n^2) equality comparison to determine if the overall array is equal. This value will override the pre-set equality
|
|
119
|
+
* function for arrays (desired === current). Return true if the desired element is equivalent to the current element.
|
|
120
|
+
*
|
|
121
|
+
* @param desired An element of the desired array
|
|
122
|
+
* @param current An element of the current array
|
|
123
|
+
*
|
|
124
|
+
* @return Return true if desired is equivalent to current.
|
|
125
|
+
*/
|
|
126
|
+
isElementEqual?: (desired: any, current: any) => boolean;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
|
|
130
|
+
* state but is still tied to the overall state of the resource. For example 'homebrew' is represented
|
|
131
|
+
* as a resource and taps, formulas and casks are represented as a stateful parameter. A formula can be installed,
|
|
132
|
+
* modified and removed (has state) but it is still tied to the overall lifecycle of homebrew.
|
|
133
|
+
*
|
|
134
|
+
*/
|
|
135
|
+
export interface StatefulParameterSetting extends DefaultParameterSetting {
|
|
136
|
+
type: 'stateful';
|
|
137
|
+
/**
|
|
138
|
+
* The stateful parameter definition. A stateful parameter is a sub-resource that can hold its own
|
|
139
|
+
* state but is still tied to the overall state of the resource. For example 'homebrew' is represented
|
|
140
|
+
* as a resource and taps, formulas and casks are represented as a stateful parameter. A formula can be installed,
|
|
141
|
+
* modified and removed (has state) but it is still tied to the overall lifecycle of homebrew.
|
|
142
|
+
*/
|
|
143
|
+
definition: StatefulParameter<any, unknown>;
|
|
144
|
+
/**
|
|
145
|
+
* The order multiple stateful parameters should be applied in. The order is applied in ascending order (1, 2, 3...).
|
|
146
|
+
*/
|
|
147
|
+
order?: number;
|
|
148
|
+
}
|
|
149
|
+
export declare const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { untildify } from '../utils/utils.js';
|
|
3
|
+
export const ParameterEqualsDefaults = {
|
|
4
|
+
'boolean': (a, b) => Boolean(a) === Boolean(b),
|
|
5
|
+
'directory': (a, b) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
|
|
6
|
+
'number': (a, b) => Number(a) === Number(b),
|
|
7
|
+
'string': (a, b) => String(a) === String(b),
|
|
8
|
+
'version': (desired, current) => String(current).includes(String(desired))
|
|
9
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { ParameterChange } from '../plan/change-set.js';
|
|
3
|
+
import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
|
|
4
|
+
import { ResourceSettings } from './resource-settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* A resource represents an object on the system (application, CLI tool, or setting)
|
|
7
|
+
* that has state and can be created and destroyed. Examples of resources include CLI tools
|
|
8
|
+
* like homebrew, docker, and xcode-tools; applications like Google Chrome, Zoom, and OpenVPN;
|
|
9
|
+
* and settings like AWS profiles, git configs and system preference settings.
|
|
10
|
+
*/
|
|
11
|
+
export declare abstract class Resource<T extends StringIndexedObject> {
|
|
12
|
+
/**
|
|
13
|
+
* Return the settings for the resource. Consult the typing for {@link ResourceSettings} for
|
|
14
|
+
* a description of the options.
|
|
15
|
+
*
|
|
16
|
+
* **Parameters**:
|
|
17
|
+
* - id: The id of the resource. This translates to the `type` id parameter in codify.json configs
|
|
18
|
+
* - schema: A JSON schema used to validate user input
|
|
19
|
+
* - allowMultiple: Allow multiple copies of the resource to exist at the same time. If true then,
|
|
20
|
+
* a matcher must be defined that matches a user defined config and a single resource on the system.
|
|
21
|
+
* - removeStatefulParametersBeforeDestory: Call the delete methods of stateful parameters before destorying
|
|
22
|
+
* the base resource. Defaults to false.
|
|
23
|
+
* - dependencies: Specify the ids of any resources that this resource depends on
|
|
24
|
+
* - parameterSettings: Parameter specific settings. Use this to define custom equals functions, default values
|
|
25
|
+
* and input transformations
|
|
26
|
+
* - inputTransformation: Transform the input value.
|
|
27
|
+
*
|
|
28
|
+
* @return ResourceSettings The resource settings
|
|
29
|
+
*/
|
|
30
|
+
abstract getSettings(): ResourceSettings<T>;
|
|
31
|
+
initialize(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Add custom validation logic in-addition to the default schema validation.
|
|
34
|
+
* In this method throw an error if the object did not validate. The message of the
|
|
35
|
+
* error will be shown to the user.
|
|
36
|
+
* @param parameters
|
|
37
|
+
*/
|
|
38
|
+
validate(parameters: Partial<T>): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Return the status of the resource on the system. If multiple resources exist, then return all instances of
|
|
41
|
+
* the resource back. Query for the individual parameters specified in the parameter param.
|
|
42
|
+
* Return null if the resource does not exist.
|
|
43
|
+
*
|
|
44
|
+
* Example (Android Studios Resource):
|
|
45
|
+
* 1. Receive Input:
|
|
46
|
+
* ```
|
|
47
|
+
* {
|
|
48
|
+
* name: 'Android Studios.app'
|
|
49
|
+
* directory: '/Application',
|
|
50
|
+
* version: '2023.2'
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
* 2. Query the system for any installed Android studio versions.
|
|
54
|
+
* 3. In this example we find that there is an 2023.2 version installed and an
|
|
55
|
+
* additional 2024.3-beta version installed as well.
|
|
56
|
+
* 4. We would return:
|
|
57
|
+
* ```
|
|
58
|
+
* [
|
|
59
|
+
* { name: 'Android Studios.app', directory: '/Application', version: '2023.2' },
|
|
60
|
+
* { name: 'Android Studios Preview.app', directory: '/Application', version: '2024.3' },
|
|
61
|
+
* ]
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @param parameters The parameters to refresh. In stateless mode this will be the parameters
|
|
65
|
+
* of the desired config. In stateful mode, this will be parameters of the state config + the desired
|
|
66
|
+
* config of any new parameters.
|
|
67
|
+
*
|
|
68
|
+
* @return A config or an array of configs representing the status of the resource on the
|
|
69
|
+
* system currently
|
|
70
|
+
*/
|
|
71
|
+
abstract refresh(parameters: Partial<T>): Promise<Array<Partial<T>> | Partial<T> | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Create the resource (install) based on the parameters passed in. Only the desired parameters will
|
|
74
|
+
* be non-null because in a CREATE plan, the current value is null.
|
|
75
|
+
*
|
|
76
|
+
* Example (Android Studios Resource):
|
|
77
|
+
* 1. We receive a plan of:
|
|
78
|
+
* ```
|
|
79
|
+
* Plan {
|
|
80
|
+
* desiredConfig: {
|
|
81
|
+
* name: 'Android Studios.app',
|
|
82
|
+
* directory: '/Application',
|
|
83
|
+
* version: '2023.2'
|
|
84
|
+
* }
|
|
85
|
+
* currentConfig: null,
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
* 2. Install version Android Studios 2023.2 and then return.
|
|
89
|
+
*
|
|
90
|
+
* @param plan The plan of what to install. Use only the desiredConfig because currentConfig is null.
|
|
91
|
+
*/
|
|
92
|
+
abstract create(plan: CreatePlan<T>): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Modify a single parameter of a resource. Modify is optional to override and is only called
|
|
95
|
+
* when a resourceSetting was set to `canModify = true`. This method should only modify
|
|
96
|
+
* a single parameter at a time as specified by the first parameter: ParameterChange.
|
|
97
|
+
*
|
|
98
|
+
* Example (AWS Profile Resource):
|
|
99
|
+
* 1. We receive a parameter change of:
|
|
100
|
+
* ```
|
|
101
|
+
* {
|
|
102
|
+
* name: 'awsAccessKeyId',
|
|
103
|
+
* operation: ParameterOperation.MODIFY,
|
|
104
|
+
* newValue: '123456',
|
|
105
|
+
* previousValue: 'abcdef'
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
* 2. Use an if statement to only apply this operation for the parameter `awsAccessKeyId`
|
|
109
|
+
* 3. Update the value of the `aws_access_key_id` to the `newValue` specified in the parameter change
|
|
110
|
+
*
|
|
111
|
+
* @param pc ParameterChange, the parameter name and values to modify on the resource
|
|
112
|
+
* @param plan The overall plan that triggered the modify operation
|
|
113
|
+
*/
|
|
114
|
+
modify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Destroy the resource (uninstall) based on the parameters passed in. Only the current parameters will
|
|
117
|
+
* be non-null because in a DESTROY plan, the desired value is null. This method will only be called in
|
|
118
|
+
* stateful mode.
|
|
119
|
+
*
|
|
120
|
+
* Example (Android Studios Resource):
|
|
121
|
+
* 1. We receive a plan of:
|
|
122
|
+
* ```
|
|
123
|
+
* Plan {
|
|
124
|
+
* currentConfig: {
|
|
125
|
+
* name: 'Android Studios.app',
|
|
126
|
+
* directory: '/Application',
|
|
127
|
+
* version: '2022.4'
|
|
128
|
+
* },
|
|
129
|
+
* desiredConfig: null
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
* 2. Uninstall version Android Studios 2022.4 and then return.
|
|
133
|
+
*
|
|
134
|
+
* @param plan The plan of what to uninstall. Use only the currentConfig because desiredConfig is null.
|
|
135
|
+
*/
|
|
136
|
+
abstract destroy(plan: DestroyPlan<T>): Promise<void>;
|
|
137
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A resource represents an object on the system (application, CLI tool, or setting)
|
|
3
|
+
* that has state and can be created and destroyed. Examples of resources include CLI tools
|
|
4
|
+
* like homebrew, docker, and xcode-tools; applications like Google Chrome, Zoom, and OpenVPN;
|
|
5
|
+
* and settings like AWS profiles, git configs and system preference settings.
|
|
6
|
+
*/
|
|
7
|
+
export class Resource {
|
|
8
|
+
async initialize() {
|
|
9
|
+
}
|
|
10
|
+
;
|
|
11
|
+
/**
|
|
12
|
+
* Add custom validation logic in-addition to the default schema validation.
|
|
13
|
+
* In this method throw an error if the object did not validate. The message of the
|
|
14
|
+
* error will be shown to the user.
|
|
15
|
+
* @param parameters
|
|
16
|
+
*/
|
|
17
|
+
async validate(parameters) {
|
|
18
|
+
}
|
|
19
|
+
;
|
|
20
|
+
/**
|
|
21
|
+
* Modify a single parameter of a resource. Modify is optional to override and is only called
|
|
22
|
+
* when a resourceSetting was set to `canModify = true`. This method should only modify
|
|
23
|
+
* a single parameter at a time as specified by the first parameter: ParameterChange.
|
|
24
|
+
*
|
|
25
|
+
* Example (AWS Profile Resource):
|
|
26
|
+
* 1. We receive a parameter change of:
|
|
27
|
+
* ```
|
|
28
|
+
* {
|
|
29
|
+
* name: 'awsAccessKeyId',
|
|
30
|
+
* operation: ParameterOperation.MODIFY,
|
|
31
|
+
* newValue: '123456',
|
|
32
|
+
* previousValue: 'abcdef'
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
* 2. Use an if statement to only apply this operation for the parameter `awsAccessKeyId`
|
|
36
|
+
* 3. Update the value of the `aws_access_key_id` to the `newValue` specified in the parameter change
|
|
37
|
+
*
|
|
38
|
+
* @param pc ParameterChange, the parameter name and values to modify on the resource
|
|
39
|
+
* @param plan The overall plan that triggered the modify operation
|
|
40
|
+
*/
|
|
41
|
+
async modify(pc, plan) {
|
|
42
|
+
}
|
|
43
|
+
;
|
|
44
|
+
}
|