Skip to content

Commit e83ef41

Browse files
[CODE-100] Add dependsOn support. Add entities for plan and resource … (#28)
* [CODE-100] Add dependsOn support. Add entities for plan and resource plan. Make most operations depend on id instead of solely relying on type * [CODE-100] Fixed bugs + removed debug logging * [CODE-100] Style improvements. Reset codify.json back to original * [CODE-100] Fixed imports
1 parent 56ec1e7 commit e83ef41

18 files changed

Lines changed: 184 additions & 92 deletions

README.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ $ npm install -g codify
1818
$ codify COMMAND
1919
running command...
2020
$ codify (--version)
21-
codify/0.0.0 darwin-arm64 node-v18.20.2
21+
codify/0.0.0 darwin-arm64 node-v18.20.3
2222
$ codify --help [COMMAND]
2323
USAGE
2424
$ codify COMMAND
@@ -69,8 +69,7 @@ EXAMPLES
6969
$ codify apply
7070
```
7171

72-
_See
73-
code: [src/commands/apply/index.ts](https://github.com/kevinwang5658/codify/blob/v0.0.0/src/commands/apply/index.ts)_
72+
_See code: [src/commands/apply/index.ts](https://github.com/kevinwang5658/codify/blob/v0.0.0/src/commands/apply/index.ts)_
7473

7574
## `codify help [COMMANDS]`
7675

@@ -210,8 +209,7 @@ EXAMPLES
210209
$ codify plugins:inspect myplugin
211210
```
212211

213-
_See
214-
code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/inspect.ts)_
212+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/inspect.ts)_
215213

216214
## `codify plugins:install PLUGIN...`
217215

@@ -251,8 +249,7 @@ EXAMPLES
251249
$ codify plugins:install someuser/someplugin
252250
```
253251

254-
_See
255-
code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/install.ts)_
252+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/install.ts)_
256253

257254
## `codify plugins:link PLUGIN`
258255

@@ -330,8 +327,7 @@ ALIASES
330327
$ codify plugins remove
331328
```
332329

333-
_See
334-
code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/uninstall.ts)_
330+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/uninstall.ts)_
335331

336332
## `codify plugins:uninstall PLUGIN...`
337333

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"ajv": "^8.12.0",
1212
"ajv-formats": "^3.0.1",
1313
"chalk": "^5.3.0",
14-
"codify-schemas": "1.0.38",
14+
"codify-schemas": "1.0.39",
1515
"debug": "^4.3.4",
1616
"ink": "^4.4.1",
1717
"parse-json": "^8.1.0",

src/commands/apply/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default class Apply extends BaseCommand {
3535
this.reporter.displayPlan(planResult.plan);
3636

3737
// Short circuit and exit if every change is NOOP
38-
if (planResult.plan.every((p) => p.operation === ResourceOperation.NOOP)) {
38+
if (planResult.plan.isEmpty()) {
3939
console.log('No changes necessary. Exiting');
4040
return process.exit(0);
4141
}

src/entities/plan.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { ParameterOperation, PlanResponseData, ResourceConfig, ResourceOperation } from 'codify-schemas';
2+
3+
export class Plan {
4+
raw: PlanResponseData[];
5+
resources: ResourcePlan[];
6+
7+
constructor(resourcePlans: ResourcePlan[]) {
8+
this.raw = resourcePlans.map((r) => r.raw);
9+
this.resources = resourcePlans
10+
}
11+
12+
getResourcePlan(id: string): ResourcePlan | null {
13+
return this.resources.find((r) => r.id === id) ?? null;
14+
}
15+
16+
filterNoopResources(): Plan {
17+
return new Plan(this.resources.filter((r) => r.operation !== ResourceOperation.NOOP))
18+
}
19+
20+
// If every operation is no-op then a plan is considered empty
21+
isEmpty() {
22+
return this.raw.every((r) => r.operation === ResourceOperation.NOOP);
23+
}
24+
25+
*[Symbol.iterator](): Iterator<ResourcePlan> {
26+
for (const resource of this.resources) {
27+
yield resource;
28+
}
29+
}
30+
}
31+
32+
export class ResourcePlan {
33+
raw: PlanResponseData;
34+
planId: string;
35+
operation: ResourceOperation;
36+
resourceName?: string;
37+
resourceType: string;
38+
parameters: Array<{
39+
name: string;
40+
newValue: null | unknown;
41+
operation: ParameterOperation;
42+
previousValue: null | unknown;
43+
}>
44+
45+
constructor(json: PlanResponseData) {
46+
this.raw = json;
47+
this.planId = json.planId;
48+
this.operation = json.operation;
49+
this.resourceName = json.resourceName;
50+
this.resourceType = json.resourceType;
51+
this.parameters = json.parameters;
52+
}
53+
54+
get id(): string {
55+
return (this.resourceName) ? `${this.resourceType}.${this.resourceName}` : this.resourceType;
56+
}
57+
58+
get desiredConfig(): ResourceConfig {
59+
return this.raw.parameters.reduce((obj, parameter) => {
60+
obj[parameter.name] = parameter.newValue;
61+
return obj;
62+
}, {} as ResourceConfig);
63+
}
64+
65+
get currentConfig(): ResourceConfig {
66+
return this.raw.parameters.reduce((obj, parameter) => {
67+
obj[parameter.name] = parameter.previousValue;
68+
return obj;
69+
}, {} as ResourceConfig);
70+
}
71+
}

src/entities/project.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export class Project {
3232

3333
for (const [idx, r] of resourceConfigs.entries()) {
3434
r.name = String(idx)
35+
r.raw.name = String(idx)
3536
}
3637
}
3738
}
@@ -56,6 +57,7 @@ export class Project {
5657

5758
for (const r of this.resourceConfigs) {
5859
// User specified dependencies are hard dependencies. They must be present.
60+
r.addDependenciesFromDependsOn((id) => resourceMap.has(id));
5961
r.addDependenciesBasedOnParameters((id) => resourceMap.has(id));
6062

6163
// Plugin dependencies are soft dependencies. They only activate if the dependent resource is present.

src/entities/resource-config.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { ResourceSchema } from 'codify-schemas';
22

3+
import { RemoveMethods } from '../common/types.js';
34
import { ConfigClass } from '../parser/language-definition.js';
45
import { ajv } from '../utils/ajv.js';
5-
import { RemoveMethods } from '../common/types.js';
66
import { ConfigBlock } from './config.js';
77

88
/** Resource JSON supported format
@@ -30,17 +30,21 @@ export class ResourceConfig implements ConfigBlock {
3030
raw: Record<string, unknown>;
3131
type: string;
3232
name?: string;
33-
parameters: Record<string, unknown>;
33+
dependsOn: string[];
34+
35+
// Calculated
3436
dependencyIds: string[] = []; // id of other nodes
37+
parameters: Record<string, unknown>;
3538

3639
constructor(config: unknown) {
3740
if (this.validateConfig(config)) {
38-
const { name, type, ...parameters } = config;
41+
const { dependsOn, name, type, ...parameters } = config;
3942

4043
this.raw = config;
4144
this.type = type;
4245
this.name = name;
4346
this.parameters = parameters ?? {};
47+
this.dependsOn = dependsOn ?? []
4448

4549
return;
4650
}
@@ -57,7 +61,17 @@ export class ResourceConfig implements ConfigBlock {
5761
}
5862

5963
get id() {
60-
return this.name === null || this.name === undefined ? this.type : `${this.type}.${this.name}`;
64+
return this.name ? `${this.type}.${this.name}` : this.type;
65+
}
66+
67+
addDependenciesFromDependsOn(resourceExists: (id: string) => boolean) {
68+
for (const id of this.dependsOn) {
69+
if (!resourceExists(id)) {
70+
throw new Error(`Reference ${id} is not a valid resource`);
71+
}
72+
73+
this.dependencyIds.push(id);
74+
}
6175
}
6276

6377
addDependenciesBasedOnParameters(resourceExists: (id: string) => boolean) {
@@ -66,26 +80,24 @@ export class ResourceConfig implements ConfigBlock {
6680
.filter(([, v]) => typeof v === 'string')
6781
.filter(([, v]) => REFERENCE_REGEX.test(v as string));
6882

69-
parametersWithDependencies.forEach(([, value]) => {
83+
for (const [, value] of parametersWithDependencies) {
7084
const matchResult = [...(value as string).matchAll(REFERENCE_REGEX)];
7185

7286
if (!matchResult) {
7387
throw new Error('Internal Error: expect dependency match result to not be null');
7488
}
7589

76-
const ids = matchResult.map(([, capturedStr]) => {
77-
return capturedStr;
78-
})
90+
const ids = matchResult.map(([, capturedStr]) => capturedStr)
7991

8092
// Validate that each id exists
81-
ids.forEach((id) => {
93+
for (const id of ids) {
8294
if (!resourceExists(id)) {
8395
throw new Error(`Reference ${id} is not a valid resource`)
8496
}
85-
});
97+
}
8698

8799
this.dependencyIds.push(...ids);
88-
})
100+
}
89101
}
90102

91103
addDependencies(dependencies: string[]) {

src/orchestrators/apply.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { ResourceOperation } from 'codify-schemas';
2-
3-
import { ctx, ProcessName } from '../events/context.js';
1+
import { ProcessName, ctx } from '../events/context.js';
42
import { PlanOrchestratorResponse } from './plan.js';
53

64
export const ApplyOrchestrator = {
75
async run(planResult: PlanOrchestratorResponse): Promise<void> {
86
const { plan, pluginManager, project } = planResult;
9-
const filteredPlan = plan
10-
.filter((p) => p.operation !== ResourceOperation.NOOP)
7+
const filteredPlan = plan.filterNoopResources()
118

129
ctx.processStarted(ProcessName.APPLY);
1310
await pluginManager.apply(project, filteredPlan);

src/orchestrators/plan.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { PlanResponseData } from 'codify-schemas';
2-
31
import { CommonOrchestrator } from '../common/orchestrator.js';
2+
import { Plan } from '../entities/plan.js';
43
import { Project } from '../entities/project.js';
5-
import { ctx, ProcessName, SubProcessName } from '../events/context.js';
4+
import { ProcessName, SubProcessName, ctx } from '../events/context.js';
65
import { Parser } from '../parser/index.js';
76
import { PluginManager } from '../plugins/plugin-manager.js';
87
import { createStartupShellScriptsIfNotExists } from '../utils/file.js';
98

109
export interface PlanOrchestratorResponse {
11-
plan: PlanResponseData[],
10+
plan: Plan,
1211
pluginManager: PluginManager;
1312
project: Project;
1413
}

src/plugins/plugin-manager.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { PlanResponseData, ResourceOperation, ValidateResponseData } from 'codify-schemas';
1+
import { ResourceOperation, ValidateResponseData } from 'codify-schemas';
22

3+
import { Plan, ResourcePlan } from '../entities/plan.js';
34
import { Project } from '../entities/project.js';
45
import { ResourceConfig } from '../entities/resource-config.js';
5-
import { ctx, SubProcessName } from '../events/context.js';
6+
import { SubProcessName, ctx } from '../events/context.js';
7+
import { prettyFormatResourcePlan } from '../ui/plan-pretty-printer.js';
68
import { groupBy } from '../utils/index.js';
79
import { Plugin } from './plugin.js';
810
import { PluginResolver } from './resolver.js';
9-
import { prettyFormatPlan } from '../ui/plan-pretty-printer.js';
1011

1112
type PluginName = string;
1213
type ResourceTypeId = string;
@@ -47,8 +48,8 @@ export class PluginManager {
4748
);
4849
}
4950

50-
async getPlan(project: Project): Promise<PlanResponseData[]> {
51-
const result = new Array<PlanResponseData>();
51+
async getPlan(project: Project): Promise<Plan> {
52+
const result = new Array<ResourcePlan>();
5253
for (const config of project.evaluationOrder) {
5354
const pluginName = this.resourceToPluginMapping.get(config.type);
5455
if (!pluginName) {
@@ -60,29 +61,27 @@ export class PluginManager {
6061
result.push(planResult);
6162
}
6263

63-
return result;
64+
return new Plan(result);
6465
}
6566

66-
async apply(project: Project, planResponseData: PlanResponseData[]): Promise<void> {
67-
for (const plan of planResponseData) {
68-
const { resourceType } = plan;
67+
async apply(project: Project, plan: Plan): Promise<void> {
68+
for (const resourcePlan of plan) {
69+
ctx.subprocessStarted(SubProcessName.APPLYING_RESOURCE, resourcePlan.id);
6970

70-
ctx.subprocessStarted(SubProcessName.APPLYING_RESOURCE, resourceType);
71-
72-
const config = project.evaluationOrder.find((r) => r.type === resourceType);
71+
const config = project.evaluationOrder.find((r) => r.id === resourcePlan.id);
7372
if (!config) {
74-
throw new Error(`Could not find plan ${resourceType}`)
73+
throw new Error(`Could not find plan ${resourcePlan.id}`)
7574
}
7675

77-
const pluginName = this.resourceToPluginMapping.get(resourceType);
76+
const pluginName = this.resourceToPluginMapping.get(resourcePlan.resourceType);
7877
if (!pluginName) {
79-
throw new Error(`Internal error: unable to determine plugin for apply: ${resourceType}`);
78+
throw new Error(`Internal error: unable to determine plugin for apply: ${resourcePlan.resourceType}`);
8079
}
8180

82-
await this.plugins.get(pluginName)!.apply(plan);
81+
await this.plugins.get(pluginName)!.apply(resourcePlan);
8382
await this.validateApply(pluginName, config);
8483

85-
ctx.subprocessFinished(SubProcessName.APPLYING_RESOURCE, resourceType);
84+
ctx.subprocessFinished(SubProcessName.APPLYING_RESOURCE, resourcePlan.id);
8685
}
8786
}
8887

@@ -145,7 +144,7 @@ export class PluginManager {
145144
throw new Error(`Plugin: '${pluginName}'. Resource: '${desired.type}'. Additional changes are needed to match the desired plan.
146145
147146
Validation returned: "${validationPlan.operation}" instead of "${ResourceOperation.NOOP}". These changes are remaining.
148-
${prettyFormatPlan(validationPlan)}
147+
${prettyFormatResourcePlan(validationPlan)}
149148
`)
150149
}
151150
}

src/plugins/plugin.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ValidateResponseDataSchema
99
} from 'codify-schemas';
1010

11+
import { ResourcePlan } from '../entities/plan.js';
1112
import { ResourceConfig } from '../entities/resource-config.js';
1213
import { ajv } from '../utils/ajv.js';
1314
import { PluginProcess } from './plugin-process.js';
@@ -62,7 +63,7 @@ export class Plugin {
6263
return data;
6364
}
6465

65-
async plan(resource: ResourceConfig): Promise<PlanResponseData> {
66+
async plan(resource: ResourceConfig): Promise<ResourcePlan> {
6667
const { data, status } = await this.process!.sendMessageForResult({ cmd: 'plan', data: resource.raw });
6768

6869
if (status === MessageStatus.ERROR) {
@@ -73,7 +74,7 @@ export class Plugin {
7374
throw new Error(`Plugin error: plugin ${this.name} returned invalid plan response: ${JSON.stringify(planResponseValidator.errors, null, 2)}`)
7475
}
7576

76-
return data;
77+
return new ResourcePlan(data);
7778
}
7879

7980
async apply(plan: PlanResponseData): Promise<void> {

0 commit comments

Comments
 (0)