codify-plugin-lib 1.0.178 → 1.0.180

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.
Files changed (43) hide show
  1. package/dist/entities/change-set.d.ts +24 -0
  2. package/dist/entities/change-set.js +152 -0
  3. package/dist/entities/errors.d.ts +4 -0
  4. package/dist/entities/errors.js +7 -0
  5. package/dist/entities/plan-types.d.ts +25 -0
  6. package/dist/entities/plan-types.js +1 -0
  7. package/dist/entities/plan.d.ts +15 -0
  8. package/dist/entities/plan.js +127 -0
  9. package/dist/entities/plugin.d.ts +16 -0
  10. package/dist/entities/plugin.js +80 -0
  11. package/dist/entities/resource-options.d.ts +31 -0
  12. package/dist/entities/resource-options.js +76 -0
  13. package/dist/entities/resource-types.d.ts +11 -0
  14. package/dist/entities/resource-types.js +1 -0
  15. package/dist/entities/resource.d.ts +42 -0
  16. package/dist/entities/resource.js +303 -0
  17. package/dist/entities/stateful-parameter.d.ts +29 -0
  18. package/dist/entities/stateful-parameter.js +46 -0
  19. package/dist/entities/transform-parameter.d.ts +4 -0
  20. package/dist/entities/transform-parameter.js +2 -0
  21. package/dist/plan/change-set.d.ts +8 -3
  22. package/dist/plan/change-set.js +10 -3
  23. package/dist/plan/plan.js +7 -4
  24. package/dist/plugin/plugin.d.ts +1 -1
  25. package/dist/plugin/plugin.js +27 -5
  26. package/dist/pty/vitest.config.d.ts +2 -0
  27. package/dist/pty/vitest.config.js +11 -0
  28. package/dist/resource/resource-settings.d.ts +11 -0
  29. package/dist/resource/stateful-parameter.d.ts +165 -0
  30. package/dist/resource/stateful-parameter.js +94 -0
  31. package/dist/utils/spawn-2.d.ts +5 -0
  32. package/dist/utils/spawn-2.js +7 -0
  33. package/dist/utils/spawn.d.ts +29 -0
  34. package/dist/utils/spawn.js +124 -0
  35. package/package.json +2 -2
  36. package/src/plan/change-set.test.ts +1 -1
  37. package/src/plan/change-set.ts +16 -3
  38. package/src/plan/plan.ts +7 -4
  39. package/src/plugin/plugin.ts +31 -5
  40. package/src/resource/resource-controller-stateful-mode.test.ts +15 -7
  41. package/src/resource/resource-controller.test.ts +4 -2
  42. package/src/resource/resource-settings.test.ts +2 -0
  43. package/src/resource/resource-settings.ts +14 -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
+ }
@@ -0,0 +1,4 @@
1
+ import { StringIndexedObject } from 'codify-schemas';
2
+ export declare abstract class TransformParameter<T extends StringIndexedObject> {
3
+ abstract transform(value: any): Promise<Partial<T>>;
4
+ }
@@ -0,0 +1,2 @@
1
+ export class TransformParameter {
2
+ }
@@ -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,
@@ -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);
@@ -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): Promise<GetResourceInfoResponseData>;
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>;
@@ -30,13 +30,25 @@ export class Plugin {
30
30
  }
31
31
  return {
32
32
  resourceDefinitions: [...this.resourceControllers.values()]
33
- .map((r) => ({
34
- dependencies: r.dependencies,
35
- type: r.typeId,
36
- }))
33
+ .map((r) => {
34
+ const sensitiveParameters = Object.entries(r.settings.parameterSettings ?? {})
35
+ .filter(([, v]) => v?.isSensitive)
36
+ .map(([k]) => k);
37
+ // Here we add '*' if the resource is sensitive but no sensitive parameters are found. This works because the import
38
+ // sensitivity check only checks for the existance of a sensitive parameter whereas the parameter blocking one blocks
39
+ // on a specific sensitive parameter.
40
+ if (r.settings.isSensitive && sensitiveParameters.length === 0) {
41
+ sensitiveParameters.push('*');
42
+ }
43
+ return {
44
+ dependencies: r.dependencies,
45
+ type: r.typeId,
46
+ sensitiveParameters,
47
+ };
48
+ })
37
49
  };
38
50
  }
39
- async getResourceInfo(data) {
51
+ getResourceInfo(data) {
40
52
  if (!this.resourceControllers.has(data.type)) {
41
53
  throw new Error(`Cannot get info for resource ${data.type}, resource doesn't exist`);
42
54
  }
@@ -48,6 +60,15 @@ export class Plugin {
48
60
  ?? undefined);
49
61
  const allowMultiple = resource.settings.allowMultiple !== undefined
50
62
  && resource.settings.allowMultiple !== false;
63
+ // Here we add '*' if the resource is sensitive but no sensitive parameters are found. This works because the import
64
+ // sensitivity check only checks for the existance of a sensitive parameter whereas the parameter blocking one blocks
65
+ // on a specific sensitive parameter.
66
+ const sensitiveParameters = Object.entries(resource.settings.parameterSettings ?? {})
67
+ .filter(([, v]) => v?.isSensitive)
68
+ .map(([k]) => k);
69
+ if (resource.settings.isSensitive && sensitiveParameters.length === 0) {
70
+ sensitiveParameters.push('*');
71
+ }
51
72
  return {
52
73
  plugin: this.name,
53
74
  type: data.type,
@@ -60,6 +81,7 @@ export class Plugin {
60
81
  import: {
61
82
  requiredParameters: requiredPropertyNames,
62
83
  },
84
+ sensitiveParameters,
63
85
  allowMultiple
64
86
  };
65
87
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { defaultExclude, defineConfig } from 'vitest/config';
2
+ export default defineConfig({
3
+ test: {
4
+ pool: 'forks',
5
+ fileParallelism: false,
6
+ exclude: [
7
+ ...defaultExclude,
8
+ './src/utils/test-utils.test.ts',
9
+ ]
10
+ },
11
+ });
@@ -18,6 +18,11 @@ export interface ResourceSettings<T extends StringIndexedObject> {
18
18
  * Schema to validate user configs with. Must be in the format JSON Schema draft07
19
19
  */
20
20
  schema?: Partial<JSONSchemaType<T | any>>;
21
+ /**
22
+ * Mark the resource as sensitive. Defaults to false. This prevents the resource from automatically being imported by init and import.
23
+ * This differs from the parameter level sensitivity which also prevents the parameter value from being displayed in the plan.
24
+ */
25
+ isSensitive?: boolean;
21
26
  /**
22
27
  * Allow multiple of the same resource to unique. Set truthy if
23
28
  * multiples are allowed, for example for applications, there can be multiple copy of the same application installed
@@ -167,6 +172,12 @@ export interface DefaultParameterSetting {
167
172
  * is mainly used to determine the equality method when performing diffing.
168
173
  */
169
174
  type?: ParameterSettingType;
175
+ /**
176
+ * Mark the field as sensitive. Defaults to false. This has two side effects:
177
+ * 1. When displaying this field in the plan, it will be replaced with asterisks
178
+ * 2. When importing, resources with sensitive fields will be skipped unless the user explicitly allows it.
179
+ */
180
+ isSensitive?: boolean;
170
181
  /**
171
182
  * Default value for the parameter. If a value is not provided in the config, then this value will be used.
172
183
  */