Skip to content
Snippets Groups Projects
Commit 611134eb authored by michael.minelli's avatar michael.minelli
Browse files

Merge branch 'add-assignment-run-command' into v3.3.0

parents 8a6de8af e7dc5bba
No related branches found
No related tags found
No related merge requests found
......@@ -3,6 +3,7 @@ import AssignmentCreateCommand from './subcommands/AssignmentCreateCommand';
import AssignmentPublishCommand from './subcommands/AssignmentPublishCommand';
import AssignmentUnpublishCommand from './subcommands/AssignmentUnpublishCommand';
import AssignmentCheckCommand from './subcommands/AssignmentCheckCommand';
import AssignmentRunCommand from './subcommands/AssignmentRunCommand';
class AssignmentCommand extends CommanderCommand {
......@@ -16,6 +17,7 @@ class AssignmentCommand extends CommanderCommand {
protected defineSubCommands() {
AssignmentCreateCommand.registerOnCommand(this.command);
AssignmentCheckCommand.registerOnCommand(this.command);
AssignmentRunCommand.registerOnCommand(this.command);
AssignmentPublishCommand.registerOnCommand(this.command);
AssignmentUnpublishCommand.registerOnCommand(this.command);
}
......
import CommanderCommand from '../../CommanderCommand';
import Config from '../../../config/Config';
import ExerciseRunHelper from '../../../helpers/Dojo/ExerciseRunHelper';
class AssignmentRunCommand extends CommanderCommand {
protected commandName: string = 'run';
protected defineCommand() {
// This command is synced with the "exercise run" command
this.command
.description('locally run the assignment as 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));
}
protected async commandAction(options: { path: string, verbose: boolean }): Promise<void> {
await ExerciseRunHelper.run(options);
}
}
export default new AssignmentRunCommand();
\ No newline at end of file
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);
import ExerciseRunHelper from '../../../helpers/Dojo/ExerciseRunHelper';
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 is synced with the "assignment run" command
this.command
.description('locally run an exercise')
.option('-p, --path <value>', 'exercise path', Config.folders.defaultLocalExercise)
......@@ -40,222 +15,8 @@ class ExerciseRunCommand extends CommanderCommand {
.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) => {
success ? resolve() : reject();
});
exerciseDockerCompose.run(true);
});
} catch ( error ) { /* empty */ }
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 }`);
}
await ExerciseRunHelper.run(options);
}
}
......
import ora from 'ora';
import chalk from 'chalk';
import Config from '../../config/Config';
import AssignmentFile from '../../shared/types/Dojo/AssignmentFile';
import ExerciseDockerCompose from '../../sharedByClients/helpers/Dojo/ExerciseDockerCompose';
import ExerciseResultsSanitizerAndValidator from '../../sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator';
import fs from 'node:fs';
import ClientsSharedConfig from '../../sharedByClients/config/ClientsSharedConfig';
import SharedAssignmentHelper from '../../shared/helpers/Dojo/SharedAssignmentHelper';
import path from 'path';
import ExerciseCheckerError from '../../shared/types/Dojo/ExerciseCheckerError';
import ClientsSharedExerciseHelper from '../../sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper';
import os from 'os';
import util from 'util';
import { exec } from 'child_process';
const execAsync = util.promisify(exec);
class ExerciseRunHelper {
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`);
private displayExecutionLogs() {
ora({
text : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`,
indent: 0
}).start().info();
}
async run(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) => {
success ? resolve() : reject();
});
exerciseDockerCompose.run(true);
});
} catch ( error ) { /* empty */ }
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 ExerciseRunHelper();
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment