codify-plugin-lib 1.0.178 → 1.0.179
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/change-set.d.ts +24 -0
- package/dist/entities/change-set.js +152 -0
- package/dist/entities/errors.d.ts +4 -0
- package/dist/entities/errors.js +7 -0
- package/dist/entities/plan-types.d.ts +25 -0
- package/dist/entities/plan-types.js +1 -0
- package/dist/entities/plan.d.ts +15 -0
- package/dist/entities/plan.js +127 -0
- package/dist/entities/plugin.d.ts +16 -0
- package/dist/entities/plugin.js +80 -0
- package/dist/entities/resource-options.d.ts +31 -0
- package/dist/entities/resource-options.js +76 -0
- package/dist/entities/resource-types.d.ts +11 -0
- package/dist/entities/resource-types.js +1 -0
- package/dist/entities/resource.d.ts +42 -0
- package/dist/entities/resource.js +303 -0
- package/dist/entities/stateful-parameter.d.ts +29 -0
- package/dist/entities/stateful-parameter.js +46 -0
- package/dist/entities/transform-parameter.d.ts +4 -0
- package/dist/entities/transform-parameter.js +2 -0
- package/dist/plan/change-set.d.ts +8 -3
- package/dist/plan/change-set.js +10 -3
- package/dist/plan/plan.js +7 -4
- package/dist/plugin/plugin.d.ts +1 -1
- package/dist/plugin/plugin.js +8 -1
- package/dist/pty/vitest.config.d.ts +2 -0
- package/dist/pty/vitest.config.js +11 -0
- package/dist/resource/resource-settings.d.ts +6 -0
- package/dist/resource/stateful-parameter.d.ts +165 -0
- package/dist/resource/stateful-parameter.js +94 -0
- package/dist/utils/spawn-2.d.ts +5 -0
- package/dist/utils/spawn-2.js +7 -0
- package/dist/utils/spawn.d.ts +29 -0
- package/dist/utils/spawn.js +124 -0
- package/package.json +2 -2
- package/src/plan/change-set.test.ts +1 -1
- package/src/plan/change-set.ts +16 -3
- package/src/plan/plan.ts +7 -4
- package/src/plugin/plugin.ts +9 -1
- package/src/resource/resource-controller-stateful-mode.test.ts +15 -7
- package/src/resource/resource-controller.test.ts +4 -2
- package/src/resource/resource-settings.test.ts +2 -0
- package/src/resource/resource-settings.ts +8 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
|
|
3
|
+
import { setsEqual, splitUserConfig } from '../utils/utils.js';
|
|
4
|
+
import { Plan } from './plan.js';
|
|
5
|
+
import { ResourceOptionsParser } from './resource-options.js';
|
|
6
|
+
export class Resource {
|
|
7
|
+
typeId;
|
|
8
|
+
statefulParameters;
|
|
9
|
+
transformParameters;
|
|
10
|
+
resourceParameters;
|
|
11
|
+
statefulParameterOrder;
|
|
12
|
+
transformParameterOrder;
|
|
13
|
+
dependencies;
|
|
14
|
+
parameterOptions;
|
|
15
|
+
options;
|
|
16
|
+
defaultValues;
|
|
17
|
+
ajv;
|
|
18
|
+
schemaValidator;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.typeId = options.type;
|
|
21
|
+
this.dependencies = options.dependencies ?? [];
|
|
22
|
+
this.options = options;
|
|
23
|
+
if (this.options.schema) {
|
|
24
|
+
this.ajv = new Ajv({
|
|
25
|
+
allErrors: true,
|
|
26
|
+
strict: true,
|
|
27
|
+
strictRequired: false,
|
|
28
|
+
});
|
|
29
|
+
this.schemaValidator = this.ajv.compile(this.options.schema);
|
|
30
|
+
}
|
|
31
|
+
const parser = new ResourceOptionsParser(options);
|
|
32
|
+
this.statefulParameters = parser.statefulParameters;
|
|
33
|
+
this.transformParameters = parser.transformParameters;
|
|
34
|
+
this.resourceParameters = parser.resourceParameters;
|
|
35
|
+
this.parameterOptions = parser.changeSetParameterOptions;
|
|
36
|
+
this.defaultValues = parser.defaultValues;
|
|
37
|
+
this.statefulParameterOrder = parser.statefulParameterOrder;
|
|
38
|
+
this.transformParameterOrder = parser.transformParameterOrder;
|
|
39
|
+
}
|
|
40
|
+
async onInitialize() { }
|
|
41
|
+
async validate(parameters, resourceMetaData) {
|
|
42
|
+
if (this.schemaValidator) {
|
|
43
|
+
const isValid = this.schemaValidator(parameters);
|
|
44
|
+
if (!isValid) {
|
|
45
|
+
return {
|
|
46
|
+
isValid: false,
|
|
47
|
+
resourceName: resourceMetaData.name,
|
|
48
|
+
resourceType: resourceMetaData.type,
|
|
49
|
+
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let isValid = true;
|
|
54
|
+
let customValidationErrorMessage;
|
|
55
|
+
try {
|
|
56
|
+
await this.customValidation(parameters);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
isValid = false;
|
|
60
|
+
customValidationErrorMessage = error.message;
|
|
61
|
+
}
|
|
62
|
+
if (!isValid) {
|
|
63
|
+
return {
|
|
64
|
+
customValidationErrorMessage,
|
|
65
|
+
isValid: false,
|
|
66
|
+
resourceName: resourceMetaData.name,
|
|
67
|
+
resourceType: resourceMetaData.type,
|
|
68
|
+
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
isValid: true,
|
|
73
|
+
resourceName: resourceMetaData.name,
|
|
74
|
+
resourceType: resourceMetaData.type,
|
|
75
|
+
schemaValidationErrors: [],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async plan(desiredConfig, currentConfig = null, statefulMode = false) {
|
|
79
|
+
this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
|
|
80
|
+
const planOptions = {
|
|
81
|
+
parameterOptions: this.parameterOptions,
|
|
82
|
+
statefulMode,
|
|
83
|
+
};
|
|
84
|
+
this.addDefaultValues(desiredConfig);
|
|
85
|
+
await this.applyTransformParameters(desiredConfig);
|
|
86
|
+
const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters);
|
|
87
|
+
const { desiredParameters, nonStatefulParameters, resourceMetadata, statefulParameters, } = parsedConfig;
|
|
88
|
+
const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
|
|
89
|
+
if (currentParameters === null || currentParameters === undefined) {
|
|
90
|
+
return Plan.create(desiredParameters, null, resourceMetadata, planOptions);
|
|
91
|
+
}
|
|
92
|
+
const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
|
|
93
|
+
return Plan.create(desiredParameters, { ...currentParameters, ...statefulCurrentParameters }, resourceMetadata, planOptions);
|
|
94
|
+
}
|
|
95
|
+
async apply(plan) {
|
|
96
|
+
if (plan.getResourceType() !== this.typeId) {
|
|
97
|
+
throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
|
|
98
|
+
}
|
|
99
|
+
switch (plan.changeSet.operation) {
|
|
100
|
+
case ResourceOperation.CREATE: {
|
|
101
|
+
return this._applyCreate(plan);
|
|
102
|
+
}
|
|
103
|
+
case ResourceOperation.MODIFY: {
|
|
104
|
+
return this._applyModify(plan);
|
|
105
|
+
}
|
|
106
|
+
case ResourceOperation.RECREATE: {
|
|
107
|
+
await this._applyDestroy(plan);
|
|
108
|
+
return this._applyCreate(plan);
|
|
109
|
+
}
|
|
110
|
+
case ResourceOperation.DESTROY: {
|
|
111
|
+
return this._applyDestroy(plan);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async _applyCreate(plan) {
|
|
116
|
+
await this.applyCreate(plan);
|
|
117
|
+
const statefulParameterChanges = plan.changeSet.parameterChanges
|
|
118
|
+
.filter((pc) => this.statefulParameters.has(pc.name))
|
|
119
|
+
.sort((a, b) => this.statefulParameterOrder.get(a.name) - this.statefulParameterOrder.get(b.name));
|
|
120
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
121
|
+
const statefulParameter = this.statefulParameters.get(parameterChange.name);
|
|
122
|
+
await statefulParameter.applyAdd(parameterChange.newValue, plan);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async _applyModify(plan) {
|
|
126
|
+
const parameterChanges = plan
|
|
127
|
+
.changeSet
|
|
128
|
+
.parameterChanges
|
|
129
|
+
.filter((c) => c.operation !== ParameterOperation.NOOP);
|
|
130
|
+
const statelessParameterChanges = parameterChanges
|
|
131
|
+
.filter((pc) => !this.statefulParameters.has(pc.name));
|
|
132
|
+
for (const pc of statelessParameterChanges) {
|
|
133
|
+
await this.applyModify(pc, plan);
|
|
134
|
+
}
|
|
135
|
+
const statefulParameterChanges = parameterChanges
|
|
136
|
+
.filter((pc) => this.statefulParameters.has(pc.name))
|
|
137
|
+
.sort((a, b) => this.statefulParameterOrder.get(a.name) - this.statefulParameterOrder.get(b.name));
|
|
138
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
139
|
+
const statefulParameter = this.statefulParameters.get(parameterChange.name);
|
|
140
|
+
switch (parameterChange.operation) {
|
|
141
|
+
case ParameterOperation.ADD: {
|
|
142
|
+
await statefulParameter.applyAdd(parameterChange.newValue, plan);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case ParameterOperation.MODIFY: {
|
|
146
|
+
await statefulParameter.applyModify(parameterChange.newValue, parameterChange.previousValue, false, plan);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case ParameterOperation.REMOVE: {
|
|
150
|
+
await statefulParameter.applyRemove(parameterChange.previousValue, plan);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async _applyDestroy(plan) {
|
|
157
|
+
if (this.options.callStatefulParameterRemoveOnDestroy) {
|
|
158
|
+
const statefulParameterChanges = plan.changeSet.parameterChanges
|
|
159
|
+
.filter((pc) => this.statefulParameters.has(pc.name))
|
|
160
|
+
.sort((a, b) => this.statefulParameterOrder.get(a.name) - this.statefulParameterOrder.get(b.name));
|
|
161
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
162
|
+
const statefulParameter = this.statefulParameters.get(parameterChange.name);
|
|
163
|
+
await statefulParameter.applyRemove(parameterChange.previousValue, plan);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
await this.applyDestroy(plan);
|
|
167
|
+
}
|
|
168
|
+
validateRefreshResults(refresh, desired) {
|
|
169
|
+
if (!refresh) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const desiredKeys = new Set(Object.keys(refresh));
|
|
173
|
+
const refreshKeys = new Set(Object.keys(refresh));
|
|
174
|
+
if (!setsEqual(desiredKeys, refreshKeys)) {
|
|
175
|
+
throw new Error(`Resource ${this.typeId}
|
|
176
|
+
refresh() must return back exactly the keys that were provided
|
|
177
|
+
Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
|
|
178
|
+
Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async applyTransformParameters(desired) {
|
|
182
|
+
if (!desired) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const transformParameters = [...this.transformParameters.entries()]
|
|
186
|
+
.sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA) - this.transformParameterOrder.get(keyB));
|
|
187
|
+
for (const [key, transformParameter] of transformParameters) {
|
|
188
|
+
if (desired[key] === undefined) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const transformedValue = await transformParameter.transform(desired[key]);
|
|
192
|
+
if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
|
|
193
|
+
throw new Error(`Transform parameter ${key} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
|
|
194
|
+
}
|
|
195
|
+
delete desired[key];
|
|
196
|
+
for (const [tvKey, tvValue] of Object.entries(transformedValue)) {
|
|
197
|
+
desired[tvKey] = tvValue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
addDefaultValues(desired) {
|
|
202
|
+
if (!desired) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const [key, defaultValue] of Object.entries(this.defaultValues)) {
|
|
206
|
+
if (defaultValue !== undefined && desired[key] === undefined) {
|
|
207
|
+
desired[key] = defaultValue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async refreshNonStatefulParameters(resourceParameters) {
|
|
212
|
+
const currentParameters = await this.refresh(resourceParameters);
|
|
213
|
+
this.validateRefreshResults(currentParameters, resourceParameters);
|
|
214
|
+
return currentParameters;
|
|
215
|
+
}
|
|
216
|
+
async refreshStatefulParameters(statefulParametersConfig, isStatefulMode) {
|
|
217
|
+
const currentParameters = {};
|
|
218
|
+
const sortedEntries = Object.entries(statefulParametersConfig)
|
|
219
|
+
.sort(([key1], [key2]) => this.statefulParameterOrder.get(key1) - this.statefulParameterOrder.get(key2));
|
|
220
|
+
for (const [key, desiredValue] of sortedEntries) {
|
|
221
|
+
const statefulParameter = this.statefulParameters.get(key);
|
|
222
|
+
if (!statefulParameter) {
|
|
223
|
+
throw new Error(`Stateful parameter ${key} was not found`);
|
|
224
|
+
}
|
|
225
|
+
let currentValue = await statefulParameter.refresh(desiredValue ?? null);
|
|
226
|
+
if (Array.isArray(currentValue)
|
|
227
|
+
&& Array.isArray(desiredValue)
|
|
228
|
+
&& !isStatefulMode
|
|
229
|
+
&& !statefulParameter.options.disableStatelessModeArrayFiltering) {
|
|
230
|
+
currentValue = currentValue.filter((c) => desiredValue?.some((d) => {
|
|
231
|
+
const parameterOptions = statefulParameter.options;
|
|
232
|
+
if (parameterOptions && parameterOptions.isElementEqual) {
|
|
233
|
+
return parameterOptions.isElementEqual(d, c);
|
|
234
|
+
}
|
|
235
|
+
return d === c;
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
currentParameters[key] = currentValue;
|
|
239
|
+
}
|
|
240
|
+
return currentParameters;
|
|
241
|
+
}
|
|
242
|
+
validatePlanInputs(desired, current, statefulMode) {
|
|
243
|
+
if (!desired && !current) {
|
|
244
|
+
throw new Error('Desired config and current config cannot both be missing');
|
|
245
|
+
}
|
|
246
|
+
if (!statefulMode && !desired) {
|
|
247
|
+
throw new Error('Desired config must be provided in non-stateful mode');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async customValidation(parameters) { }
|
|
251
|
+
;
|
|
252
|
+
async applyModify(pc, plan) { }
|
|
253
|
+
;
|
|
254
|
+
}
|
|
255
|
+
class ConfigParser {
|
|
256
|
+
desiredConfig;
|
|
257
|
+
currentConfig;
|
|
258
|
+
statefulParametersMap;
|
|
259
|
+
transformParametersMap;
|
|
260
|
+
constructor(desiredConfig, currentConfig, statefulParameters, transformParameters) {
|
|
261
|
+
this.desiredConfig = desiredConfig;
|
|
262
|
+
this.currentConfig = currentConfig;
|
|
263
|
+
this.statefulParametersMap = statefulParameters;
|
|
264
|
+
this.transformParametersMap = transformParameters;
|
|
265
|
+
}
|
|
266
|
+
get resourceMetadata() {
|
|
267
|
+
const desiredMetadata = this.desiredConfig ? splitUserConfig(this.desiredConfig).resourceMetadata : undefined;
|
|
268
|
+
const currentMetadata = this.currentConfig ? splitUserConfig(this.currentConfig).resourceMetadata : undefined;
|
|
269
|
+
if (!desiredMetadata && !currentMetadata) {
|
|
270
|
+
throw new Error(`Unable to parse resource metadata from ${this.desiredConfig}, ${this.currentConfig}`);
|
|
271
|
+
}
|
|
272
|
+
if (currentMetadata && desiredMetadata && (Object.keys(desiredMetadata).length !== Object.keys(currentMetadata).length
|
|
273
|
+
|| Object.entries(desiredMetadata).some(([key, value]) => currentMetadata[key] !== value))) {
|
|
274
|
+
throw new Error(`The metadata for the current config does not match the desired config.
|
|
275
|
+
Desired metadata:
|
|
276
|
+
${JSON.stringify(desiredMetadata, null, 2)}
|
|
277
|
+
|
|
278
|
+
Current metadata:
|
|
279
|
+
${JSON.stringify(currentMetadata, null, 2)}`);
|
|
280
|
+
}
|
|
281
|
+
return desiredMetadata ?? currentMetadata;
|
|
282
|
+
}
|
|
283
|
+
get desiredParameters() {
|
|
284
|
+
if (!this.desiredConfig) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const { parameters } = splitUserConfig(this.desiredConfig);
|
|
288
|
+
return parameters;
|
|
289
|
+
}
|
|
290
|
+
get parameters() {
|
|
291
|
+
const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
|
|
292
|
+
const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
|
|
293
|
+
return { ...desiredParameters, ...currentParameters };
|
|
294
|
+
}
|
|
295
|
+
get nonStatefulParameters() {
|
|
296
|
+
const { parameters } = this;
|
|
297
|
+
return Object.fromEntries(Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))));
|
|
298
|
+
}
|
|
299
|
+
get statefulParameters() {
|
|
300
|
+
const { parameters } = this;
|
|
301
|
+
return Object.fromEntries(Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Plan } from './plan.js';
|
|
2
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
3
|
+
export interface StatefulParameterOptions<V> {
|
|
4
|
+
isEqual?: (desired: any, current: any) => boolean;
|
|
5
|
+
disableStatelessModeArrayFiltering?: boolean;
|
|
6
|
+
default?: V;
|
|
7
|
+
}
|
|
8
|
+
export interface ArrayStatefulParameterOptions<V> extends StatefulParameterOptions<V> {
|
|
9
|
+
isEqual?: (desired: any[], current: any[]) => boolean;
|
|
10
|
+
isElementEqual?: (desired: any, current: any) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
|
|
13
|
+
readonly options: StatefulParameterOptions<V>;
|
|
14
|
+
constructor(options?: StatefulParameterOptions<V>);
|
|
15
|
+
abstract refresh(desired: V | null): Promise<V | null>;
|
|
16
|
+
abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
|
|
17
|
+
abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
|
|
18
|
+
abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any> {
|
|
21
|
+
options: ArrayStatefulParameterOptions<V>;
|
|
22
|
+
constructor(options?: ArrayStatefulParameterOptions<V>);
|
|
23
|
+
applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void>;
|
|
24
|
+
applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void>;
|
|
25
|
+
applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void>;
|
|
26
|
+
abstract refresh(desired: V[] | null): Promise<V[] | null>;
|
|
27
|
+
abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
|
|
28
|
+
abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class StatefulParameter {
|
|
2
|
+
options;
|
|
3
|
+
constructor(options = {}) {
|
|
4
|
+
this.options = options;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class ArrayStatefulParameter extends StatefulParameter {
|
|
8
|
+
options;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
async applyAdd(valuesToAdd, plan) {
|
|
14
|
+
for (const value of valuesToAdd) {
|
|
15
|
+
await this.applyAddItem(value, plan);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async applyModify(newValues, previousValues, allowDeletes, plan) {
|
|
19
|
+
const options = this.options;
|
|
20
|
+
const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
|
|
21
|
+
if (options.isElementEqual) {
|
|
22
|
+
return options.isElementEqual(n, p);
|
|
23
|
+
}
|
|
24
|
+
return n === p;
|
|
25
|
+
}));
|
|
26
|
+
const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
|
|
27
|
+
if (options.isElementEqual) {
|
|
28
|
+
return options.isElementEqual(n, p);
|
|
29
|
+
}
|
|
30
|
+
return n === p;
|
|
31
|
+
}));
|
|
32
|
+
for (const value of valuesToAdd) {
|
|
33
|
+
await this.applyAddItem(value, plan);
|
|
34
|
+
}
|
|
35
|
+
if (allowDeletes) {
|
|
36
|
+
for (const value of valuesToRemove) {
|
|
37
|
+
await this.applyRemoveItem(value, plan);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async applyRemove(valuesToRemove, plan) {
|
|
42
|
+
for (const value of valuesToRemove) {
|
|
43
|
+
await this.applyRemoveItem(value, plan);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
2
2
|
import { ParsedParameterSetting } from '../resource/parsed-resource-settings.js';
|
|
3
|
+
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
3
4
|
/**
|
|
4
5
|
* A parameter change describes a parameter level change to a resource.
|
|
5
6
|
*/
|
|
@@ -20,6 +21,10 @@ export interface ParameterChange<T extends StringIndexedObject> {
|
|
|
20
21
|
* The new value of the resource (the desired value)
|
|
21
22
|
*/
|
|
22
23
|
newValue: any | null;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the parameter is sensitive
|
|
26
|
+
*/
|
|
27
|
+
isSensitive: boolean;
|
|
23
28
|
}
|
|
24
29
|
export declare class ChangeSet<T extends StringIndexedObject> {
|
|
25
30
|
operation: ResourceOperation;
|
|
@@ -28,9 +33,9 @@ export declare class ChangeSet<T extends StringIndexedObject> {
|
|
|
28
33
|
get desiredParameters(): T;
|
|
29
34
|
get currentParameters(): T;
|
|
30
35
|
static empty<T extends StringIndexedObject>(): ChangeSet<T>;
|
|
31
|
-
static create<T extends StringIndexedObject>(desired: Partial<T>): ChangeSet<T>;
|
|
32
|
-
static noop<T extends StringIndexedObject>(parameters: Partial<T>): ChangeSet<T>;
|
|
33
|
-
static destroy<T extends StringIndexedObject>(current: Partial<T>): ChangeSet<T>;
|
|
36
|
+
static create<T extends StringIndexedObject>(desired: Partial<T>, settings?: ResourceSettings<T>): ChangeSet<T>;
|
|
37
|
+
static noop<T extends StringIndexedObject>(parameters: Partial<T>, settings?: ResourceSettings<T>): ChangeSet<T>;
|
|
38
|
+
static destroy<T extends StringIndexedObject>(current: Partial<T>, settings?: ResourceSettings<T>): ChangeSet<T>;
|
|
34
39
|
static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParsedParameterSetting>>): ChangeSet<T>;
|
|
35
40
|
/**
|
|
36
41
|
* Calculates the differences between the desired and current parameters,
|
package/dist/plan/change-set.js
CHANGED
|
@@ -24,33 +24,36 @@ export class ChangeSet {
|
|
|
24
24
|
static empty() {
|
|
25
25
|
return new ChangeSet(ResourceOperation.NOOP, []);
|
|
26
26
|
}
|
|
27
|
-
static create(desired) {
|
|
27
|
+
static create(desired, settings) {
|
|
28
28
|
const parameterChanges = Object.entries(desired)
|
|
29
29
|
.map(([k, v]) => ({
|
|
30
30
|
name: k,
|
|
31
31
|
operation: ParameterOperation.ADD,
|
|
32
32
|
previousValue: null,
|
|
33
33
|
newValue: v ?? null,
|
|
34
|
+
isSensitive: settings?.parameterSettings?.[k]?.isSensitive ?? false,
|
|
34
35
|
}));
|
|
35
36
|
return new ChangeSet(ResourceOperation.CREATE, parameterChanges);
|
|
36
37
|
}
|
|
37
|
-
static noop(parameters) {
|
|
38
|
+
static noop(parameters, settings) {
|
|
38
39
|
const parameterChanges = Object.entries(parameters)
|
|
39
40
|
.map(([k, v]) => ({
|
|
40
41
|
name: k,
|
|
41
42
|
operation: ParameterOperation.NOOP,
|
|
42
43
|
previousValue: v ?? null,
|
|
43
44
|
newValue: v ?? null,
|
|
45
|
+
isSensitive: settings?.parameterSettings?.[k]?.isSensitive ?? false,
|
|
44
46
|
}));
|
|
45
47
|
return new ChangeSet(ResourceOperation.NOOP, parameterChanges);
|
|
46
48
|
}
|
|
47
|
-
static destroy(current) {
|
|
49
|
+
static destroy(current, settings) {
|
|
48
50
|
const parameterChanges = Object.entries(current)
|
|
49
51
|
.map(([k, v]) => ({
|
|
50
52
|
name: k,
|
|
51
53
|
operation: ParameterOperation.REMOVE,
|
|
52
54
|
previousValue: v ?? null,
|
|
53
55
|
newValue: null,
|
|
56
|
+
isSensitive: settings?.parameterSettings?.[k]?.isSensitive ?? false,
|
|
54
57
|
}));
|
|
55
58
|
return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
|
|
56
59
|
}
|
|
@@ -98,6 +101,7 @@ export class ChangeSet {
|
|
|
98
101
|
previousValue: current[k] ?? null,
|
|
99
102
|
newValue: desired[k] ?? null,
|
|
100
103
|
operation: ParameterOperation.NOOP,
|
|
104
|
+
isSensitive: parameterOptions?.[k]?.isSensitive ?? false,
|
|
101
105
|
});
|
|
102
106
|
continue;
|
|
103
107
|
}
|
|
@@ -107,6 +111,7 @@ export class ChangeSet {
|
|
|
107
111
|
previousValue: current[k] ?? null,
|
|
108
112
|
newValue: null,
|
|
109
113
|
operation: ParameterOperation.REMOVE,
|
|
114
|
+
isSensitive: parameterOptions?.[k]?.isSensitive ?? false,
|
|
110
115
|
});
|
|
111
116
|
continue;
|
|
112
117
|
}
|
|
@@ -116,6 +121,7 @@ export class ChangeSet {
|
|
|
116
121
|
previousValue: null,
|
|
117
122
|
newValue: desired[k] ?? null,
|
|
118
123
|
operation: ParameterOperation.ADD,
|
|
124
|
+
isSensitive: parameterOptions?.[k]?.isSensitive ?? false,
|
|
119
125
|
});
|
|
120
126
|
continue;
|
|
121
127
|
}
|
|
@@ -124,6 +130,7 @@ export class ChangeSet {
|
|
|
124
130
|
previousValue: current[k] ?? null,
|
|
125
131
|
newValue: desired[k] ?? null,
|
|
126
132
|
operation: ParameterOperation.MODIFY,
|
|
133
|
+
isSensitive: parameterOptions?.[k]?.isSensitive ?? false,
|
|
127
134
|
});
|
|
128
135
|
}
|
|
129
136
|
return parameterChangeSet;
|
package/dist/plan/plan.js
CHANGED
|
@@ -68,15 +68,15 @@ export class Plan {
|
|
|
68
68
|
}
|
|
69
69
|
// CREATE
|
|
70
70
|
if (!filteredCurrentParameters && desired) {
|
|
71
|
-
return new Plan(uuidV4(), ChangeSet.create(desired), core, isStateful);
|
|
71
|
+
return new Plan(uuidV4(), ChangeSet.create(desired, settings), core, isStateful);
|
|
72
72
|
}
|
|
73
73
|
// DESTROY
|
|
74
74
|
if (filteredCurrentParameters && !desired) {
|
|
75
75
|
// We can manually override destroys. If a resource cannot be destroyed (for instance the npm resource relies on NodeJS being created and destroyed)
|
|
76
76
|
if (!settings.canDestroy) {
|
|
77
|
-
return new Plan(uuidV4(), ChangeSet.noop(filteredCurrentParameters), core, isStateful);
|
|
77
|
+
return new Plan(uuidV4(), ChangeSet.noop(filteredCurrentParameters, settings), core, isStateful);
|
|
78
78
|
}
|
|
79
|
-
return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), core, isStateful);
|
|
79
|
+
return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters, settings), core, isStateful);
|
|
80
80
|
}
|
|
81
81
|
// NO-OP, MODIFY or RE-CREATE
|
|
82
82
|
const changeSet = ChangeSet.calculateModification(desired, filteredCurrentParameters, settings.parameterSettings);
|
|
@@ -88,7 +88,10 @@ export class Plan {
|
|
|
88
88
|
throw new Error('Data is empty');
|
|
89
89
|
}
|
|
90
90
|
addDefaultValues();
|
|
91
|
-
return new Plan(uuidV4(), new ChangeSet(data.operation, data.parameters)
|
|
91
|
+
return new Plan(uuidV4(), new ChangeSet(data.operation, data.parameters.map((p) => ({
|
|
92
|
+
...p,
|
|
93
|
+
isSensitive: p.isSensitive ?? false,
|
|
94
|
+
}))), {
|
|
92
95
|
type: data.resourceType,
|
|
93
96
|
name: data.resourceName,
|
|
94
97
|
}, data.isStateful);
|
package/dist/plugin/plugin.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare class Plugin {
|
|
|
11
11
|
constructor(name: string, resourceControllers: Map<string, ResourceController<ResourceConfig>>);
|
|
12
12
|
static create(name: string, resources: Resource<any>[]): Plugin;
|
|
13
13
|
initialize(data: InitializeRequestData): Promise<InitializeResponseData>;
|
|
14
|
-
getResourceInfo(data: GetResourceInfoRequestData):
|
|
14
|
+
getResourceInfo(data: GetResourceInfoRequestData): GetResourceInfoResponseData;
|
|
15
15
|
match(data: MatchRequestData): Promise<MatchResponseData>;
|
|
16
16
|
import(data: ImportRequestData): Promise<ImportResponseData>;
|
|
17
17
|
validate(data: ValidateRequestData): Promise<ValidateResponseData>;
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -33,10 +33,13 @@ export class Plugin {
|
|
|
33
33
|
.map((r) => ({
|
|
34
34
|
dependencies: r.dependencies,
|
|
35
35
|
type: r.typeId,
|
|
36
|
+
sensitiveParameters: Object.entries(r.settings.parameterSettings ?? {})
|
|
37
|
+
.filter(([, v]) => v?.isSensitive)
|
|
38
|
+
.map(([k]) => k),
|
|
36
39
|
}))
|
|
37
40
|
};
|
|
38
41
|
}
|
|
39
|
-
|
|
42
|
+
getResourceInfo(data) {
|
|
40
43
|
if (!this.resourceControllers.has(data.type)) {
|
|
41
44
|
throw new Error(`Cannot get info for resource ${data.type}, resource doesn't exist`);
|
|
42
45
|
}
|
|
@@ -48,6 +51,9 @@ export class Plugin {
|
|
|
48
51
|
?? undefined);
|
|
49
52
|
const allowMultiple = resource.settings.allowMultiple !== undefined
|
|
50
53
|
&& resource.settings.allowMultiple !== false;
|
|
54
|
+
const sensitiveParameters = Object.entries(resource.settings.parameterSettings ?? {})
|
|
55
|
+
.filter(([, v]) => v?.isSensitive)
|
|
56
|
+
.map(([k]) => k);
|
|
51
57
|
return {
|
|
52
58
|
plugin: this.name,
|
|
53
59
|
type: data.type,
|
|
@@ -60,6 +66,7 @@ export class Plugin {
|
|
|
60
66
|
import: {
|
|
61
67
|
requiredParameters: requiredPropertyNames,
|
|
62
68
|
},
|
|
69
|
+
sensitiveParameters,
|
|
63
70
|
allowMultiple
|
|
64
71
|
};
|
|
65
72
|
}
|
|
@@ -167,6 +167,12 @@ export interface DefaultParameterSetting {
|
|
|
167
167
|
* is mainly used to determine the equality method when performing diffing.
|
|
168
168
|
*/
|
|
169
169
|
type?: ParameterSettingType;
|
|
170
|
+
/**
|
|
171
|
+
* Mark the field as sensitive. Defaults to false. This has two side effects:
|
|
172
|
+
* 1. When displaying this field in the plan, it will be replaced with asterisks
|
|
173
|
+
* 2. When importing, resources with sensitive fields will be skipped unless the user explicitly allows it.
|
|
174
|
+
*/
|
|
175
|
+
isSensitive?: boolean;
|
|
170
176
|
/**
|
|
171
177
|
* Default value for the parameter. If a value is not provided in the config, then this value will be used.
|
|
172
178
|
*/
|