Skip to content

Commit 3ce4cd3

Browse files
[CODE-25] Stateful plans (#7)
* Add the concept of current parameter to plan function. During a plan the current + desired will be refreshed. Changed ordering to clean up plan method. * Modified plan to support a undefined desiredConfig in order to support deletes. Added validation checks and changed plan code to support a null desiredConfig. Added tests and fixed bug with change set * Deleted test utils * Changed desiredConfig and currentConfig to return null for destory and create respectively. Altered tests to support this new definition. This was done so that plan and refresh is bi-directional now. Fixed bugs with apply validation not working. Added tests for apply validation * Fixed plan types * Removed apply validation from apply method. Apply validation will now be separately done from the CLI
1 parent 9c7c765 commit 3ce4cd3

17 files changed

Lines changed: 556 additions & 236 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codify-plugin-lib",
3-
"version": "1.0.64",
3+
"version": "1.0.69",
44
"description": "",
55
"main": "dist/index.js",
66
"typings": "dist/index.d.ts",

src/entities/change-set.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ export class ChangeSet<T extends StringIndexedObject> {
123123
): ParameterChange<T>[] {
124124
const parameterChangeSet = new Array<ParameterChange<T>>();
125125

126-
const _desired = { ...desired };
127-
const _current = { ...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));
128128

129129
this.addDefaultValues(_desired, parameterOptions);
130130

@@ -190,8 +190,9 @@ export class ChangeSet<T extends StringIndexedObject> {
190190
): ParameterChange<T>[] {
191191
const parameterChangeSet = new Array<ParameterChange<T>>();
192192

193-
const _desired = { ...desired };
194-
const _current = { ...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+
195196

196197
this.addDefaultValues(_desired, parameterOptions);
197198

src/entities/errors.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { Plan } from './plan.js';
2-
import { StringIndexedObject } from 'codify-schemas';
3-
41
export class SudoError extends Error {
52
command: string;
63

@@ -9,17 +6,3 @@ export class SudoError extends Error {
96
this.command = command;
107
}
118
}
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-
}

src/entities/plan-types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { Plan } from './plan.js';
2+
import { StringIndexedObject } from 'codify-schemas';
3+
14
/**
25
* Customize properties for specific parameters. This will alter the way the library process changes to the parameter.
36
*/
@@ -24,3 +27,18 @@ export interface PlanOptions<T> {
2427
statefulMode: boolean;
2528
parameterOptions?: Record<keyof T, ParameterOptions>;
2629
}
30+
31+
export interface CreatePlan<T extends StringIndexedObject> extends Plan<T> {
32+
desiredConfig: T;
33+
currentConfig: null;
34+
}
35+
36+
export interface DestroyPlan<T extends StringIndexedObject> extends Plan<T> {
37+
desiredConfig: null;
38+
currentConfig: T;
39+
}
40+
41+
export interface ModifyPlan<T extends StringIndexedObject> extends Plan<T> {
42+
desiredConfig: T;
43+
currentConfig: T;
44+
}

src/entities/plan.test.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@ describe('Plan entity tests', () => {
1919
}]
2020
}, resource.defaultValues);
2121

22-
expect(plan.currentConfig).toMatchObject({
23-
type: 'type',
24-
propA: null,
25-
propB: null,
26-
})
22+
expect(plan.currentConfig).to.be.null;
2723

2824
expect(plan.desiredConfig).toMatchObject({
2925
type: 'type',
@@ -56,11 +52,7 @@ describe('Plan entity tests', () => {
5652
propB: 'propBValue',
5753
})
5854

59-
expect(plan.desiredConfig).toMatchObject({
60-
type: 'type',
61-
propA: null,
62-
propB: null,
63-
})
55+
expect(plan.desiredConfig).to.be.null;
6456

6557
expect(plan.changeSet.parameterChanges
6658
.every((pc) => pc.operation === ParameterOperation.REMOVE)
@@ -117,11 +109,7 @@ describe('Plan entity tests', () => {
117109
}]
118110
}, resource.defaultValues);
119111

120-
expect(plan.currentConfig).toMatchObject({
121-
type: 'type',
122-
propA: null,
123-
propB: null,
124-
})
112+
expect(plan.currentConfig).to.be.null
125113

126114
expect(plan.desiredConfig).toMatchObject({
127115
type: 'type',

src/entities/plan.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,22 @@ export class Plan<T extends StringIndexedObject> {
144144

145145
}
146146

147-
get desiredConfig(): T {
147+
get desiredConfig(): T | null {
148+
if (this.changeSet.operation === ResourceOperation.DESTROY) {
149+
return null;
150+
}
151+
148152
return {
149153
...this.resourceMetadata,
150154
...this.changeSet.desiredParameters,
151155
}
152156
}
153157

154-
get currentConfig(): T {
158+
get currentConfig(): T | null {
159+
if (this.changeSet.operation === ResourceOperation.CREATE) {
160+
return null;
161+
}
162+
155163
return {
156164
...this.resourceMetadata,
157165
...this.changeSet.currentParameters,

src/entities/plugin.test.ts

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class TestResource extends Resource<TestConfig> {
4444

4545
describe('Plugin tests', () => {
4646
it('Validates that applies were successfully applied', async () => {
47-
const resource = new class extends TestResource {
47+
const resource= new class extends TestResource {
4848
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
4949
}
5050

@@ -56,9 +56,9 @@ describe('Plugin tests', () => {
5656
}
5757
}
5858

59-
const testPlugin = Plugin.create('testPlugin', [resource])
59+
const plugin = Plugin.create('testPlugin', [resource])
6060

61-
const desiredPlan = {
61+
const plan = {
6262
operation: ResourceOperation.CREATE,
6363
resourceType: 'testResource',
6464
parameters: [
@@ -67,7 +67,7 @@ describe('Plugin tests', () => {
6767
};
6868

6969
// If this doesn't throw then it passes the test
70-
await testPlugin.apply({ plan: desiredPlan });
70+
await plugin.apply({ plan });
7171
});
7272

7373
it('Validates that applies were successfully applied (error)', async () => {
@@ -80,17 +80,114 @@ describe('Plugin tests', () => {
8080
return null;
8181
}
8282
}
83+
const plugin = Plugin.create('testPlugin', [resource])
8384

84-
const testPlugin = Plugin.create('testPlugin', [resource])
85-
86-
const desiredPlan = {
85+
const plan = {
8786
operation: ResourceOperation.CREATE,
8887
resourceType: 'testResource',
8988
parameters: [
9089
{ name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null },
9190
]
9291
};
9392

94-
await expect(async () => testPlugin.apply({ plan: desiredPlan })).rejects.toThrowError(expect.any(ApplyValidationError));
93+
await expect(async () => plugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
94+
});
95+
96+
it('Validates that deletes were successfully applied', async () => {
97+
const resource = new class extends TestResource {
98+
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
99+
}
100+
101+
// Return null to indicate that the resource was deleted
102+
async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
103+
return null;
104+
}
105+
}
106+
107+
const testPlugin = Plugin.create('testPlugin', [resource])
108+
109+
const plan = {
110+
operation: ResourceOperation.DESTROY,
111+
resourceType: 'testResource',
112+
parameters: [
113+
{ name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' },
114+
]
115+
};
116+
117+
// If this doesn't throw then it passes the test
118+
await testPlugin.apply({ plan })
119+
});
120+
121+
it('Validates that deletes were successfully applied (error)', async () => {
122+
const resource = new class extends TestResource {
123+
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
124+
}
125+
126+
// Return a value to indicate that the resource still exists
127+
async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
128+
return { propA: 'abc' };
129+
}
130+
}
131+
132+
const testPlugin = Plugin.create('testPlugin', [resource])
133+
134+
const plan = {
135+
operation: ResourceOperation.DESTROY,
136+
resourceType: 'testResource',
137+
parameters: [
138+
{ name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' },
139+
]
140+
};
141+
142+
// If this doesn't throw then it passes the test
143+
expect(async () => await testPlugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
144+
});
145+
146+
it('Validates that re-create was successfully applied', async () => {
147+
const resource = new class extends TestResource {
148+
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
149+
}
150+
151+
async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
152+
return { propA: 'def'};
153+
}
154+
}
155+
156+
const testPlugin = Plugin.create('testPlugin', [resource])
157+
158+
const plan = {
159+
operation: ResourceOperation.RECREATE,
160+
resourceType: 'testResource',
161+
parameters: [
162+
{ name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
163+
]
164+
};
165+
166+
// If this doesn't throw then it passes the test
167+
await testPlugin.apply({ plan })
168+
});
169+
170+
it('Validates that modify was successfully applied (error)', async () => {
171+
const resource = new class extends TestResource {
172+
async applyCreate(plan: Plan<TestConfig>): Promise<void> {
173+
}
174+
175+
async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
176+
return { propA: 'abc' };
177+
}
178+
}
179+
180+
const testPlugin = Plugin.create('testPlugin', [resource])
181+
182+
const plan = {
183+
operation: ResourceOperation.DESTROY,
184+
resourceType: 'testResource',
185+
parameters: [
186+
{ name: 'propA', operation: ParameterOperation.REMOVE, newValue: 'def', previousValue: 'abc' },
187+
]
188+
};
189+
190+
// If this doesn't throw then it passes the test
191+
expect(async () => await testPlugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
95192
});
96193
});

src/entities/plugin.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@ import {
55
PlanRequestData,
66
PlanResponseData,
77
ResourceConfig,
8-
ResourceOperation,
98
ValidateRequestData,
109
ValidateResponseData
1110
} from 'codify-schemas';
1211
import { Plan } from './plan.js';
1312
import { splitUserConfig } from '../utils/utils.js';
14-
import { ApplyValidationError } from './errors.js';
1513

1614
export class Plugin {
1715
planStorage: Map<string, Plan<any>>;
@@ -92,13 +90,6 @@ export class Plugin {
9290
}
9391

9492
await resource.apply(plan);
95-
96-
// Perform a validation check after to ensure that the plan was properly applied.
97-
// Sometimes no errors are returned (exit code 0) but the apply was not successful
98-
const validationPlan = await resource.plan({ ...plan.resourceMetadata, ...plan.desiredConfig });
99-
if (validationPlan.changeSet.operation !== ResourceOperation.NOOP) {
100-
throw new ApplyValidationError(plan, validationPlan);
101-
}
10293
}
10394

10495
private resolvePlan(data: ApplyRequestData): Plan<ResourceConfig> {

0 commit comments

Comments
 (0)