Skip to content
Snippets Groups Projects
Select Git revision
  • a7082518c0440e9f57ca78d832a97cd7f35238ac
  • main default protected
  • jw_sonar
  • v6.0.0 protected
  • interactive-mode-preference
  • bedran_exercise-list
  • add_route_user
  • Jw_sonar_backup
  • exercise_list_filter
  • assignment_filter
  • add_route_assignments
  • move-to-esm-only
  • 6.0.0-dev
  • Pre-alpha
  • 5.0.0
  • Latest
  • 4.2.0
  • 4.1.1
  • 4.1.0
  • 4.0.1
  • 4.0.0
  • 3.5.0
  • 3.4.2
  • 3.4.1
  • 3.3.0
  • 3.2.3
  • 3.2.2
  • 3.2.0
  • 3.1.2
  • 3.1.1
  • 3.1.0
  • 3.0.1
32 results

ExerciseRunCommand.ts

Blame
  • 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();