Skip to content

Commit 7a58844

Browse files
committed
fix: Fixed various bugs:
1. Apply order fix (re-order by eval order since parallel plans mess up the order) 2. Added additional tests to validate the new logic. 3. Type fixes 4. Refactoring 5. Plan integration tests
1 parent 558f683 commit 7a58844

File tree

13 files changed

+335
-45
lines changed

13 files changed

+335
-45
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ USAGE
4545
FLAGS
4646
-o, --output=<option> [default: default]
4747
<options: plain|default|debug|json>
48-
-p, --path=<value> path to project
48+
-p, --path=<value> Path to codify.json file
4949
-s, --secure
5050
--debug
5151
@@ -70,11 +70,12 @@ Destroy or uninstall a resource (or many resources).
7070

7171
```
7272
USAGE
73-
$ codify destroy [--json] [--debug] [-o plain|default|debug|json] [-s]
73+
$ codify destroy [--json] [--debug] [-o plain|default|debug|json] [-s] [-p <value>]
7474
7575
FLAGS
7676
-o, --output=<option> [default: default]
7777
<options: plain|default|debug|json>
78+
-p, --path=<value> Path to codify.json file
7879
-s, --secure
7980
--debug
8081
@@ -121,7 +122,7 @@ USAGE
121122
FLAGS
122123
-o, --output=<option> [default: default]
123124
<options: plain|default|debug|json>
124-
-p, --path=<value> path to project
125+
-p, --path=<value> Path to codify.json file
125126
-s, --secure
126127
--debug
127128
@@ -148,7 +149,7 @@ USAGE
148149
FLAGS
149150
-o, --output=<option> [default: default]
150151
<options: plain|default|debug|json>
151-
-p, --path=<value> path to project
152+
-p, --path=<value> Path to codify.json file
152153
-s, --secure
153154
--debug
154155

src/entities/plan.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { expect, describe, it } from 'vitest';
2+
import { ResourceOperation } from 'codify-schemas';
3+
4+
import { Plan, ResourcePlan } from './plan.js';
5+
import { Project } from './project.js';
6+
import { ResourceConfig } from './resource-config.js';
7+
8+
describe('Unit tests for Plan entity', () => {
9+
it('Can sort itself by eval order (useful since plans occur in parallel now and can be return in random order', () => {
10+
const project = new Project(
11+
null,
12+
[
13+
new ResourceConfig({ type: 'type1' }),
14+
new ResourceConfig({ type: 'type2', dependsOn: ['type1'] }),
15+
new ResourceConfig({ type: 'type3', dependsOn: ['type1', 'type2']}),
16+
],
17+
'./somewhere',
18+
);
19+
20+
project.resolveDependenciesAndCalculateEvalOrder()
21+
console.log(project.evaluationOrder)
22+
23+
expect(project.evaluationOrder!.length).to.eq(3);
24+
expect(project.evaluationOrder![0]).to.eq('type1');
25+
expect(project.evaluationOrder![1]).to.eq('type2');
26+
expect(project.evaluationOrder![2]).to.eq('type3');
27+
28+
// Here we simulate receiving a plan that is out of order.
29+
const plan = new Plan([
30+
new ResourcePlan({
31+
planId: '1',
32+
operation: ResourceOperation.CREATE,
33+
resourceName: undefined,
34+
resourceType: 'type3',
35+
isStateful: false,
36+
parameters: []
37+
}),
38+
new ResourcePlan({
39+
planId: '1',
40+
operation: ResourceOperation.CREATE,
41+
resourceName: undefined,
42+
resourceType: 'type1',
43+
isStateful: false,
44+
parameters: []
45+
}),
46+
new ResourcePlan({
47+
planId: '1',
48+
operation: ResourceOperation.CREATE,
49+
resourceName: undefined,
50+
resourceType: 'type2',
51+
isStateful: false,
52+
parameters: []
53+
})
54+
], project);
55+
56+
expect(plan.raw.length).to.eq(3);
57+
expect(plan.raw[0].resourceType).to.eq('type3');
58+
expect(plan.raw[1].resourceType).to.eq('type1');
59+
expect(plan.raw[2].resourceType).to.eq('type2');
60+
61+
expect(plan.resources.length).to.eq(3);
62+
expect(plan.resources[0].resourceType).to.eq('type3');
63+
expect(plan.resources[1].resourceType).to.eq('type1');
64+
expect(plan.resources[2].resourceType).to.eq('type2');
65+
66+
plan.sortByEvalOrder(project.evaluationOrder);
67+
expect(plan.raw.length).to.eq(3);
68+
expect(plan.raw[0].resourceType).to.eq('type1');
69+
expect(plan.raw[1].resourceType).to.eq('type2');
70+
expect(plan.raw[2].resourceType).to.eq('type3');
71+
72+
expect(plan.resources.length).to.eq(3);
73+
expect(plan.resources[0].resourceType).to.eq('type1');
74+
expect(plan.resources[1].resourceType).to.eq('type2');
75+
expect(plan.resources[2].resourceType).to.eq('type3');
76+
77+
})
78+
})

src/entities/plan.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ParameterOperation, PlanResponseData, ResourceConfig, ResourceOperation } from 'codify-schemas';
22

3+
import { getId } from '../utils/index.js';
34
import { Project } from './project.js';
45

56
export class Plan {
@@ -12,6 +13,33 @@ export class Plan {
1213
this.resources = resourcePlans
1314
this.project = project
1415
}
16+
17+
// This sorting is necessary with parallel plans because the order may be jumbled in the process
18+
sortByEvalOrder(evalOrder: null | string[]) {
19+
if (!evalOrder) {
20+
return;
21+
}
22+
23+
this.raw.sort((a, b) => {
24+
const aId = getId(a.resourceType, a.resourceName);
25+
const bId = getId(b.resourceType, b.resourceName);
26+
27+
const indexA = evalOrder.indexOf(aId);
28+
const indexB = evalOrder.indexOf(bId);
29+
30+
return indexA > indexB ? 1 : -1;
31+
});
32+
33+
this.resources.sort((a, b) => {
34+
const aId = getId(a.resourceType, a.resourceName);
35+
const bId = getId(b.resourceType, b.resourceName);
36+
37+
const indexA = evalOrder.indexOf(aId);
38+
const indexB = evalOrder.indexOf(bId);
39+
40+
return indexA > indexB ? 1 : -1;
41+
})
42+
}
1543

1644
getResourcePlan(id: string): ResourcePlan | null {
1745
return this.resources.find((r) => r.id === id) ?? null;

src/entities/project.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ValidateResponseData, PlanRequestData } from 'codify-schemas';
1+
import { PlanRequestData, ResourceOperation, ValidateResponseData } from 'codify-schemas';
22

33
import { PluginValidationError, PluginValidationErrorParams, TypeNotFoundError } from '../common/errors.js';
44
import { ctx } from '../events/context.js';
@@ -7,6 +7,7 @@ import { DependencyMap } from '../plugins/plugin-manager.js';
77
import { DependencyGraphResolver } from '../utils/dependency-graph-resolver.js';
88
import { groupBy } from '../utils/index.js';
99
import { ConfigBlock, ConfigType } from './config.js';
10+
import { type Plan } from './plan.js';
1011
import { ProjectConfig } from './project-config.js';
1112
import { ResourceConfig } from './resource-config.js';
1213

@@ -125,6 +126,10 @@ ${JSON.stringify(projectConfigs, null, 2)}`);
125126
this.resourceConfigs.unshift(new ResourceConfig({
126127
type: 'xcode-tools'
127128
}));
129+
130+
if (this.evaluationOrder) {
131+
this.evaluationOrder.unshift('xcode-tools');
132+
}
128133
}
129134

130135
validateTypeIds(resourceMap: Map<string, string[]>) {
@@ -135,26 +140,9 @@ ${JSON.stringify(projectConfigs, null, 2)}`);
135140
}
136141
}
137142

138-
resolveResourceDependencies(dependencyMap: DependencyMap) {
139-
const resourceMap = new Map(this.resourceConfigs.map((r) => [r.id, r] as const));
140-
141-
for (const r of this.resourceConfigs) {
142-
// User specified dependencies are hard dependencies. They must be present.
143-
r.addDependenciesFromDependsOn((id) => resourceMap.has(id));
144-
r.addDependenciesBasedOnParameters((id) => resourceMap.has(id));
145-
146-
// Plugin dependencies are soft dependencies. They only activate if the dependent resource is present.
147-
r.addDependencies(dependencyMap.get(r.type)
148-
?.filter((type) => [...resourceMap.values()].some((r) => r.type === type))
149-
?.flatMap((type) => [...resourceMap.values()].filter((r) => r.type === type).map((r) => r.id)) ?? []
150-
);
151-
152-
// Add this to ensure that the default config xcode-tools gets applied first
153-
// TODO: remove this in the future with required dependencies
154-
if (r.type !== 'xcode-tools') {
155-
r.addDependencies(['xcode-tools'])
156-
}
157-
}
143+
resolveDependenciesAndCalculateEvalOrder(dependencyMap?: DependencyMap) {
144+
this.resolveResourceDependencies(dependencyMap);
145+
this.calculateEvaluationOrder();
158146
}
159147

160148
handlePluginResourceValidationResults(results: ValidateResponseData[]) {
@@ -172,7 +160,29 @@ ${JSON.stringify(projectConfigs, null, 2)}`);
172160
}
173161
}
174162

175-
calculateEvaluationOrder() {
163+
removeNoopFromEvaluationOrder(plan: Plan) {
164+
this.evaluationOrder = this.evaluationOrder?.filter((id) =>
165+
plan.getResourcePlan(id)?.operation !== ResourceOperation.NOOP,
166+
) ?? null;
167+
}
168+
169+
private resolveResourceDependencies(dependencyMap?: DependencyMap) {
170+
const resourceMap = new Map(this.resourceConfigs.map((r) => [r.id, r] as const));
171+
172+
for (const r of this.resourceConfigs) {
173+
// User specified dependencies are hard dependencies. They must be present.
174+
r.addDependenciesFromDependsOn((id) => resourceMap.has(id));
175+
r.addDependenciesBasedOnParameters((id) => resourceMap.has(id));
176+
177+
// Plugin dependencies are soft dependencies. They only activate if the dependent resource is present.
178+
r.addDependencies(dependencyMap?.get(r.type)
179+
?.filter((type) => [...resourceMap.values()].some((r) => r.type === type))
180+
?.flatMap((type) => [...resourceMap.values()].filter((r) => r.type === type).map((r) => r.id)) ?? []
181+
);
182+
}
183+
}
184+
185+
private calculateEvaluationOrder() {
176186
const resourceOrder = DependencyGraphResolver.calculateDependencyList(
177187
this.resourceConfigs,
178188
(r) => r.id,

src/orchestrators/destroy.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { InitializeOrchestrator } from './initialize.js';
99

1010
export interface DestroyArgs {
1111
ids: string[];
12-
path: string;
12+
path?: string;
1313
secureMode?: boolean;
1414
}
1515

@@ -45,8 +45,7 @@ export class DestroyOrchestrator {
4545
await DestroyOrchestrator.validate(project, pluginManager, dependencyMap)
4646

4747
const uninstallProject = project.toDestroyProject()
48-
uninstallProject.resolveResourceDependencies(dependencyMap);
49-
uninstallProject.calculateEvaluationOrder();
48+
uninstallProject.resolveDependenciesAndCalculateEvalOrder(dependencyMap);
5049

5150
const plan = await ctx.process(ProcessName.PLAN, () =>
5251
pluginManager.getPlan(uninstallProject)

src/orchestrators/plan.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,17 @@ export class PlanOrchestrator {
2323

2424
const { dependencyMap, pluginManager, project } = await InitializeOrchestrator.run({
2525
...args,
26-
transformProject(project) {
27-
project.addXCodeToolsConfig();
28-
return project;
29-
}
3026
}, reporter);
3127

3228
await createStartupShellScriptsIfNotExists();
3329

3430
await PlanOrchestrator.validate(project, pluginManager, dependencyMap)
31+
project.resolveDependenciesAndCalculateEvalOrder(dependencyMap);
32+
project.addXCodeToolsConfig(); // We have to add xcode-tools config always since almost every resource depends on it
3533

36-
project.resolveResourceDependencies(dependencyMap);
37-
project.calculateEvaluationOrder();
38-
39-
const plan = await PlanOrchestrator.plan(project, pluginManager)
34+
const plan = await PlanOrchestrator.plan(project, pluginManager);
35+
plan.sortByEvalOrder(project.evaluationOrder);
36+
project.removeNoopFromEvaluationOrder(plan);
4037

4138
ctx.processFinished(ProcessName.PLAN)
4239

src/plugins/plugin-manager.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,18 @@ export class PluginManager {
102102
}
103103

104104
async apply(project: Project, plan: Plan): Promise<void> {
105-
for (const resourcePlan of plan) {
106-
ctx.subprocessStarted(SubProcessName.APPLYING_RESOURCE, resourcePlan.id);
105+
for (const id of project.evaluationOrder ?? []) {
106+
ctx.subprocessStarted(SubProcessName.APPLYING_RESOURCE, id);
107107

108-
const planRequest = project.getPlanRequest(resourcePlan.id)
109-
if (!planRequest) {
110-
throw new InternalError(`Could not find plan request: ${resourcePlan.id}`)
108+
const resourcePlan = plan.getResourcePlan(id);
109+
if (!resourcePlan) {
110+
throw new InternalError(`Could not find resourcePlan: ${id}`)
111111
}
112112

113-
const pluginName = this.resourceToPluginMapping.get(resourcePlan.resourceType);
113+
const { resourceType } = resourcePlan;
114+
const pluginName = this.resourceToPluginMapping.get(resourceType);
114115
if (!pluginName) {
115-
throw new InternalError(`Unable to determine plugin for apply: ${resourcePlan.resourceType}`);
116+
throw new InternalError(`Unable to determine plugin for apply: ${resourceType}`);
116117
}
117118

118119
await this.plugins.get(pluginName)!.apply(resourcePlan);

src/utils/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ export function getTypeAndNameFromId(id: string): { type: string; name: string |
1919
name: nameParts.length === 0 ? undefined : nameParts.join('.')
2020
}
2121
}
22+
23+
export function getId(type: string, name?: string): string {
24+
return name ? `${type}.${name}` : type;
25+
}

test/orchestrator/apply/apply.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ vi.mock('../../../src/plugins/plugin.js', async () => {
1414
})
1515

1616
// The apply orchestrator directly calls plan so this will test both
17-
describe('Apply and plan orchestrator tests', () => {
17+
describe('Apply orchestrator tests', () => {
1818
it('Can apply a resource (create)', async () => {
1919
const reporter = new MockReporter({
2020
validatePlan(plan: Plan) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[
2+
{ "type": "project", "plugins": { "default": "./test/orchestrator/mocks/plugin.ts" } },
3+
{ "type": "mock", "propA": "abc", "propB": 123, "directory": "/home" },
4+
{ "type": "mock", "propA": "abc", "propB": 123, "directory": "/home" },
5+
{ "type": "mock", "propA": "abc", "propB": 123, "directory": "/home" },
6+
{ "type": "mock", "propA": "abc", "propB": 123, "directory": "/home" },
7+
{ "type": "mock", "propA": "abc", "propB": 123, "directory": "/home" },
8+
{ "type": "mock", "propA": "abc", "propB": 123, "directory": "/home" }
9+
]

0 commit comments

Comments
 (0)