codify-plugin-lib 1.0.64 → 1.0.66
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.js +4 -4
- package/dist/entities/errors.d.ts +7 -0
- package/dist/entities/errors.js +9 -0
- package/dist/entities/plan-types.d.ts +1 -1
- package/dist/entities/plan.d.ts +3 -3
- package/dist/entities/plan.js +9 -3
- package/dist/entities/plugin.d.ts +2 -1
- package/dist/entities/plugin.js +10 -0
- package/dist/entities/resource-types.d.ts +1 -1
- package/dist/entities/resource.d.ts +3 -2
- package/dist/entities/resource.js +63 -27
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/messages/handlers.js +16 -2
- package/package.json +1 -1
- package/src/entities/change-set.ts +5 -4
- package/src/entities/errors.ts +17 -0
- package/src/entities/plan-types.ts +1 -1
- package/src/entities/plan.test.ts +3 -15
- package/src/entities/plan.ts +14 -6
- package/src/entities/plugin.test.ts +193 -0
- package/src/entities/plugin.ts +22 -1
- package/src/entities/resource-parameters.test.ts +14 -26
- package/src/entities/resource-stateful-mode.test.ts +247 -0
- package/src/entities/resource-types.ts +1 -1
- package/src/entities/resource.test.ts +12 -24
- package/src/entities/resource.ts +98 -41
- package/src/entities/stateful-parameter.test.ts +2 -5
- package/src/index.ts +0 -1
- package/src/messages/handlers.ts +17 -2
- package/src/utils/test-utils.test.ts +0 -52
- package/src/utils/test-utils.ts +0 -20
|
@@ -59,8 +59,8 @@ export class ChangeSet {
|
|
|
59
59
|
}
|
|
60
60
|
static calculateStatefulModeChangeSet(desired, current, parameterOptions) {
|
|
61
61
|
const parameterChangeSet = new Array();
|
|
62
|
-
const _desired = {
|
|
63
|
-
const _current = {
|
|
62
|
+
const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
|
|
63
|
+
const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
|
|
64
64
|
this.addDefaultValues(_desired, parameterOptions);
|
|
65
65
|
for (const [k, v] of Object.entries(_current)) {
|
|
66
66
|
if (_desired[k] == null) {
|
|
@@ -108,8 +108,8 @@ export class ChangeSet {
|
|
|
108
108
|
}
|
|
109
109
|
static calculateStatelessModeChangeSet(desired, current, parameterOptions) {
|
|
110
110
|
const parameterChangeSet = new Array();
|
|
111
|
-
const _desired = {
|
|
112
|
-
const _current = {
|
|
111
|
+
const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
|
|
112
|
+
const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
|
|
113
113
|
this.addDefaultValues(_desired, parameterOptions);
|
|
114
114
|
for (const [k, v] of Object.entries(_desired)) {
|
|
115
115
|
if (_current[k] == null) {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { Plan } from './plan.js';
|
|
2
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
1
3
|
export declare class SudoError extends Error {
|
|
2
4
|
command: string;
|
|
3
5
|
constructor(command: string);
|
|
4
6
|
}
|
|
7
|
+
export declare class ApplyValidationError<T extends StringIndexedObject> extends Error {
|
|
8
|
+
desiredPlan: Plan<T>;
|
|
9
|
+
validatedPlan: Plan<T>;
|
|
10
|
+
constructor(desiredPlan: Plan<T>, validatedPlan: Plan<T>);
|
|
11
|
+
}
|
package/dist/entities/errors.js
CHANGED
|
@@ -5,3 +5,12 @@ export class SudoError extends Error {
|
|
|
5
5
|
this.command = command;
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
|
+
export class ApplyValidationError extends Error {
|
|
9
|
+
desiredPlan;
|
|
10
|
+
validatedPlan;
|
|
11
|
+
constructor(desiredPlan, validatedPlan) {
|
|
12
|
+
super();
|
|
13
|
+
this.desiredPlan = desiredPlan;
|
|
14
|
+
this.validatedPlan = validatedPlan;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/entities/plan.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
8
8
|
constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
|
|
9
9
|
static create<T extends StringIndexedObject>(desiredParameters: Partial<T> | null, currentParameters: Partial<T> | null, resourceMetadata: ResourceConfig, options: PlanOptions<T>): Plan<T>;
|
|
10
10
|
getResourceType(): string;
|
|
11
|
-
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues
|
|
12
|
-
get desiredConfig(): T;
|
|
13
|
-
get currentConfig(): T;
|
|
11
|
+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T>;
|
|
12
|
+
get desiredConfig(): T | null;
|
|
13
|
+
get currentConfig(): T | null;
|
|
14
14
|
toResponse(): PlanResponseData;
|
|
15
15
|
}
|
package/dist/entities/plan.js
CHANGED
|
@@ -31,8 +31,8 @@ export class Plan {
|
|
|
31
31
|
if (statefulParameterNames.has(curr.name)) {
|
|
32
32
|
newOperation = ResourceOperation.MODIFY;
|
|
33
33
|
}
|
|
34
|
-
else if (parameterOptions[curr.name]?.
|
|
35
|
-
newOperation = parameterOptions[curr.name].
|
|
34
|
+
else if (parameterOptions[curr.name]?.modifyOnChange) {
|
|
35
|
+
newOperation = parameterOptions[curr.name].modifyOnChange ? ResourceOperation.MODIFY : ResourceOperation.RECREATE;
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
38
38
|
newOperation = ResourceOperation.RECREATE;
|
|
@@ -55,7 +55,7 @@ export class Plan {
|
|
|
55
55
|
name: data.resourceName,
|
|
56
56
|
});
|
|
57
57
|
function addDefaultValues() {
|
|
58
|
-
Object.entries(defaultValues)
|
|
58
|
+
Object.entries(defaultValues ?? {})
|
|
59
59
|
.forEach(([key, defaultValue]) => {
|
|
60
60
|
const configValueExists = data
|
|
61
61
|
.parameters
|
|
@@ -98,12 +98,18 @@ export class Plan {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
get desiredConfig() {
|
|
101
|
+
if (this.changeSet.operation === ResourceOperation.DESTROY) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
101
104
|
return {
|
|
102
105
|
...this.resourceMetadata,
|
|
103
106
|
...this.changeSet.desiredParameters,
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
109
|
get currentConfig() {
|
|
110
|
+
if (this.changeSet.operation === ResourceOperation.CREATE) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
107
113
|
return {
|
|
108
114
|
...this.resourceMetadata,
|
|
109
115
|
...this.changeSet.currentParameters,
|
|
@@ -4,7 +4,8 @@ import { Plan } from './plan.js';
|
|
|
4
4
|
export declare class Plugin {
|
|
5
5
|
name: string;
|
|
6
6
|
resources: Map<string, Resource<ResourceConfig>>;
|
|
7
|
-
planStorage: Map<string, Plan<
|
|
7
|
+
planStorage: Map<string, Plan<any>>;
|
|
8
|
+
static create(name: string, resources: Resource<any>[]): Plugin;
|
|
8
9
|
constructor(name: string, resources: Map<string, Resource<ResourceConfig>>);
|
|
9
10
|
initialize(): Promise<InitializeResponseData>;
|
|
10
11
|
validate(data: ValidateRequestData): Promise<ValidateResponseData>;
|
package/dist/entities/plugin.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { ResourceOperation } from 'codify-schemas';
|
|
1
2
|
import { Plan } from './plan.js';
|
|
2
3
|
import { splitUserConfig } from '../utils/utils.js';
|
|
4
|
+
import { ApplyValidationError } from './errors.js';
|
|
3
5
|
export class Plugin {
|
|
4
6
|
name;
|
|
5
7
|
resources;
|
|
6
8
|
planStorage;
|
|
9
|
+
static create(name, resources) {
|
|
10
|
+
const resourceMap = new Map(resources.map((r) => [r.typeId, r]));
|
|
11
|
+
return new Plugin(name, resourceMap);
|
|
12
|
+
}
|
|
7
13
|
constructor(name, resources) {
|
|
8
14
|
this.name = name;
|
|
9
15
|
this.resources = resources;
|
|
@@ -58,6 +64,10 @@ export class Plugin {
|
|
|
58
64
|
throw new Error('Malformed plan with resource that cannot be found');
|
|
59
65
|
}
|
|
60
66
|
await resource.apply(plan);
|
|
67
|
+
const validationPlan = await resource.plan(plan.desiredConfig, plan.currentConfig, true);
|
|
68
|
+
if (validationPlan.changeSet.operation !== ResourceOperation.NOOP) {
|
|
69
|
+
throw new ApplyValidationError(plan, validationPlan);
|
|
70
|
+
}
|
|
61
71
|
}
|
|
62
72
|
resolvePlan(data) {
|
|
63
73
|
const { planId, plan: planRequest } = data;
|
|
@@ -24,7 +24,7 @@ export declare abstract class Resource<T extends StringIndexedObject> {
|
|
|
24
24
|
protected constructor(options: ResourceOptions<T>);
|
|
25
25
|
onInitialize(): Promise<void>;
|
|
26
26
|
validateResource(parameters: unknown): Promise<ValidationResult>;
|
|
27
|
-
plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>>;
|
|
27
|
+
plan(desiredConfig: Partial<T> & ResourceConfig | null, currentConfig?: Partial<T> & ResourceConfig | null, statefulMode?: boolean): Promise<Plan<T>>;
|
|
28
28
|
apply(plan: Plan<T>): Promise<void>;
|
|
29
29
|
private _applyCreate;
|
|
30
30
|
private _applyModify;
|
|
@@ -32,8 +32,9 @@ export declare abstract class Resource<T extends StringIndexedObject> {
|
|
|
32
32
|
private validateRefreshResults;
|
|
33
33
|
private applyTransformParameters;
|
|
34
34
|
private addDefaultValues;
|
|
35
|
-
private
|
|
35
|
+
private refreshNonStatefulParameters;
|
|
36
36
|
private refreshStatefulParameters;
|
|
37
|
+
private validatePlanInputs;
|
|
37
38
|
validate(parameters: unknown): Promise<ValidationResult>;
|
|
38
39
|
abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
|
|
39
40
|
abstract applyCreate(plan: Plan<T>): Promise<void>;
|
|
@@ -49,21 +49,22 @@ export class Resource {
|
|
|
49
49
|
}
|
|
50
50
|
return this.validate(parameters);
|
|
51
51
|
}
|
|
52
|
-
async plan(desiredConfig) {
|
|
52
|
+
async plan(desiredConfig, currentConfig = null, statefulMode = false) {
|
|
53
|
+
this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
|
|
53
54
|
const planOptions = {
|
|
54
|
-
statefulMode
|
|
55
|
+
statefulMode,
|
|
55
56
|
parameterOptions: this.parameterOptions,
|
|
56
57
|
};
|
|
57
58
|
this.addDefaultValues(desiredConfig);
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const currentParameters = await this.
|
|
59
|
+
await this.applyTransformParameters(desiredConfig);
|
|
60
|
+
const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters);
|
|
61
|
+
const { desiredParameters, resourceMetadata, nonStatefulParameters, statefulParameters, } = parsedConfig;
|
|
62
|
+
const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
|
|
62
63
|
if (currentParameters == null) {
|
|
63
|
-
return Plan.create(
|
|
64
|
+
return Plan.create(desiredParameters, null, resourceMetadata, planOptions);
|
|
64
65
|
}
|
|
65
66
|
const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
|
|
66
|
-
return Plan.create(
|
|
67
|
+
return Plan.create(desiredParameters, { ...currentParameters, ...statefulCurrentParameters }, resourceMetadata, planOptions);
|
|
67
68
|
}
|
|
68
69
|
async apply(plan) {
|
|
69
70
|
if (plan.getResourceType() !== this.typeId) {
|
|
@@ -151,20 +152,30 @@ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
|
|
|
151
152
|
Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
|
-
async applyTransformParameters(
|
|
155
|
-
|
|
155
|
+
async applyTransformParameters(desired) {
|
|
156
|
+
if (!desired) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const transformParameters = [...this.transformParameters.entries()]
|
|
156
160
|
.sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA) - this.transformParameterOrder.get(keyB));
|
|
157
|
-
for (const [key,
|
|
158
|
-
|
|
161
|
+
for (const [key, transformParameter] of transformParameters) {
|
|
162
|
+
if (desired[key] === undefined) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const transformedValue = await transformParameter.transform(desired[key]);
|
|
159
166
|
if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
|
|
160
167
|
throw new Error(`Transform parameter ${key} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
|
|
161
168
|
}
|
|
169
|
+
delete desired[key];
|
|
162
170
|
Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
|
|
163
171
|
desired[tvKey] = tvValue;
|
|
164
172
|
});
|
|
165
173
|
}
|
|
166
174
|
}
|
|
167
175
|
addDefaultValues(desired) {
|
|
176
|
+
if (!desired) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
168
179
|
Object.entries(this.defaultValues)
|
|
169
180
|
.forEach(([key, defaultValue]) => {
|
|
170
181
|
if (defaultValue !== undefined && desired[key] === undefined) {
|
|
@@ -172,7 +183,7 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
|
172
183
|
}
|
|
173
184
|
});
|
|
174
185
|
}
|
|
175
|
-
async
|
|
186
|
+
async refreshNonStatefulParameters(resourceParameters) {
|
|
176
187
|
const entriesToRefresh = new Map(Object.entries(resourceParameters));
|
|
177
188
|
const currentParameters = await this.refresh(entriesToRefresh);
|
|
178
189
|
this.validateRefreshResults(currentParameters, entriesToRefresh);
|
|
@@ -204,6 +215,14 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
|
204
215
|
}
|
|
205
216
|
return currentParameters;
|
|
206
217
|
}
|
|
218
|
+
validatePlanInputs(desired, current, statefulMode) {
|
|
219
|
+
if (!desired && !current) {
|
|
220
|
+
throw new Error('Desired config and current config cannot both be missing');
|
|
221
|
+
}
|
|
222
|
+
if (!statefulMode && !desired) {
|
|
223
|
+
throw new Error('Desired config must be provided in non-stateful mode');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
207
226
|
async validate(parameters) {
|
|
208
227
|
return {
|
|
209
228
|
isValid: true,
|
|
@@ -214,23 +233,46 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
|
214
233
|
;
|
|
215
234
|
}
|
|
216
235
|
class ConfigParser {
|
|
217
|
-
|
|
236
|
+
desiredConfig;
|
|
237
|
+
currentConfig;
|
|
218
238
|
statefulParametersMap;
|
|
219
239
|
transformParametersMap;
|
|
220
|
-
constructor(
|
|
221
|
-
this.
|
|
240
|
+
constructor(desiredConfig, currentConfig, statefulParameters, transformParameters) {
|
|
241
|
+
this.desiredConfig = desiredConfig;
|
|
242
|
+
this.currentConfig = currentConfig;
|
|
222
243
|
this.statefulParametersMap = statefulParameters;
|
|
223
244
|
this.transformParametersMap = transformParameters;
|
|
224
245
|
}
|
|
225
246
|
get resourceMetadata() {
|
|
226
|
-
const
|
|
227
|
-
|
|
247
|
+
const desiredMetadata = this.desiredConfig ? splitUserConfig(this.desiredConfig).resourceMetadata : undefined;
|
|
248
|
+
const currentMetadata = this.currentConfig ? splitUserConfig(this.currentConfig).resourceMetadata : undefined;
|
|
249
|
+
if (!desiredMetadata && !currentMetadata) {
|
|
250
|
+
throw new Error(`Unable to parse resource metadata from ${this.desiredConfig}, ${this.currentConfig}`);
|
|
251
|
+
}
|
|
252
|
+
if (currentMetadata && desiredMetadata && (Object.keys(desiredMetadata).length !== Object.keys(currentMetadata).length
|
|
253
|
+
|| Object.entries(desiredMetadata).some(([key, value]) => currentMetadata[key] !== value))) {
|
|
254
|
+
throw new Error(`The metadata for the current config does not match the desired config.
|
|
255
|
+
Desired metadata:
|
|
256
|
+
${JSON.stringify(desiredMetadata, null, 2)}
|
|
257
|
+
|
|
258
|
+
Current metadata:
|
|
259
|
+
${JSON.stringify(currentMetadata, null, 2)}`);
|
|
260
|
+
}
|
|
261
|
+
return desiredMetadata ?? currentMetadata;
|
|
228
262
|
}
|
|
229
|
-
get
|
|
230
|
-
|
|
263
|
+
get desiredParameters() {
|
|
264
|
+
if (!this.desiredConfig) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const { parameters } = splitUserConfig(this.desiredConfig);
|
|
231
268
|
return parameters;
|
|
232
269
|
}
|
|
233
|
-
get
|
|
270
|
+
get parameters() {
|
|
271
|
+
const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
|
|
272
|
+
const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
|
|
273
|
+
return { ...(desiredParameters ?? {}), ...(currentParameters ?? {}) };
|
|
274
|
+
}
|
|
275
|
+
get nonStatefulParameters() {
|
|
234
276
|
const parameters = this.parameters;
|
|
235
277
|
return Object.fromEntries([
|
|
236
278
|
...Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))),
|
|
@@ -242,10 +284,4 @@ class ConfigParser {
|
|
|
242
284
|
...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
|
|
243
285
|
]);
|
|
244
286
|
}
|
|
245
|
-
get transformParameters() {
|
|
246
|
-
const parameters = this.parameters;
|
|
247
|
-
return Object.fromEntries([
|
|
248
|
-
...Object.entries(parameters).filter(([key]) => this.transformParametersMap.has(key)),
|
|
249
|
-
]);
|
|
250
|
-
}
|
|
251
287
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,5 @@ export * from './entities/plan.js';
|
|
|
8
8
|
export * from './entities/plan-types.js';
|
|
9
9
|
export * from './entities/stateful-parameter.js';
|
|
10
10
|
export * from './entities/errors.js';
|
|
11
|
-
export * from './utils/test-utils.js';
|
|
12
11
|
export * from './utils/utils.js';
|
|
13
12
|
export declare function runPlugin(plugin: Plugin): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,6 @@ export * from './entities/plan.js';
|
|
|
8
8
|
export * from './entities/plan-types.js';
|
|
9
9
|
export * from './entities/stateful-parameter.js';
|
|
10
10
|
export * from './entities/errors.js';
|
|
11
|
-
export * from './utils/test-utils.js';
|
|
12
11
|
export * from './utils/utils.js';
|
|
13
12
|
export async function runPlugin(plugin) {
|
|
14
13
|
const messageHandler = new MessageHandler(plugin);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import addFormats from 'ajv-formats';
|
|
2
2
|
import { ApplyRequestDataSchema, ApplyResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
|
|
3
3
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
|
-
import { SudoError } from '../entities/errors.js';
|
|
4
|
+
import { ApplyValidationError, SudoError } from '../entities/errors.js';
|
|
5
5
|
const SupportedRequests = {
|
|
6
6
|
'initialize': {
|
|
7
7
|
requestValidator: InitializeRequestDataSchema,
|
|
@@ -83,12 +83,26 @@ export class MessageHandler {
|
|
|
83
83
|
}
|
|
84
84
|
const cmd = message.cmd + '_Response';
|
|
85
85
|
if (e instanceof SudoError) {
|
|
86
|
-
process.send?.({
|
|
86
|
+
return process.send?.({
|
|
87
87
|
cmd,
|
|
88
88
|
status: MessageStatus.ERROR,
|
|
89
89
|
data: `Plugin: '${this.plugin.name}'. Forbidden usage of sudo for command '${e.command}'. Please contact the plugin developer to fix this.`,
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
|
+
if (e instanceof ApplyValidationError) {
|
|
93
|
+
return process.send?.({
|
|
94
|
+
cmd,
|
|
95
|
+
status: MessageStatus.ERROR,
|
|
96
|
+
data: `Plugin: '${this.plugin.name}'. Apply validation was not successful (additional changes are needed to match the desired plan).
|
|
97
|
+
|
|
98
|
+
Validation plan:
|
|
99
|
+
${JSON.stringify(e.validatedPlan, null, 2)},
|
|
100
|
+
|
|
101
|
+
User desired plan:
|
|
102
|
+
${JSON.stringify(e.desiredPlan, null, 2)}
|
|
103
|
+
`
|
|
104
|
+
});
|
|
105
|
+
}
|
|
92
106
|
const isDebug = process.env.DEBUG?.includes('*') ?? false;
|
|
93
107
|
process.send?.({
|
|
94
108
|
cmd,
|
package/package.json
CHANGED
|
@@ -123,8 +123,8 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
123
123
|
): ParameterChange<T>[] {
|
|
124
124
|
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
125
125
|
|
|
126
|
-
const _desired = {
|
|
127
|
-
const _current = {
|
|
126
|
+
const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
|
|
127
|
+
const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
|
|
128
128
|
|
|
129
129
|
this.addDefaultValues(_desired, parameterOptions);
|
|
130
130
|
|
|
@@ -190,8 +190,9 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
190
190
|
): ParameterChange<T>[] {
|
|
191
191
|
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
192
192
|
|
|
193
|
-
const _desired = {
|
|
194
|
-
const _current = {
|
|
193
|
+
const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
|
|
194
|
+
const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
|
|
195
|
+
|
|
195
196
|
|
|
196
197
|
this.addDefaultValues(_desired, parameterOptions);
|
|
197
198
|
|
package/src/entities/errors.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Plan } from './plan.js';
|
|
2
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
3
|
+
|
|
1
4
|
export class SudoError extends Error {
|
|
2
5
|
command: string;
|
|
3
6
|
|
|
@@ -6,3 +9,17 @@ export class SudoError extends Error {
|
|
|
6
9
|
this.command = command;
|
|
7
10
|
}
|
|
8
11
|
}
|
|
12
|
+
|
|
13
|
+
export class ApplyValidationError<T extends StringIndexedObject> extends Error {
|
|
14
|
+
desiredPlan: Plan<T>;
|
|
15
|
+
validatedPlan: Plan<T>;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
desiredPlan: Plan<T>,
|
|
19
|
+
validatedPlan: Plan<T>
|
|
20
|
+
) {
|
|
21
|
+
super();
|
|
22
|
+
this.desiredPlan = desiredPlan;
|
|
23
|
+
this.validatedPlan = validatedPlan;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -5,7 +5,7 @@ export interface ParameterOptions {
|
|
|
5
5
|
/**
|
|
6
6
|
* Chose if the resource should be re-created or modified if this parameter is changed. Defaults to false (re-creates resource on change).
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
modifyOnChange?: boolean;
|
|
9
9
|
/**
|
|
10
10
|
* Customize the equality comparison for a parameter.
|
|
11
11
|
* @param a
|
|
@@ -19,11 +19,7 @@ describe('Plan entity tests', () => {
|
|
|
19
19
|
}]
|
|
20
20
|
}, resource.defaultValues);
|
|
21
21
|
|
|
22
|
-
expect(plan.currentConfig).
|
|
23
|
-
type: 'type',
|
|
24
|
-
propA: null,
|
|
25
|
-
propB: null,
|
|
26
|
-
})
|
|
22
|
+
expect(plan.currentConfig).to.be.null;
|
|
27
23
|
|
|
28
24
|
expect(plan.desiredConfig).toMatchObject({
|
|
29
25
|
type: 'type',
|
|
@@ -56,11 +52,7 @@ describe('Plan entity tests', () => {
|
|
|
56
52
|
propB: 'propBValue',
|
|
57
53
|
})
|
|
58
54
|
|
|
59
|
-
expect(plan.desiredConfig).
|
|
60
|
-
type: 'type',
|
|
61
|
-
propA: null,
|
|
62
|
-
propB: null,
|
|
63
|
-
})
|
|
55
|
+
expect(plan.desiredConfig).to.be.null;
|
|
64
56
|
|
|
65
57
|
expect(plan.changeSet.parameterChanges
|
|
66
58
|
.every((pc) => pc.operation === ParameterOperation.REMOVE)
|
|
@@ -117,11 +109,7 @@ describe('Plan entity tests', () => {
|
|
|
117
109
|
}]
|
|
118
110
|
}, resource.defaultValues);
|
|
119
111
|
|
|
120
|
-
expect(plan.currentConfig).
|
|
121
|
-
type: 'type',
|
|
122
|
-
propA: null,
|
|
123
|
-
propB: null,
|
|
124
|
-
})
|
|
112
|
+
expect(plan.currentConfig).to.be.null
|
|
125
113
|
|
|
126
114
|
expect(plan.desiredConfig).toMatchObject({
|
|
127
115
|
type: 'type',
|
package/src/entities/plan.ts
CHANGED
|
@@ -55,8 +55,8 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
55
55
|
let newOperation: ResourceOperation;
|
|
56
56
|
if (statefulParameterNames.has(curr.name)) {
|
|
57
57
|
newOperation = ResourceOperation.MODIFY // All stateful parameters are modify only
|
|
58
|
-
} else if (parameterOptions[curr.name]?.
|
|
59
|
-
newOperation = parameterOptions[curr.name].
|
|
58
|
+
} else if (parameterOptions[curr.name]?.modifyOnChange) {
|
|
59
|
+
newOperation = parameterOptions[curr.name].modifyOnChange ? ResourceOperation.MODIFY : ResourceOperation.RECREATE;
|
|
60
60
|
} else {
|
|
61
61
|
newOperation = ResourceOperation.RECREATE; // Default to Re-create. Should handle the majority of use cases
|
|
62
62
|
}
|
|
@@ -75,7 +75,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
75
75
|
return this.resourceMetadata.type
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues
|
|
78
|
+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T> {
|
|
79
79
|
if (!data) {
|
|
80
80
|
throw new Error('Data is empty');
|
|
81
81
|
}
|
|
@@ -95,7 +95,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
95
95
|
);
|
|
96
96
|
|
|
97
97
|
function addDefaultValues(): void {
|
|
98
|
-
Object.entries(defaultValues)
|
|
98
|
+
Object.entries(defaultValues ?? {})
|
|
99
99
|
.forEach(([key, defaultValue]) => {
|
|
100
100
|
const configValueExists = data!
|
|
101
101
|
.parameters
|
|
@@ -144,14 +144,22 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
144
144
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
get desiredConfig(): T {
|
|
147
|
+
get desiredConfig(): T | null {
|
|
148
|
+
if (this.changeSet.operation === ResourceOperation.DESTROY) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
return {
|
|
149
153
|
...this.resourceMetadata,
|
|
150
154
|
...this.changeSet.desiredParameters,
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
get currentConfig(): T {
|
|
158
|
+
get currentConfig(): T | null {
|
|
159
|
+
if (this.changeSet.operation === ResourceOperation.CREATE) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
return {
|
|
156
164
|
...this.resourceMetadata,
|
|
157
165
|
...this.changeSet.currentParameters,
|