Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • ask-user-to-delete-exercises-on-duplicates
  • jw_sonar
  • jw_sonar_backup
  • main
  • move-to-esm-only
  • open_tool_for_self_hosting
  • v5.0
  • v6.0
  • v4.1
  • v4.2
10 results

Target

Select target project
  • dojo_project/projects/shared/nodeclientsharedcode
1 result
Select Git revision
  • ask-user-to-delete-exercises-on-duplicates
  • jw_sonar
  • jw_sonar_backup
  • main
  • move-to-esm-only
  • open_tool_for_self_hosting
  • v5.0
  • v6.0
  • v4.1
  • v4.2
10 results
Show changes
Commits on Source (12)
......@@ -8,6 +8,12 @@ These shared modules are needed :
- `NodeSharedCode` imported in parent directory with directory name `shared`
These packages are needed :
- `boxen@5.1`
- `chalk@4.1`
- `fs-extra`
## How to use it
By adding this repo as submodule
......
......@@ -6,6 +6,16 @@ class ClientsSharedConfig {
dojoAccount: { id: number; username: string; };
};
public readonly dockerCompose: {
projectName: string
};
public readonly exerciceResultsFolderMaxSizeInBytes: number;
public readonly filenames: {
results: string;
};
constructor() {
this.apiURL = process.env.API_URL || '';
......@@ -17,6 +27,16 @@ class ClientsSharedConfig {
username: process.env.GITLAB_DOJO_ACCOUNT_USERNAME || ''
}
};
this.dockerCompose = {
projectName: process.env.DOCKER_COMPOSE_PROJECT_NAME || ''
};
this.exerciceResultsFolderMaxSizeInBytes = Number(process.env.EXERCICE_RESULTS_FOLDER_MAX_SIZE_IN_BYTES || 0);
this.filenames = {
results: process.env.EXERCICE_RESULTS_FILENAME || ''
};
}
}
......
import ExerciceResultsFile from '../../../shared/types/Dojo/ExerciceResultsFile';
import chalk from 'chalk';
import boxen from 'boxen';
import Icon from '../../types/Icon';
class ClientsSharedExerciceHelper {
displayExecutionResults(exerciceResults: ExerciceResultsFile, containerExitCode: number, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }, additionalText: string = '') {
const finalLogGlobalResult = `${ Style.INFO('Global result') } : ${ exerciceResults.success ? Style.SUCCESS(`${ Icon.SUCCESS } Success`) : Style.FAILURE(`${ Icon.FAILURE } Failure`) }`;
const finalLogExecutionExitCode = `${ Style.INFO('Execution exit code') } : ${ (containerExitCode == 0 ? Style.SUCCESS : Style.FAILURE)(containerExitCode) }`;
const finalLogResultNumbers = exerciceResults.successfulTests || exerciceResults.failedTests ? `\n\n${ Style.SUCCESS('Tests passed') } : ${ exerciceResults.successfulTests ?? '--' }\n${ Style.FAILURE('Tests failed') } : ${ exerciceResults.failedTests ?? '--' }` : '';
const finalLogSuccessResultDetails = (exerciceResults.successfulTestsList ?? []).map(testName => `- ${ Icon.SUCCESS } ${ testName }`).join('\n');
const finalLogFailedResultDetails = (exerciceResults.failedTestsList ?? []).map(testName => `- ${ Icon.FAILURE } ${ testName }`).join('\n');
const finalLogResultDetails = exerciceResults.successfulTestsList || exerciceResults.failedTestsList ? `\n\n${ Style.INFO('Tests') } :${ finalLogSuccessResultDetails != '' ? '\n' + finalLogSuccessResultDetails : '' }${ finalLogFailedResultDetails != '' ? '\n' + finalLogFailedResultDetails : '' }` : '';
console.log(boxen(`${ finalLogGlobalResult }\n\n${ finalLogExecutionExitCode }${ finalLogResultNumbers }${ finalLogResultDetails }${ additionalText }`, {
title : 'Results',
titleAlignment: 'center',
borderColor : 'yellow',
borderStyle : 'bold',
margin : 1,
padding : 1,
textAlignment : 'left'
}));
}
}
export default new ClientsSharedExerciceHelper();
\ No newline at end of file
import EnonceFile from '../../../shared/types/Dojo/EnonceFile';
import { TypedEmitter } from 'tiny-typed-emitter';
import ExerciceRunningEvents from '../../types/Dojo/ExerciceRunningEvents';
import { spawn } from 'child_process';
import ExerciceCheckerError from '../../../shared/types/Dojo/ExerciceCheckerError';
class ExerciceDockerCompose {
readonly events: TypedEmitter<ExerciceRunningEvents> = new TypedEmitter<ExerciceRunningEvents>();
public displayableLogs: string = '';
public allLogs: string = '';
public isFinished: boolean = false;
public success: boolean = false;
public exitCode: number = -1;
constructor(private projectName: string, private enonceFile: EnonceFile, private executionFolder: string, private composeFileOverride: Array<string> = []) {
this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
this.allLogs += log;
this.displayableLogs += displayable ? log : '';
});
this.events.on('finished', (success: boolean, exitCode: number) => {
this.isFinished = true;
this.success = success;
this.exitCode = exitCode;
});
}
run(doDown: boolean = false) {
(async () => {
let containerExitCode: number = -1;
const dockerComposeCommand = `docker compose --project-name ${ this.projectName } --progress plain --file docker-compose.yml ${ this.composeFileOverride.map((file) => `--file ${ file }`).join(' ') }`;
// Run the service
{
try {
this.events.emit('step', 'COMPOSE_RUN', 'Running Docker Compose file');
containerExitCode = await new Promise<number>((resolve, reject) => {
this.events.emit('logs', '####################################################### Docker Compose & Main Container Logs #######################################################\n', false, false);
const dockerCompose = spawn(`${ dockerComposeCommand } run --build --rm ${ this.enonceFile.result.container }`, {
cwd : this.executionFolder,
shell: true,
env : {
'DOCKER_BUILDKIT' : '1',
'BUILDKIT_PROGRESS': 'plain', ...process.env
}
});
dockerCompose.stdout.on('data', (data) => {
this.events.emit('logs', data.toString(), false, true);
});
dockerCompose.stderr.on('data', (data) => {
this.events.emit('logs', data.toString(), true, true);
});
dockerCompose.on('exit', (code) => {
code !== null ? resolve(code) : reject();
});
});
} catch ( error ) {
this.events.emit('endStep', 'COMPOSE_RUN', `Error while running the docker compose file`, true);
this.events.emit('finished', false, ExerciceCheckerError.DOCKER_COMPOSE_RUN_ERROR);
return;
}
this.events.emit('endStep', 'COMPOSE_RUN', `Docker Compose file run successfully`, false);
}
// Get linked services logs
{
try {
this.events.emit('step', 'COMPOSE_LOGS', 'Linked services logs acquisition');
await new Promise<void>((resolve, reject) => {
this.events.emit('logs', '####################################################### Other Services Logs #######################################################\n', false, false);
const dockerCompose = spawn(`${ dockerComposeCommand } logs --timestamps`, {
cwd : this.executionFolder,
shell: true
});
dockerCompose.stdout.on('data', (data) => {
this.events.emit('logs', data.toString(), false, false);
});
dockerCompose.stderr.on('data', (data) => {
this.events.emit('logs', data.toString(), true, false);
});
dockerCompose.on('exit', (code) => {
code !== null ? resolve() : reject();
});
});
} catch ( error ) {
this.events.emit('endStep', 'COMPOSE_LOGS', `Error while getting the linked services logs`, true);
this.events.emit('finished', false, ExerciceCheckerError.DOCKER_COMPOSE_LOGS_ERROR);
return;
}
this.events.emit('endStep', 'COMPOSE_LOGS', `Linked services logs acquired`, false);
}
// Remove containers if asked
{
if ( doDown ) {
try {
this.events.emit('step', 'COMPOSE_DOWN', 'Stopping and removing containers');
await new Promise<void>((resolve, reject) => {
this.events.emit('logs', '####################################################### Stop and remove containers #######################################################\n', false, false);
const dockerCompose = spawn(`${ dockerComposeCommand } down --volumes`, {
cwd : this.executionFolder,
shell: true
});
dockerCompose.stdout.on('data', (data) => {
this.events.emit('logs', data.toString(), false, false);
});
dockerCompose.stderr.on('data', (data) => {
this.events.emit('logs', data.toString(), true, false);
});
dockerCompose.on('exit', (code) => {
code !== null ? resolve() : reject();
});
});
} catch ( error ) {
this.events.emit('endStep', 'COMPOSE_DOWN', `Error stop and remove containers`, true);
this.events.emit('finished', false, ExerciceCheckerError.DOCKER_COMPOSE_DOWN_ERROR);
return;
}
this.events.emit('endStep', 'COMPOSE_DOWN', `Containers stopped and removed`, false);
}
}
this.events.emit('finished', true, containerExitCode);
})();
}
}
export default ExerciceDockerCompose;
\ No newline at end of file
import { TypedEmitter } from 'tiny-typed-emitter';
import ExerciceRunningEvents from '../../types/Dojo/ExerciceRunningEvents';
import ExerciceCheckerError from '../../../shared/types/Dojo/ExerciceCheckerError';
import path from 'node:path';
import SharedExerciceHelper from '../../../shared/helpers/Dojo/SharedExerciceHelper';
import ClientsSharedConfig from '../../config/ClientsSharedConfig';
import Toolbox from '../../../shared/helpers/Toolbox';
import * as fs from 'fs-extra';
import ExerciceResultsFile from '../../../shared/types/Dojo/ExerciceResultsFile';
class ExerciceResultsValidation {
readonly events: TypedEmitter<ExerciceRunningEvents> = new TypedEmitter<ExerciceRunningEvents>();
public exerciceResults: ExerciceResultsFile | undefined = undefined;
constructor(private folderResultsDojo: string, private folderResultsExercice: string) { }
run() {
(async () => {
let resultsFilePath: string;
// Results file existence
{
this.events.emit('step', 'CHECK_RESULTS_FILE_EXIST', 'Checking if results file exists');
const resultsFileOriginPath = path.join(this.folderResultsExercice, ClientsSharedConfig.filenames.results);
resultsFilePath = path.join(this.folderResultsDojo, ClientsSharedConfig.filenames.results);
if ( !fs.existsSync(resultsFileOriginPath) ) {
this.events.emit('endStep', 'CHECK_RESULTS_FILE_EXIST', `Results file not found`, true);
this.events.emit('finished', false, ExerciceCheckerError.EXERCICE_RESULTS_FILE_NOT_FOUND);
return;
}
this.events.emit('endStep', 'CHECK_RESULTS_FILE_EXIST', 'Results file found', false);
fs.moveSync(resultsFileOriginPath, resultsFilePath, { overwrite: true });
}
// Results file schema validation
{
this.events.emit('step', 'VALIDATE_RESULTS_FILE', 'Validating results file schema');
const validationResults = SharedExerciceHelper.validateResultFile(resultsFilePath);
if ( !validationResults.isValid ) {
this.events.emit('endStep', 'VALIDATE_RESULTS_FILE', `Results file is not valid. Here are the errors :\n${ JSON.stringify(validationResults.errors) }`, true);
this.events.emit('finished', false, ExerciceCheckerError.EXERCICE_RESULTS_FILE_SCHEMA_NOT_VALID);
return;
}
this.exerciceResults = validationResults.results;
this.events.emit('endStep', 'VALIDATE_RESULTS_FILE', 'Results file is valid', false);
}
// Results folder size
// ATTENTION: This test is at the end because even if it fail the local execution will continue and we need the other test above to be done
{
this.events.emit('step', 'CHECK_SIZE', 'Validating results folder size');
const resultsFolderSize = await Toolbox.fs.getTotalSize(this.folderResultsExercice);
if ( resultsFolderSize > ClientsSharedConfig.exerciceResultsFolderMaxSizeInBytes ) {
this.events.emit('endStep', 'CHECK_SIZE', `Results folder size is too big (bigger than ${ ClientsSharedConfig.exerciceResultsFolderMaxSizeInBytes / 1000000 } MB)`, true);
this.events.emit('finished', false, ExerciceCheckerError.EXERCICE_RESULTS_FOLDER_TOO_BIG);
return;
}
this.events.emit('endStep', 'CHECK_SIZE', 'Results folder size is in bounds', false);
}
this.events.emit('finished', true, 0);
})();
}
}
export default ExerciceResultsValidation;
\ No newline at end of file
enum ApiRoutes {
enum ApiRoute {
LOGIN = '/login',
TEST_SESSION = '/test_session',
GITLAB_CHECK_TEMPLATE_ACCESS = '/gitlab/project/{{id}}/checkTemplateAccess',
......@@ -12,4 +12,4 @@ enum ApiRoutes {
}
export default ApiRoutes;
\ No newline at end of file
export default ApiRoute;
\ No newline at end of file
interface ExerciceRunningEvents {
step: (name: string, message: string) => void;
endStep: (stepName: string, message: string, error: boolean) => void;
logs: (log: string, error: boolean, displayable: boolean) => void;
finished: (success: boolean, exitCode: number) => void;
}
export default ExerciceRunningEvents;
\ No newline at end of file
enum Icon {
INFO = 'ℹ️',
ERROR = '⛔️',
SUCCESS = '',
FAILURE = ''
}
export default Icon;
\ No newline at end of file