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

v_1.0.1

parents 498ab340 b525a023
No related branches found
No related tags found
No related merge requests found
Pipeline #25793 canceled
Subproject commit fec06e6aeeff2083bfe82b38182f6a02c73d023f
Subproject commit 57997f6ff4ad2d2e23e03f86d997f64463cc898d
......@@ -31,6 +31,7 @@
"@types/node": "^18.17.1",
"@types/tar-stream": "^2.2.2",
"pkg": "^5.8.1",
"tiny-typed-emitter": "^2.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
}
......@@ -2230,6 +2231,12 @@
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"node_modules/tiny-typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
"dev": true
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
......
{
"name" : "dojo_exercice_checker",
"version" : "1.0.0",
"version" : "1.0.1",
"main" : "dist/app.js",
"bin" : {
"dirmanager": "./dist/app.js"
......@@ -43,6 +43,7 @@
"@types/node" : "^18.17.1",
"@types/tar-stream" : "^2.2.2",
"pkg" : "^5.8.1",
"tiny-typed-emitter": "^2.1.0",
"ts-node" : "^10.9.1",
"typescript" : "^5.1.6"
}
......
......@@ -4,20 +4,23 @@ const path = require('node:path');
require('dotenv').config({ path: path.join(__dirname, '../.env') });
require('./shared/helpers/TypeScriptExtensions'); // ATTENTION : This line MUST be the second of this file
import Styles from './types/Styles';
import Icon from './types/Icon';
import boxen from 'boxen';
import ClientsSharedConfig from './sharedByClients/config/ClientsSharedConfig';
import Styles from './types/Style';
import Icon from './sharedByClients/types/Icon';
import RecursiveFilesStats from './shared/helpers/recursiveFilesStats/RecursiveFilesStats';
import Toolbox from './shared/helpers/Toolbox';
import ExerciceHelper from './shared/helpers/ExerciceHelper';
import ExerciceCheckerError from './types/ExerciceCheckerError';
import { exec, spawn } from 'child_process';
import ExerciceCheckerError from './shared/types/Dojo/ExerciceCheckerError';
import { exec } from 'child_process';
import util from 'util';
import fs from 'fs-extra';
import HttpManager from './managers/HttpManager';
import DojoBackendManager from './managers/DojoBackendManager';
import Config from './config/Config';
import ArchiveHelper from './shared/helpers/ArchiveHelper';
import ExerciceDockerCompose from './sharedByClients/helpers/Dojo/ExerciceDockerCompose';
import ExerciceResultsValidation from './sharedByClients/helpers/Dojo/ExerciceResultsValidation';
import ExerciceEnonce from './sharedByClients/models/ExerciceEnonce';
import ClientsSharedExerciceHelper from './sharedByClients/helpers/Dojo/ClientsSharedExerciceHelper';
(async () => {
......@@ -27,13 +30,18 @@ import ArchiveHelper from './shared/helpers/ArchiveHelper';
console.log(Styles.APP_NAME(Config.appName));
let exerciceEnonce: ExerciceEnonce | undefined;
let exerciceDockerCompose: ExerciceDockerCompose;
let exerciceResultsValidation: ExerciceResultsValidation;
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1 & 2:
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1:
- Read the dojo enonce file from the enonce repository
- Download immutables files (maybe throw or show an error if the files have been modified ?)
*/
{
console.log(Styles.INFO(`${ Icon.INFO }️ Checking the exercice's enonce and his immutable files`));
const exerciceEnonce = await DojoBackendManager.getExerciceEnonce();
exerciceEnonce = await DojoBackendManager.getExerciceEnonce();
if ( !exerciceEnonce ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while getting the exercice's enonce`));
process.exit(ExerciceCheckerError.EXERCICE_ENONCE_GET_ERROR);
......@@ -44,93 +52,85 @@ import ArchiveHelper from './shared/helpers/ArchiveHelper';
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, immutableFile.content, { encoding: 'base64' });
});
}
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 3 & 4 & 5:
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 2:
- Get override of docker-compose file (for override the volume by a bind mount to the results folder shared between dind and the host)
- Run docker-compose file
- Get logs from linked services
*/
console.log(Styles.INFO(`${ Icon.INFO } Run docker compose file`));
const dockerComposeOverride = fs.readFileSync(path.join(__dirname, '../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', exerciceEnonce.enonceFile.result.volume).replace('{{MOUNT_PATH}}', Config.folders.resultsExercice);
fs.writeFileSync(`${ Config.folders.project }/docker-compose-override.yml`, dockerComposeOverride);
const changeDirectoryCommand = `cd "${ Config.folders.project }"`;
const dockerComposeCommand = `docker compose --project-name ${ Config.dockerCompose.projectName } --progress plain --file docker-compose.yml --file docker-compose-override.yml`;
const containerExitStatus = await new Promise<[ number, string ]>((resolve) => {
let logs = '####################################################### Docker Compose & Main Container Logs #######################################################\n';
const dockerCompose = spawn(`${ dockerComposeCommand } run --build ${ exerciceEnonce.enonceFile.result.container }`, {
cwd : Config.folders.project,
shell: true,
env : {
'DOCKER_BUILDKIT' : '1',
'BUILDKIT_PROGRESS': 'plain', ...process.env
}
});
{
const composeOverridePath: string = path.join(Config.folders.project, 'docker-compose-override.yml');
dockerCompose.stdout.on('data', (data) => {
logs += data.toString();
console.log(data.toString());
const composeOverride = fs.readFileSync(path.join(__dirname, '../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', exerciceEnonce.enonceFile.result.volume).replace('{{MOUNT_PATH}}', Config.folders.resultsExercice);
fs.writeFileSync(composeOverridePath, composeOverride);
exerciceDockerCompose = new ExerciceDockerCompose(ClientsSharedConfig.dockerCompose.projectName, exerciceEnonce.enonceFile, Config.folders.project, [ composeOverridePath ]);
try {
await new Promise<void>((resolve, reject) => {
exerciceDockerCompose.events.on('step', (name: string, message: string) => {
console.log(Styles.INFO(`${ Icon.INFO } ${ message }`));
});
dockerCompose.stderr.on('data', (data) => {
logs += data.toString();
console.error(data.toString());
exerciceDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => {
if ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } ${ message }`));
}
});
dockerCompose.on('exit', (code) => {
logs += '####################################################### Other Services Logs #######################################################\n';
resolve([ code ?? ExerciceCheckerError.DOCKER_COMPOSE_UP_ERROR, logs ]);
exerciceDockerCompose.events.on('finished', (success: boolean, exitCode: number) => {
success ? resolve() : reject();
});
exerciceDockerCompose.run();
});
const containerExitCode = containerExitStatus[0];
if ( containerExitCode === ExerciceCheckerError.DOCKER_COMPOSE_UP_ERROR ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while running the docker compose file`));
process.exit(containerExitCode);
}
fs.writeFileSync(`${ Config.folders.resultsDojo }/dockerComposeLogs.txt`, containerExitStatus[1]);
} catch ( error ) { }
console.log(Styles.INFO(`${ Icon.INFO } Acquire logs of linked services`));
try {
await execAsync(`${ changeDirectoryCommand };${ dockerComposeCommand } logs --timestamps >> ${ Config.folders.resultsDojo }/dockerComposeLogs.txt`);
} catch ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while getting the linked services logs`));
process.exit(ExerciceCheckerError.DOCKER_COMPOSE_LOGS_ERROR);
fs.rmSync(composeOverridePath);
fs.writeFileSync(path.join(Config.folders.resultsDojo, 'dockerComposeLogs.txt'), exerciceDockerCompose.allLogs);
if ( !exerciceDockerCompose.success ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Execution logs are available in artifacts`));
process.exit(exerciceDockerCompose.exitCode);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 6: Check content requirements and content size
console.log(Styles.INFO(`${ Icon.INFO } Validating results folder size`));
const resultsFolderSize = await Toolbox.fs.getTotalSize(Config.folders.resultsExercice);
if ( resultsFolderSize > Config.resultsFolderMaxSizeInBytes ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Results folder size is too big (bigger than ${ Config.resultsFolderMaxSizeInBytes / 1000000 })`));
process.exit(ExerciceCheckerError.EXERCICE_RESULTS_FOLDER_TOO_BIG);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 3: Check content requirements and content size
{
exerciceResultsValidation = new ExerciceResultsValidation(Config.folders.resultsDojo, Config.folders.resultsExercice);
console.log(Styles.INFO(`${ Icon.INFO } Checking results file`));
const resultsFileOriginPath = path.join(Config.folders.resultsExercice, Config.filenames.results);
const resultsFilePath = path.join(Config.folders.resultsDojo, Config.filenames.results);
try {
await new Promise<void>((resolve) => {
exerciceResultsValidation.events.on('step', (name: string, message: string) => {
console.log(Styles.INFO(`${ Icon.INFO } ${ message }`));
});
if ( !fs.existsSync(resultsFileOriginPath) ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Results file not found.`));
process.exit(ExerciceCheckerError.EXERCICE_RESULTS_FILE_NOT_FOUND);
exerciceResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => {
if ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } ${ message }`));
}
});
fs.moveSync(resultsFileOriginPath, resultsFilePath, { overwrite: true });
exerciceResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
if ( !success ) {
process.exit(exitCode);
}
const validationResults = ExerciceHelper.validateResultFile(resultsFilePath);
resolve();
});
if ( !validationResults.isValid ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Results file is not valid. Here are the errors :`));
console.error(Styles.ERROR(JSON.stringify(validationResults.errors)));
process.exit(ExerciceCheckerError.EXERCICE_RESULTS_FILE_SCHEMA_NOT_VALID);
exerciceResultsValidation.run();
});
} catch ( error ) { }
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 7: Upload and show the results
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 4: Upload results
{
try {
console.log(Styles.INFO(`${ Icon.INFO } Uploading results to the dojo server`));
const commit: any = {};
......@@ -143,34 +143,23 @@ import ArchiveHelper from './shared/helpers/ArchiveHelper';
liteStats : true
});
await DojoBackendManager.sendResults(containerExitCode, commit, validationResults.results!, files, await ArchiveHelper.getBase64(Config.folders.resultsVolume));
await DojoBackendManager.sendResults(exerciceDockerCompose.exitCode, commit, exerciceResultsValidation.exerciceResults!, files, await ArchiveHelper.getBase64(Config.folders.resultsVolume));
} catch ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while uploading the results`));
console.error(JSON.stringify(error));
process.exit(ExerciceCheckerError.UPLOAD);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 8: Exit with container exit code
const finalLogGlobalResult = `${ Styles.INFO('Global result') } : ${ validationResults.results!.success ? Styles.SUCCESS(`${ Icon.SUCCESS } Success`) : Styles.FAILURE(`${ Icon.FAILURE } Failure`) }`;
const finalLogExecutionExitCode = `${ Styles.INFO('Execution exit code') } : ${ (containerExitCode == 0 ? Styles.SUCCESS : Styles.ERROR)(containerExitCode) }`;
const finalLogResultNumbers = validationResults.results!.successfulTests || validationResults.results!.failedTests ? `\n\n${ Styles.SUCCESS('Tests passed') } : ${ validationResults.results!.successfulTests ?? '--' }\n${ Styles.ERROR('Tests failed') } : ${ validationResults.results!.failedTests ?? '--' }` : '';
const finalLogSuccessResultDetails = (validationResults.results!.successfulTestsList ?? []).map(testName => `- ${ Icon.SUCCESS } ${ testName }`).join('\n');
const finalLogFailedResultDetails = (validationResults.results!.failedTestsList ?? []).map(testName => `- ${ Icon.FAILURE } ${ testName }`).join('\n');
const finalLogResultDetails = validationResults.results!.successfulTestsList || validationResults.results!.failedTestsList ? `\n\n${ Styles.INFO('Tests') } :${ finalLogSuccessResultDetails != '' ? '\n' + finalLogSuccessResultDetails : '' }${ finalLogFailedResultDetails != '' ? '\n' + finalLogFailedResultDetails : '' }` : '';
console.log(boxen(`${ finalLogGlobalResult }\n\n${ finalLogExecutionExitCode }${ finalLogResultNumbers }${ finalLogResultDetails }`, {
title : 'Results',
titleAlignment: 'center',
borderColor : 'yellow',
borderStyle : 'bold',
margin : 1,
padding : 1,
textAlignment : 'left'
}));
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 5:
- Display results
- Exit with container exit code
*/
{
ClientsSharedExerciceHelper.displayExecutionResults(exerciceResultsValidation.exerciceResults!, exerciceDockerCompose.exitCode, Styles, `\n\n${ Icon.INFO }️ More detailed logs and resources may be available in artifacts`);
process.exit(containerExitCode);
process.exit(exerciceDockerCompose.exitCode);
}
})();
\ No newline at end of file
......@@ -5,49 +5,29 @@ import path from 'path';
class Config {
public readonly appName: string;
public readonly resultsFolderMaxSizeInBytes: number;
public readonly folders: {
project: string; resultsVolume: string; resultsDojo: string; resultsExercice: string;
};
public readonly filenames: {
results: string;
};
public readonly exercice: {
id: string; secret: string;
};
public readonly dockerCompose: {
projectName: string
};
constructor() {
this.appName = process.env.APP_NAME || '';
this.resultsFolderMaxSizeInBytes = Number(process.env.RESULTS_FOLDER_MAX_SIZE_IN_BYTES || 0);
this.folders = {
project : process.env.FILES_FOLDER?.convertWithEnvVars() ?? './',
resultsVolume : process.env.RESULTS_VOLUME?.convertWithEnvVars() ?? '',
resultsDojo : path.join(process.env.RESULTS_VOLUME?.convertWithEnvVars() ?? '', 'Dojo/'),
resultsExercice: path.join(process.env.RESULTS_VOLUME?.convertWithEnvVars() ?? '', 'Exercice/')
project : process.env.PROJECT_FOLDER?.convertWithEnvVars() ?? './',
resultsVolume : process.env.EXERCICE_RESULTS_VOLUME?.convertWithEnvVars() ?? '',
resultsDojo : path.join(process.env.EXERCICE_RESULTS_VOLUME?.convertWithEnvVars() ?? '', 'Dojo/'),
resultsExercice: path.join(process.env.EXERCICE_RESULTS_VOLUME?.convertWithEnvVars() ?? '', 'Exercice/')
};
this.resetResultsVolume();
this.filenames = {
results: process.env.RESULTS_FILENAME || ''
};
this.exercice = {
id : process.env.DOJO_EXERCICE_ID || '',
secret: process.env.DOJO_SECRET || ''
};
this.dockerCompose = {
projectName: process.env.DOCKER_COMPOSE_PROJECT_NAME || ''
};
}
private resetResultsVolume(): void {
......
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
import ApiRoutes from '../sharedByClients/types/ApiRoutes';
import axios from 'axios';
import DojoResponse from '../shared/types/Dojo/DojoResponse';
import ExerciceEnonce from '../sharedByClients/models/ExerciceEnonce';
import Config from '../config/Config';
import ExerciceResultsFile from '../shared/types/Dojo/ExerciceResultsFile';
import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute';
class DojoBackendManager {
public getApiUrl(route: ApiRoutes): string {
public getApiUrl(route: ApiRoute): string {
return `${ ClientsSharedConfig.apiURL }${ route }`;
}
public async getExerciceEnonce(): Promise<ExerciceEnonce | undefined> {
try {
return (await axios.get<DojoResponse<ExerciceEnonce>>(this.getApiUrl(ApiRoutes.EXERCICE_ENONCE).replace('{{id}}', Config.exercice.id))).data.data;
return (await axios.get<DojoResponse<ExerciceEnonce>>(this.getApiUrl(ApiRoute.EXERCICE_ENONCE).replace('{{id}}', Config.exercice.id))).data.data;
} catch ( error ) {
return undefined;
}
......@@ -22,7 +22,7 @@ class DojoBackendManager {
public async sendResults(exitCode: number, commit: any, results: ExerciceResultsFile, files: any, archiveBase64: string): Promise<void> {
try {
await axios.post(this.getApiUrl(ApiRoutes.EXERCICE_RESULTS).replace('{{id}}', Config.exercice.id), {
await axios.post(this.getApiUrl(ApiRoute.EXERCICE_RESULTS).replace('{{id}}', Config.exercice.id), {
exitCode : exitCode,
commit : JSON.stringify(commit),
results : JSON.stringify(results),
......
Subproject commit eab5c0a5a32079fcb439a1ad79453611c8605536
Subproject commit f33e4e0c7b34f9060e8995550920d25cd3e73c40
Subproject commit c0f105590a4332ce4d6eff046324e537e769f756
Subproject commit 8872f91f280e60287c4dba46de58f3f412e0a462
enum ExerciceCheckerError {
EXERCICE_ENONCE_GET_ERROR = 200,
DOCKER_COMPOSE_UP_ERROR = 201,
DOCKER_COMPOSE_LOGS_ERROR = 202,
EXERCICE_RESULTS_FOLDER_TOO_BIG = 203,
EXERCICE_RESULTS_FILE_NOT_FOUND = 204,
EXERCICE_RESULTS_FILE_SCHEMA_NOT_VALID = 205,
UPLOAD = 206
}
export default ExerciceCheckerError;
\ No newline at end of file
enum Icon {
INFO = 'ℹ️',
ERROR = '⛔️',
SUCCESS = '',
FAILURE = ''
}
export default Icon;
\ No newline at end of file
import chalk from 'chalk';
class Styles {
class Style {
public readonly APP_NAME = chalk.bgBlue.black.bold;
public readonly INFO = chalk.blue;
public readonly ERROR = chalk.red;
......@@ -10,4 +10,4 @@ class Styles {
}
export default new Styles();
\ No newline at end of file
export default new Style();
\ 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