Skip to content

Commit f91cffa

Browse files
committed
feat: Improved the display for test. Added linux support on a macOS host.
1 parent 26cfda2 commit f91cffa

File tree

14 files changed

+274
-138
lines changed

14 files changed

+274
-138
lines changed

src/commands/test.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Args, Flags } from '@oclif/core'
22
import chalk from 'chalk';
3+
import { OS } from 'codify-schemas';
4+
import os from 'node:os';
35

46
import { BaseCommand } from '../common/base-command.js';
5-
import { ApplyOrchestrator } from '../orchestrators/apply.js';
7+
import { ctx } from '../events/context.js';
68
import { TestOrchestrator } from '../orchestrators/test.js';
79

8-
export default class Apply extends BaseCommand {
10+
export default class Test extends BaseCommand {
911
static description =
1012
`Install or update resources on the system based on a codify.jsonc file.
1113
@@ -26,6 +28,12 @@ For more information, visit: https://docs.codifycli.com/commands/apply
2628
description: 'Automatically use this password for any handlers that require elevated permissions.',
2729
char: 'S'
2830
}),
31+
'operatingSystem': Flags.string({
32+
options: ['macOS', 'linux'],
33+
optional: true,
34+
description: 'Operating system to use for the test VM. Defaults to the host operating system.',
35+
char: 'o',
36+
}),
2937
}
3038

3139
static args = {
@@ -40,20 +48,25 @@ For more information, visit: https://docs.codifycli.com/commands/apply
4048
]
4149

4250
async init(): Promise<void> {
43-
console.log('Running Codify apply...')
51+
ctx.log('Running Codify test...')
4452
return super.init();
4553
}
4654

4755
public async run(): Promise<void> {
48-
const { flags, args } = await this.parse(Apply)
56+
const { flags, args } = await this.parse(Test)
4957

5058
if (flags.path && args.pathArgs) {
5159
throw new Error('Cannot specify both --path and path argument');
5260
}
5361

62+
const hostSystem = os.platform() === 'darwin' ? OS.Darwin : OS.Linux;
63+
const osFlag = flags.operatingSystem === 'macOS' ? OS.Darwin :
64+
flags.operatingSystem === 'linux' ? OS.Linux : hostSystem;
65+
5466
await TestOrchestrator.run({
5567
path: flags.path ?? args.pathArgs,
5668
verbosityLevel: flags.debug ? 3 : 0,
69+
vmOs: osFlag,
5770
// secure: flags.secure,
5871
}, this.reporter);
5972

src/common/initialize-plugins.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface InitializeArgs {
2020
allowEmptyProject?: boolean;
2121
forceEmptyProject?: boolean;
2222
codifyConfigs?: Config[];
23+
noProgress?: boolean;
2324
}
2425

2526
export interface InitializationResult {
@@ -38,10 +39,10 @@ export class PluginInitOrchestrator {
3839
reporter
3940
);
4041

41-
ctx.subprocessStarted(SubProcessName.INITIALIZE_PLUGINS)
42+
if (!args.noProgress) ctx.subprocessStarted(SubProcessName.INITIALIZE_PLUGINS)
4243
const pluginManager = new PluginManager();
4344
const resourceDefinitions = await pluginManager.initialize(project, args.secure, args.verbosityLevel);
44-
ctx.subprocessFinished(SubProcessName.INITIALIZE_PLUGINS)
45+
if (!args.noProgress) ctx.subprocessFinished(SubProcessName.INITIALIZE_PLUGINS)
4546

4647
return { resourceDefinitions, pluginManager, project };
4748
}

src/events/context.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export enum Event {
2121
}
2222

2323
export enum ProcessName {
24+
TEST = 'test',
2425
APPLY = 'apply',
2526
PLAN = 'plan',
2627
DESTROY = 'destroy',
@@ -39,6 +40,12 @@ export enum SubProcessName {
3940
VALIDATE = 'validate',
4041
GET_REQUIRED_PARAMETERS = 'get_required_parameters',
4142
IMPORT_RESOURCE = 'import_resource',
43+
TEST_INITIALIZE_AND_VALIDATE = 'test_initialize_and_validate',
44+
TEST_CHECKING_VM_INSTALLED = 'test_checking_vm_installed',
45+
TEST_STARTING_VM = 'test_starting_vm',
46+
TEST_COPYING_OVER_CONFIGS_AND_OPENING_TERMINAL = 'test_copying_over_configs_and_opening_terminal',
47+
TEST_USER_CONTINUE_ON_VM = 'test_user_continue_on_vm',
48+
TEST_DELETING_VM = 'test_deleting_vm',
4249
}
4350

4451
export const ctx = new class {

src/orchestrators/apply.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ export interface ApplyArgs {
77
path?: string;
88
secure?: boolean;
99
verbosityLevel?: number;
10+
noProgress?: boolean;
1011
}
1112

1213
export const ApplyOrchestrator = {
1314
async run(args: ApplyArgs, reporter: Reporter): Promise<void> {
14-
1515
const planResult = await PlanOrchestrator.run(args, reporter);
1616

1717
// Short circuit and exit if every change is NOOP
@@ -28,9 +28,9 @@ export const ApplyOrchestrator = {
2828
const { plan, pluginManager, project } = planResult;
2929
const filteredPlan = plan.filterNoopResources()
3030

31-
ctx.processStarted(ProcessName.APPLY);
31+
if (!args.noProgress) ctx.processStarted(ProcessName.APPLY);
3232
await pluginManager.apply(project, filteredPlan);
33-
ctx.processFinished(ProcessName.APPLY);
33+
if (!args.noProgress) ctx.processFinished(ProcessName.APPLY);
3434

3535
reporter.displayMessage(`
3636
🎉 Finished applying 🎉

src/orchestrators/plan.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface PlanArgs {
1414
secureMode?: boolean;
1515
verbosityLevel?: number;
1616
codifyConfigs?: Config[];
17+
noProgress?: boolean;
1718
}
1819

1920
export interface PlanOrchestratorResponse {
@@ -24,7 +25,7 @@ export interface PlanOrchestratorResponse {
2425

2526
export class PlanOrchestrator {
2627
static async run(args: PlanArgs, reporter: Reporter): Promise<PlanOrchestratorResponse> {
27-
ctx.processStarted(ProcessName.PLAN);
28+
if (!args.noProgress) ctx.processStarted(ProcessName.PLAN);
2829

2930
const initializationResult = await PluginInitOrchestrator.run({
3031
...args,
@@ -33,16 +34,17 @@ export class PlanOrchestrator {
3334

3435
await createStartupShellScriptsIfNotExists();
3536

36-
await ValidateOrchestrator.run({ existing: initializationResult }, reporter);
37+
await ValidateOrchestrator.run({ existing: initializationResult, noProgress: args.noProgress }, reporter);
3738
project.resolveDependenciesAndCalculateEvalOrder(resourceDefinitions);
3839
project.addXCodeToolsConfig(); // We have to add xcode-tools config always since almost every resource depends on it
3940

40-
const plan = await PlanOrchestrator.plan(project, pluginManager);
41+
const plan = await PlanOrchestrator.plan(project, pluginManager, args.noProgress);
4142
plan.sortByEvalOrder(project.evaluationOrder);
4243
project.removeNoopFromEvaluationOrder(plan);
4344

44-
ctx.processFinished(ProcessName.PLAN)
45+
if (!args.noProgress) ctx.processFinished(ProcessName.PLAN)
4546

47+
await reporter.hide();
4648
reporter.displayPlan(plan);
4749

4850
return {
@@ -52,10 +54,10 @@ export class PlanOrchestrator {
5254
};
5355
}
5456

55-
private static async plan(project: Project, pluginManager: PluginManager): Promise<Plan> {
56-
ctx.subprocessStarted(SubProcessName.GENERATE_PLAN)
57+
private static async plan(project: Project, pluginManager: PluginManager, silent?: boolean): Promise<Plan> {
58+
if (!silent) ctx.subprocessStarted(SubProcessName.GENERATE_PLAN)
5759
const plan = await pluginManager.plan(project);
58-
ctx.subprocessFinished(SubProcessName.GENERATE_PLAN)
60+
if (!silent) ctx.subprocessFinished(SubProcessName.GENERATE_PLAN)
5961

6062
return plan;
6163
}

src/orchestrators/test.ts

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,73 @@
11

2-
import { SpawnStatus } from 'codify-schemas';
2+
import { OS, SpawnStatus } from 'codify-schemas';
3+
import os from 'node:os';
34

45
import { PluginInitOrchestrator } from '../common/initialize-plugins.js';
5-
import { ProcessName, ctx } from '../events/context.js';
6+
import { ProcessName, ctx, SubProcessName } from '../events/context.js';
67
import { Reporter } from '../ui/reporters/reporter.js';
8+
import { StubReporter } from '../ui/reporters/stub-reporter.js';
79
import { sleep } from '../utils/index.js';
810
import { spawn, spawnSafe } from '../utils/spawn.js';
9-
import { PlanOrchestrator } from './plan.js';
11+
import { PlanOrchestrator, PlanOrchestratorResponse } from './plan.js';
1012
import { ValidateOrchestrator } from './validate.js';
1113

1214
export interface TestArgs {
1315
path?: string;
1416
secure?: boolean;
1517
verbosityLevel?: number;
18+
vmOs: OS;
1619
}
1720

1821
export const TestOrchestrator = {
1922
async run(args: TestArgs, reporter: Reporter): Promise<void> {
20-
21-
// Perform validation initially to ensure the project is valid
22-
const initializationResult = await PluginInitOrchestrator.run(args, reporter);
23-
await ValidateOrchestrator.run({ existing: initializationResult }, reporter);
24-
25-
const planResult = await PlanOrchestrator.run({
26-
codifyConfigs: [{
27-
type: 'project',
28-
plugins: { default: '/Users/kevinwang/Projects/codify-homebrew-plugin/src/index.ts' }
29-
}, {
30-
type: 'homebrew',
31-
formulae: ['sshpass']
32-
}, {
33-
type: 'tart',
34-
clone: [{ sourceName: 'ghcr.io/cirruslabs/macos-tahoe-base:latest', name: 'codify-test-vm' }],
35-
}],
36-
}, reporter);
23+
ctx.processStarted(ProcessName.TEST);
24+
reporter.silent = true;
3725

38-
// Short circuit and exit if every change is NOOP
39-
if (!planResult.plan.isEmpty()) {
40-
const confirm = await reporter.promptConfirmation('The following resources will need to be installed (Tart VM - 25gb). Do you want to continue?')
41-
if (!confirm) {
42-
return process.exit(0);
43-
}
44-
45-
const { plan, pluginManager, project } = planResult;
46-
const filteredPlan = plan.filterNoopResources()
26+
ctx.subprocessStarted(SubProcessName.TEST_INITIALIZE_AND_VALIDATE);
27+
// Perform validation initially to ensure the project is valid
28+
const initializationResult = await PluginInitOrchestrator.run({ ...args, noProgress: true }, new StubReporter());
29+
await ValidateOrchestrator.run({ existing: initializationResult, noProgress: true }, new StubReporter());
30+
ctx.subprocessFinished(SubProcessName.TEST_INITIALIZE_AND_VALIDATE);
4731

48-
ctx.processStarted(ProcessName.APPLY);
49-
await pluginManager.apply(project, filteredPlan);
50-
ctx.processFinished(ProcessName.APPLY);
51-
}
32+
await this.ensureVmIsInstalled(reporter, args.vmOs);
5233

34+
ctx.subprocessStarted(SubProcessName.TEST_STARTING_VM);
35+
const baseVmName = args.vmOs === OS.Darwin ? 'codify-test-vm-macos' : 'codify-test-vm-linux';
5336
const vmName = this.generateVmName();
54-
await spawnSafe(`tart clone codify-test-vm ${vmName}`, { interactive: true });
37+
await spawnSafe(`tart clone ${baseVmName} ${vmName}`, { interactive: true });
5538

5639
// Run this in the background. The user will have to manually exit the GUI to stop the test.
5740
spawnSafe(`tart run ${vmName}`, { interactive: true })
5841
.finally(() => {
42+
ctx.subprocessFinished(SubProcessName.TEST_USER_CONTINUE_ON_VM);
43+
ctx.subprocessStarted(SubProcessName.TEST_DELETING_VM);
44+
45+
spawnSafe(`tart delete ${vmName}`, { interactive: true })
46+
ctx.subprocessFinished(SubProcessName.TEST_DELETING_VM);
47+
48+
ctx.processFinished(ProcessName.TEST);
5949
console.log('VM has been killed... exiting.')
6050
process.exit(1);
6151
})
6252
await sleep(10_000);
6353
await this.waitUntilVmIsReady(vmName);
6454

55+
ctx.subprocessFinished(SubProcessName.TEST_STARTING_VM);
56+
57+
ctx.subprocessStarted(SubProcessName.TEST_COPYING_OVER_CONFIGS_AND_OPENING_TERMINAL);
6558
// Install codify on the VM
6659
// await spawn(`tart exec ${vmName} /bin/bash -c "$(curl -fsSL https://releases.codifycli.com/install.sh)"`, { interactive: true });
6760
const { data: ip } = await spawnSafe(`tart ip ${vmName}`, { interactive: true });
6861
await spawn(`sshpass -p "admin" scp -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${initializationResult.project.codifyFiles[0]} admin@${ip}:~/codify.jsonc`, { interactive: true });
69-
// await spawn(`tart exec ${vmName} codify apply`, undefined, { interactive: true });
7062

71-
await spawn(`tart exec ${vmName} osascript -e "tell application \\"Terminal\\" to do script \\"cd ~/ && codify apply\\""`)
63+
if (args.vmOs === OS.Darwin) {
64+
await spawn(`tart exec ${vmName} osascript -e "tell application \\"Terminal\\" to do script \\"cd ~/ && codify apply\\""`, { interactive: true });
65+
} else {
66+
await spawn(`tart exec ${vmName} gnome-terminal -- bash -c "cd ~/ && codify apply"`, { interactive: true });
67+
}
68+
69+
ctx.subprocessFinished(SubProcessName.TEST_COPYING_OVER_CONFIGS_AND_OPENING_TERMINAL);
70+
ctx.subprocessStarted(SubProcessName.TEST_USER_CONTINUE_ON_VM);
7271

7372
await sleep(1_000_000_000);
7473
},
@@ -77,6 +76,57 @@ export const TestOrchestrator = {
7776
return `codify-test-vm-${Date.now()}`;
7877
},
7978

79+
async ensureVmIsInstalled(reporter: Reporter, vmOs: OS): Promise<void> {
80+
if (vmOs === OS.Windows) {
81+
throw new Error('VM installation not supported on Windows');
82+
}
83+
84+
const hostSystem = os.platform() === 'darwin' ? OS.Darwin : OS.Linux;
85+
86+
let planResult: PlanOrchestratorResponse;
87+
if (hostSystem === OS.Darwin) {
88+
const vmImage = vmOs === OS.Darwin ? 'ghcr.io/cirruslabs/macos-tahoe-base:latest' : 'ghcr.io/cirruslabs/ubuntu-runner-arm64:latest';
89+
const vmName = vmOs === OS.Darwin ? 'codify-test-vm-macos' : 'codify-test-vm-linux';
90+
91+
ctx.subprocessStarted(SubProcessName.TEST_CHECKING_VM_INSTALLED);
92+
planResult = await PlanOrchestrator.run({
93+
codifyConfigs: [{
94+
type: 'project',
95+
plugins: { default: '/Users/kevinwang/Projects/codify-homebrew-plugin/src/index.ts' }
96+
}, {
97+
type: 'homebrew',
98+
formulae: ['sshpass']
99+
}, {
100+
type: 'tart',
101+
clone: [{ sourceName: vmImage, name: vmName }],
102+
}],
103+
noProgress: true,
104+
verbosityLevel: -1,
105+
}, new StubReporter());
106+
107+
reporter.silent = false;
108+
ctx.subprocessFinished(SubProcessName.TEST_CHECKING_VM_INSTALLED);
109+
} else {
110+
throw new Error('VM installation not supported on Linux just yet');
111+
}
112+
113+
// Short circuit and exit if every change is NOOP
114+
if (!planResult.plan.isEmpty()) {
115+
reporter.displayPlan(planResult.plan);
116+
const confirm = await reporter.promptConfirmation('The following resources will need to be installed (Tart VM - 25gb). Do you want to continue?')
117+
if (!confirm) {
118+
return process.exit(0);
119+
}
120+
121+
const { plan, pluginManager, project } = planResult;
122+
await pluginManager.setVerbosityLevel(3);
123+
const filteredPlan = plan.filterNoopResources()
124+
125+
await reporter.displayProgress();
126+
await pluginManager.apply(project, filteredPlan);
127+
}
128+
},
129+
80130
async waitUntilVmIsReady(vmName: string): Promise<void> {
81131
while (true) {
82132
const result = await spawnSafe(`tart exec ${vmName} pwd`, { interactive: true, timeout: 5000 })

src/orchestrators/validate.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface ValidateArgs {
66
existing?: InitializationResult;
77
path?: string;
88
verbosityLevel?: number;
9+
noProgress?: boolean;
910
}
1011

1112
export const ValidateOrchestrator = {
@@ -20,20 +21,24 @@ export const ValidateOrchestrator = {
2021
pluginManager,
2122
} = args.existing ?? await PluginInitOrchestrator.run(args, reporter)
2223

23-
if (args.existing) {
24-
ctx.subprocessStarted(SubProcessName.VALIDATE)
25-
} else {
26-
ctx.processStarted(SubProcessName.VALIDATE)
24+
if (!args.noProgress) {
25+
if (args.existing) {
26+
ctx.subprocessStarted(SubProcessName.VALIDATE)
27+
} else {
28+
if (!args.noProgress) ctx.processStarted(SubProcessName.VALIDATE)
29+
}
2730
}
2831

2932
project.validateTypeIds(resourceDefinitions);
3033
const validationResults = await pluginManager.validate(project);
3134
project.handlePluginResourceValidationResults(validationResults);
3235

33-
if (args.existing) {
34-
ctx.subprocessFinished(SubProcessName.VALIDATE)
35-
} else {
36-
ctx.processFinished(SubProcessName.VALIDATE)
36+
if (!args.noProgress) {
37+
if (args.existing) {
38+
ctx.subprocessFinished(SubProcessName.VALIDATE)
39+
} else {
40+
ctx.processFinished(SubProcessName.VALIDATE)
41+
}
3742
}
3843
},
3944
};

src/plugins/plugin-manager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ export class PluginManager {
152152
}
153153
}
154154

155+
async setVerbosityLevel(verbosityLevel: number): Promise<void> {
156+
for (const plugin of this.plugins.values()) {
157+
await plugin.setVerbosityLevel(verbosityLevel);
158+
}
159+
}
160+
155161
private async resolvePlugins(project: Project | null): Promise<Plugin[]> {
156162
const pluginDefinitions: Record<string, string> = {
157163
...DEFAULT_PLUGINS,

0 commit comments

Comments
 (0)