Select Git revision
ExerciseRunCommand.ts

michael.minelli authored
ExerciseRunCommand.ts 11.72 KiB
import CommanderCommand from '../../CommanderCommand';
import Config from '../../../config/Config';
import fs from 'node:fs';
import ora from 'ora';
import util from 'util';
import { exec } from 'child_process';
import chalk from 'chalk';
import * as os from 'os';
import path from 'path';
import ClientsSharedConfig from '../../../sharedByClients/config/ClientsSharedConfig';
import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile';
import ExerciseDockerCompose from '../../../sharedByClients/helpers/Dojo/ExerciseDockerCompose';
import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper';
import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError';
import ClientsSharedExerciseHelper from '../../../sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper';
import ExerciseResultsSanitizerAndValidator from '../../../sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator';
const execAsync = util.promisify(exec);
class ExerciseRunCommand extends CommanderCommand {
protected commandName: string = 'run';
private readonly dateISOString: string = (new Date()).toISOString().replace(/:/g, '_').replace(/\./g, '_');
private readonly folderResultsVolume: string = path.join(os.homedir(), 'DojoExecutions', `dojo_execLogs_${ this.dateISOString }`);
private readonly folderResultsDojo: string = path.join(this.folderResultsVolume, `Dojo/`);
private readonly folderResultsExercise: string = path.join(this.folderResultsVolume, `Exercise/`);
private readonly projectName: string = `${ ClientsSharedConfig.dockerCompose.projectName }_${ this.dateISOString.toLowerCase() }`;
private readonly fileComposeLogs: string = path.join(this.folderResultsDojo, `dockerComposeLogs.txt`);
protected defineCommand() {
this.command
.description('locally run an exercise')
.option('-p, --path <value>', 'exercise path', Config.folders.defaultLocalExercise)
.option('-v, --verbose', 'verbose mode (display docker compose logs in live)')
.action(this.commandAction.bind(this));
}
private displayExecutionLogs() {
ora({
text : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`,
indent: 0
}).start().info();
}
protected async commandAction(options: { path: string, verbose: boolean }): Promise<void> {
const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise;
let assignmentFile: AssignmentFile;
let exerciseDockerCompose: ExerciseDockerCompose;
let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;
let haveResultsVolume: boolean;
// Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running)
{
console.log(chalk.cyan('Please wait while we are checking and creating dependencies...'));
// Create result temp folder
fs.mkdirSync(this.folderResultsVolume, { recursive: true });
fs.mkdirSync(this.folderResultsDojo, { recursive: true });
fs.mkdirSync(this.folderResultsExercise, { recursive: true });
ora({
text : `Checking exercise content:`,
indent: 4
}).start().info();
// Exercise folder
{
const spinner: ora.Ora = ora({
text : `Checking exercise folder`,
indent: 8
}).start();
const files = fs.readdirSync(options.path);
const missingFiles = Config.exercise.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]);
if ( missingFiles.length > 0 ) {
spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`);
return;
}
spinner.succeed(`The exercise folder contains all the needed files`);
}
// dojo_assignment.json validity
{
const spinner: ora.Ora = ora({
text : `Checking ${ ClientsSharedConfig.assignment.filename } file`,
indent: 8
}).start();
const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(options.path, ClientsSharedConfig.assignment.filename));
if ( !validationResults.isValid ) {
spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ JSON.stringify(validationResults.errors) }`);
return;
} else {
assignmentFile = validationResults.results!;
}
haveResultsVolume = assignmentFile.result.volume !== undefined;
spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`);
}
// Docker daemon
{
const spinner: ora.Ora = ora({
text : `Checking Docker daemon`,
indent: 4
}).start();
try {
await execAsync(`cd "${ Config.folders.defaultLocalExercise }";docker ps`);
} catch ( error ) {
spinner.fail(`The Docker daemon is not running`);
return;
}
spinner.succeed(`The Docker daemon is running`);
}
}
// Step 2: Run docker-compose file
{
console.log(chalk.cyan('Please wait while we are running the exercise...'));
let composeFileOverride: string[] = [];
const composeOverridePath: string = path.join(localExercisePath, 'docker-compose-override.yml');
if ( haveResultsVolume ) {
const composeOverride = fs.readFileSync(path.join(__dirname, '../../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise);
fs.writeFileSync(composeOverridePath, composeOverride);
composeFileOverride = [ composeOverridePath ];
}
exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, assignmentFile, localExercisePath, composeFileOverride);
try {
await new Promise<void>((resolve, reject) => {
let spinner: ora.Ora;
if ( options.verbose ) {
exerciseDockerCompose.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
if ( displayable ) {
console.log(log);
}
});
}
exerciseDockerCompose.events.on('step', (name: string, message: string) => {
spinner = ora({
text : message,
indent: 4
}).start();
if ( options.verbose && name == 'COMPOSE_RUN' ) {
spinner.info();
}
});
exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => {
if ( error ) {
if ( options.verbose && stepName == 'COMPOSE_RUN' ) {
ora({
text : message,
indent: 4
}).start().fail();
} else {
spinner.fail(message);
}
} else {
if ( options.verbose && stepName == 'COMPOSE_RUN' ) {
ora({
text : message,
indent: 4
}).start().succeed();
} else {
spinner.succeed(message);
}
}
});
exerciseDockerCompose.events.on('finished', (success: boolean, exitCode: number) => {
success ? resolve() : reject();
});
exerciseDockerCompose.run(true);
});
} catch ( error ) { }
fs.rmSync(composeOverridePath, { force: true });
fs.writeFileSync(this.fileComposeLogs, exerciseDockerCompose.allLogs);
if ( !exerciseDockerCompose.success ) {
this.displayExecutionLogs();
return;
}
}
// Step 3: Get results
{
console.log(chalk.cyan('Please wait while we are checking the results...'));
exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode);
try {
await new Promise<void>((resolve, reject) => {
let spinner: ora.Ora;
exerciseResultsValidation.events.on('step', (name: string, message: string) => {
spinner = ora({
text : message,
indent: 4
}).start();
});
exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => {
if ( error ) {
if ( stepName == 'CHECK_SIZE' ) {
spinner.warn(message);
} else {
spinner.fail(message);
}
} else {
spinner.succeed(message);
}
});
exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
success || exitCode == ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject();
});
exerciseResultsValidation.run();
});
} catch ( error ) {
this.displayExecutionLogs();
return;
}
}
// Step 4: Display results + Volume location
{
ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, {
INFO : chalk.bold,
SUCCESS: chalk.green,
FAILURE: chalk.red
}, `\n\n${ chalk.bold('Execution results folder') } : ${ this.folderResultsVolume }`);
}
}
}
export default new ExerciseRunCommand();