Skip to content

Commit 693ea4f

Browse files
feat!: add import command (#40)
* Add command + parsing + validation * Add import step and generate imported configs * Added check to prevent complicated resources from being imported (resources with complex required statements) * Added rendering for question prompts to default renderer * Changed uninstallation text to destroy. Switched to the new getResourceInfo API. Improved import copy. Improved import error messages BREAKING CHANGE: Changed `uninstall` command to `destroy`
1 parent 3faa098 commit 693ea4f

File tree

19 files changed

+590
-47
lines changed

19 files changed

+590
-47
lines changed

.eslintrc.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@
99
"warn",
1010
"always"
1111
],
12-
"perfectionist/sort-classes": [
13-
"off"
14-
],
12+
"perfectionist/sort-classes": "off",
13+
"perfectionist/sort-interfaces": "off",
14+
"perfectionist/sort-enums": "off",
15+
"perfectionist/sort-objects": "off",
16+
"perfectionist/sort-object-types": "off",
17+
"unicorn/no-array-reduce": "off",
18+
"unicorn/no-array-for-each": "off",
19+
"unicorn/prefer-object-from-entries": "off",
20+
"unicorn/prefer-type-error": "off",
21+
"no-await-in-loop": "off",
1522
"quotes": [
1623
"error",
1724
"single"

package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@
1111
"ajv": "^8.12.0",
1212
"ajv-formats": "^3.0.1",
1313
"chalk": "^5.3.0",
14-
"codify-schemas": "1.0.44",
14+
"codify-schemas": "1.0.52",
1515
"debug": "^4.3.4",
1616
"ink": "^4.4.1",
17+
"ink-form": "^2.0.1",
18+
"js-yaml": "^4.1.0",
19+
"js-yaml-source-map": "^0.2.2",
20+
"json-source-map": "^0.6.1",
1721
"parse-json": "^8.1.0",
1822
"react": "^18.3.1",
1923
"semver": "^7.5.4",
20-
"supports-color": "^9.4.0",
21-
"json-source-map": "^0.6.1",
22-
"js-yaml": "^4.1.0",
23-
"js-yaml-source-map": "^0.2.2"
24+
"supports-color": "^9.4.0"
2425
},
2526
"description": "Codify is a set up as code tool for developers",
2627
"devDependencies": {
@@ -30,12 +31,12 @@
3031
"@types/chai-as-promised": "^7.1.7",
3132
"@types/chalk": "^2.2.0",
3233
"@types/debug": "^4.1.12",
34+
"@types/js-yaml": "^4.0.9",
3335
"@types/mocha": "^9.0.0",
3436
"@types/mock-fs": "^4.13.3",
3537
"@types/node": "^18",
3638
"@types/react": "^18.3.1",
3739
"@types/semver": "^7.5.4",
38-
"@types/js-yaml": "^4.0.9",
3940
"@types/strip-ansi": "^5.2.1",
4041
"chai": "^4",
4142
"chai-as-promised": "^7.1.1",
Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,32 @@ import path from 'node:path';
22

33
import { BaseCommand } from '../common/base-command.js';
44
import { ApplyOrchestrator } from '../orchestrators/apply.js';
5-
import { UninstallOrchestrator } from '../orchestrators/uninstall.js';
6-
7-
export default class Uninstall extends BaseCommand {
8-
static description = 'Uninstall a given resource based on id.'
5+
import { DestroyOrchestrator } from '../orchestrators/destroy.js';
96

7+
export default class Destroy extends BaseCommand {
8+
static strict = false;
9+
static description = 'Destroy or uninstall a resource (or many resources).'
1010
static examples = [
11-
'<%= config.bin %> <%= command.id %>',
11+
'<%= config.bin %> <%= command.id %> homebrew nvm',
1212
]
13-
14-
static strict = false;
15-
13+
1614
public async run(): Promise<void> {
17-
const { flags, raw } = await this.parse(Uninstall)
15+
const { flags, raw } = await this.parse(Destroy)
1816

1917
const args = raw
2018
.filter((r) => r.type === 'arg')
2119
.map((r) => r.input);
2220

2321
if (args.length === 0) {
24-
throw new Error('A resource id must be specified for uninstall. Ex: "codify uninstall homebrew"')
22+
throw new Error('At least one resource <type> must be specified. Ex: "codify destroy homebrew"')
2523
}
2624

2725
if (flags.path) {
2826
this.log(`Applying Codify from: ${flags.path}`);
2927
}
3028

3129
const resolvedPath = path.resolve(flags.path ?? '.');
32-
const planResult = await UninstallOrchestrator.getUninstallPlan(args, resolvedPath, flags.secure);
30+
const planResult = await DestroyOrchestrator.getDestroyPlan(args, resolvedPath, flags.secure);
3331

3432
this.reporter.displayPlan(planResult.plan);
3533

src/commands/import.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Flags } from '@oclif/core';
2+
import path from 'node:path';
3+
4+
import { BaseCommand } from '../common/base-command.js';
5+
import { ImportOrchestrator } from '../orchestrators/import.js';
6+
7+
export default class Import extends BaseCommand {
8+
static strict = false;
9+
static override description = 'Generate codify configs from existing installations'
10+
static override examples = [
11+
'<%= config.bin %> <%= command.id %> homebrew nvm',
12+
]
13+
14+
static flags = {
15+
// flag with a value (-p, --path=VALUE)
16+
path: Flags.string({ char: 'p', description: 'path to project' }),
17+
}
18+
19+
20+
public async run(): Promise<void> {
21+
const { raw, flags } = await this.parse(Import)
22+
23+
if (flags.path) {
24+
this.log(`Applying Codify from: ${flags.path}`);
25+
}
26+
27+
const resolvedPath = path.resolve(flags.path ?? '.');
28+
29+
const args = raw
30+
.filter((r) => r.type === 'arg')
31+
.map((r) => r.input);
32+
33+
if (args.length === 0) {
34+
throw new Error('At least one resource <type> must be specified. Ex: "codify import homebrew"')
35+
}
36+
37+
const { pluginManager } = await ImportOrchestrator.initializeAndValidate(args, resolvedPath, flags.secure)
38+
const requiredParameters = await ImportOrchestrator.getRequiredParameters(args, pluginManager);
39+
const userSuppliedProperties = await this.reporter.askRequiredPropertiesForImport(requiredParameters);
40+
41+
const importResult = await ImportOrchestrator.getImportedConfigs(pluginManager, args, userSuppliedProperties)
42+
this.reporter.displayImportResult(importResult);
43+
44+
process.exit(0)
45+
}
46+
}

src/events/context.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export enum Event {
2020
export enum ProcessName {
2121
APPLY = 'apply',
2222
PLAN = 'plan',
23-
UNINSTALL = 'uninstall',
23+
DESTROY = 'destroy',
24+
IMPORT = 'import',
2425
}
2526

2627
export enum SubProcessName {
@@ -29,6 +30,8 @@ export enum SubProcessName {
2930
INITIALIZE_PLUGINS = 'initialize_plugins',
3031
PARSE = 'parse',
3132
VALIDATE = 'validate',
33+
GET_REQUIRED_PARAMETERS = 'get_required_parameters',
34+
IMPORT_RESOURCE = 'import_resource',
3235
}
3336

3437
export const ctx = new class {
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,25 @@ import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js';
99
import { getTypeAndNameFromId } from '../utils/index.js';
1010
import { PlanOrchestratorResponse } from './plan.js';
1111

12-
export class UninstallOrchestrator {
13-
static async getUninstallPlan(
12+
export class DestroyOrchestrator {
13+
static async getDestroyPlan(
1414
ids: string[], path: null | string, secureMode: boolean): Promise<PlanOrchestratorResponse> {
1515
if (ids.length === 0) {
16-
throw new InternalError('getUninstallPlan called with no ids passed in');
16+
throw new InternalError('getDestroyPlan called with no ids passed in');
1717
}
1818

19-
ctx.processStarted(ProcessName.PLAN)
19+
ctx.processStarted(ProcessName.DESTROY)
2020

21-
const project = await UninstallOrchestrator.parse(path, ids)
21+
const project = await DestroyOrchestrator.parse(path, ids)
2222

2323
const { dependencyMap, pluginManager } = await CommonOrchestrator.initializePlugins(project, secureMode);
24-
await UninstallOrchestrator.validate(project, pluginManager, dependencyMap)
24+
await DestroyOrchestrator.validate(project, pluginManager, dependencyMap)
2525

2626
const uninstallProject = project.toUninstallProject()
2727
uninstallProject.resolveResourceDependencies(dependencyMap);
2828
uninstallProject.calculateEvaluationOrder();
2929

30-
const plan = await UninstallOrchestrator.plan(uninstallProject, pluginManager)
30+
const plan = await DestroyOrchestrator.plan(uninstallProject, pluginManager)
3131
return {
3232
plan,
3333
pluginManager,

src/orchestrators/import.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { ResourceConfig , ResourceConfig as SchemaResourceConfig } from 'codify-schemas';
2+
3+
import { InternalError } from '../common/errors.js';
4+
import { CommonOrchestrator } from '../common/orchestrator.js';
5+
import { Project } from '../entities/project.js';
6+
import { ProcessName, SubProcessName, ctx } from '../events/context.js';
7+
import { CodifyParser } from '../parser/index.js';
8+
import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js';
9+
10+
export type RequiredProperties = Map<string, RequiredProperty[]>;
11+
export type UserSuppliedProperties = Map<string, Record<string, unknown>>;
12+
export type ImportResult = { result: ResourceConfig[], errors: string[] }
13+
14+
export interface RequiredProperty {
15+
propertyName: string;
16+
propertyType: string;
17+
plugin: string;
18+
}
19+
20+
export class ImportOrchestrator {
21+
static async initializeAndValidate(
22+
typeIds: string[],
23+
path: string,
24+
secureMode: boolean
25+
): Promise<{
26+
project: Project;
27+
pluginManager: PluginManager;
28+
}> {
29+
if (typeIds.length === 0) {
30+
throw new InternalError('importAndGenerateConfigs called with no typeIds passed in');
31+
}
32+
33+
ctx.processStarted(ProcessName.IMPORT)
34+
35+
const project = await ImportOrchestrator.parse(path)
36+
37+
const { dependencyMap, pluginManager } = await CommonOrchestrator.initializePlugins(project, secureMode);
38+
await ImportOrchestrator.validate(typeIds, project, pluginManager, dependencyMap)
39+
40+
return { project, pluginManager };
41+
}
42+
43+
static async getRequiredParameters(
44+
typeIds: string[],
45+
pluginManager: PluginManager
46+
): Promise<RequiredProperties> {
47+
ctx.subprocessStarted(SubProcessName.GET_REQUIRED_PARAMETERS);
48+
49+
const allRequiredProperties = new Map<string, RequiredProperty[]>();
50+
for (const type of typeIds) {
51+
const resourceInfo = await pluginManager.getResourceInfo(type);
52+
53+
const { schema } = resourceInfo;
54+
if (!schema) {
55+
continue;
56+
}
57+
58+
const requiredPropertyNames = resourceInfo.import?.requiredParameters;
59+
if (!requiredPropertyNames || requiredPropertyNames.length === 0) {
60+
continue;
61+
}
62+
63+
requiredPropertyNames
64+
.forEach((name) => {
65+
if (!allRequiredProperties.has(type)) {
66+
allRequiredProperties.set(type, []);
67+
}
68+
69+
const propertyInfo = (schema.properties as any)[name];
70+
71+
allRequiredProperties.get(type)!.push({
72+
propertyName: name,
73+
propertyType: propertyInfo.type ?? null,
74+
plugin: resourceInfo.plugin
75+
})
76+
});
77+
}
78+
79+
ctx.subprocessFinished(SubProcessName.GET_REQUIRED_PARAMETERS);
80+
81+
return allRequiredProperties;
82+
}
83+
84+
static async getImportedConfigs(
85+
pluginManager: PluginManager,
86+
typeIds: string[],
87+
userSuppliedProperties: UserSuppliedProperties
88+
): Promise<ImportResult> {
89+
const importedConfigs = [];
90+
const errors = [];
91+
92+
for (const type of typeIds) {
93+
ctx.subprocessStarted(SubProcessName.IMPORT_RESOURCE, type);
94+
try {
95+
const config: SchemaResourceConfig = {
96+
type,
97+
...userSuppliedProperties.get(type),
98+
};
99+
100+
const response = await pluginManager.importResource(config);
101+
102+
if (response.result !== null && response.result.length > 0) {
103+
importedConfigs.push(...response.result);
104+
} else {
105+
errors.push(`Unable to import resource '${type}', resource not found`);
106+
}
107+
} catch (error: any) {
108+
errors.push(error.message ?? error);
109+
}
110+
111+
ctx.subprocessFinished(SubProcessName.IMPORT_RESOURCE, type);
112+
}
113+
114+
ctx.processFinished(ProcessName.IMPORT)
115+
116+
return {
117+
result: importedConfigs,
118+
errors,
119+
}
120+
}
121+
122+
private static async parse(path: string): Promise<Project> {
123+
ctx.subprocessStarted(SubProcessName.PARSE);
124+
const project = await CodifyParser.parse(path);
125+
ctx.subprocessFinished(SubProcessName.PARSE);
126+
127+
return project
128+
}
129+
130+
private static async validate(typeIds: string[], project: Project, pluginManager: PluginManager, dependencyMap: DependencyMap): Promise<void> {
131+
ctx.subprocessStarted(SubProcessName.VALIDATE)
132+
133+
project.validateTypeIds(dependencyMap);
134+
135+
const unsupportedTypeIds = typeIds.filter((type) => !dependencyMap.has(type));
136+
if (unsupportedTypeIds.length > 0) {
137+
throw new Error(`The following resources cannot be imported. No plugins found that support the following types:
138+
${JSON.stringify(unsupportedTypeIds)}`);
139+
}
140+
141+
ctx.subprocessFinished(SubProcessName.VALIDATE)
142+
}
143+
}

0 commit comments

Comments
 (0)