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

Target

Select target project
  • dojo_project/projects/shared/nodeclientsharedcode
1 result
Select Git revision
Show changes
Commits on Source (47)
Showing with 756 additions and 275 deletions
import axios from 'axios';
import DojoBackendResponse from '../../shared/types/Dojo/DojoBackendResponse';
import ApiRoute from '../types/Dojo/ApiRoute';
interface ClientsConfig {
gitlabUrl: string,
gitlabAccountId: number,
gitlabAccountUsername: string,
loginGitlabClientId: string,
exerciseMaxPerAssignment: number
}
class ClientsSharedConfig { class ClientsSharedConfig {
public apiURL: string; private static config: ClientsSharedConfig | undefined = undefined;
public assignment: { public apiURL!: string;
filename: string, neededFiles: Array<string>
public gitlab!: {
URL: string, dojoAccount: { id: number; username: string; };
};
public login!: {
gitlab: {
client: {
id: string
}, url: {
redirect: string, token: string
}
}
}; };
public gitlab: { public assignment!: {
dojoAccount: { id: number; username: string; }; filename: string, neededFiles: Array<string>, name: string, secret: string;
}; };
public readonly dockerCompose: { public exercise!: {
maxPerAssignment: number
};
public dockerCompose!: {
projectName: string projectName: string
}; };
public readonly exerciseResultsFolderMaxSizeInBytes: number; public exerciseResultsFolderMaxSizeInBytes!: number;
public readonly filenames: { public filenames!: {
results: string; results: string;
}; };
constructor() { public constructor() {
this.apiURL = process.env.API_URL || ''; this.login = {
gitlab: {
client: {
id: ''
},
url : {
redirect: '',
token : ''
}
}
};
}
this.assignment = { public envVarGetter(): (envName: string, defaultValue: string) => string {
filename : process.env.ASSIGNMENT_FILENAME || '', return (envName: string, defaultValue: string) => {
neededFiles: JSON.parse(process.env.EXERCISE_NEEDED_FILES || '[]') let value = process.env[envName] ?? defaultValue;
if ( value.includes('{{GITLAB_URL}}') ) {
value = value.replace('{{GITLAB_URL}}', this.gitlab.URL);
}
if ( value.includes('{{GITLAB_ACCOUNT_ID}}') ) {
value = value.replace('{{GITLAB_ACCOUNT_ID}}', String(this.gitlab.dojoAccount.id));
}
if ( value.includes('{{GITLAB_ACCOUNT_USERNAME}}') ) {
value = value.replace('{{GITLAB_ACCOUNT_USERNAME}}', this.gitlab.dojoAccount.username);
}
if ( value.includes('{{LOGIN_GITLAB_CLIENT_ID}}') ) {
value = value.replace('{{LOGIN_GITLAB_CLIENT_ID}}', this.login.gitlab.client.id);
}
return value;
}; };
}
private async fetchConfigFromApi() {
const downloadedConfig: ClientsConfig = (await axios.get<DojoBackendResponse<ClientsConfig>>(`${ this.apiURL }${ ApiRoute.CLIENTS_CONFIG }`)).data.data;
this.gitlab = { this.gitlab = {
URL : downloadedConfig.gitlabUrl,
dojoAccount: { dojoAccount: {
id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID) || -1, id : downloadedConfig.gitlabAccountId,
username: process.env.GITLAB_DOJO_ACCOUNT_USERNAME || '' username: downloadedConfig.gitlabAccountUsername
} }
}; };
this.login.gitlab.client.id = downloadedConfig.loginGitlabClientId;
this.exercise = {
maxPerAssignment: downloadedConfig.exerciseMaxPerAssignment
};
}
async init(apiUrl: string) {
this.apiURL = apiUrl;
await this.fetchConfigFromApi();
const getEnvVar = this.envVarGetter();
this.login.gitlab.url = {
redirect: getEnvVar('LOGIN_GITLAB_URL_REDIRECT', ''),
token : getEnvVar('LOGIN_GITLAB_URL_TOKEN', '')
};
this.assignment = {
filename : getEnvVar('ASSIGNMENT_FILENAME', ''),
neededFiles: JSON.parse(getEnvVar('EXERCISE_NEEDED_FILES', '[]')),
name : process.env.DOJO_ASSIGNMENT_NAME || '',
secret : process.env.DOJO_ASSIGNMENT_SECRET || ''
};
this.dockerCompose = { this.dockerCompose = {
projectName: process.env.DOCKER_COMPOSE_PROJECT_NAME || '' projectName: getEnvVar('DOCKER_COMPOSE_PROJECT_NAME', '')
}; };
this.exerciseResultsFolderMaxSizeInBytes = Number(process.env.EXERCISE_RESULTS_FOLDER_MAX_SIZE_IN_BYTES || 0); this.exerciseResultsFolderMaxSizeInBytes = Number(getEnvVar('EXERCISE_RESULTS_FOLDER_MAX_SIZE_IN_BYTES', '0'));
this.filenames = { this.filenames = {
results: process.env.EXERCISE_RESULTS_FILENAME || '' results: getEnvVar('EXERCISE_RESULTS_FILENAME', '')
}; };
} }
} }
......
import { TypedEmitter } from 'tiny-typed-emitter'; import { TypedEmitter } from 'tiny-typed-emitter';
import AssignmentValidatorEvents from '../../types/Dojo/AssignmentValidatorEvents'; import AssignmentValidatorEvents from '../../types/Dojo/AssignmentValidatorEvents';
import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper'; import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper';
import path from 'node:path'; import path from 'node:path';
import AssignmentCheckerError from '../../../shared/types/Dojo/AssignmentCheckerError'; import AssignmentCheckerError from '../../../shared/types/Dojo/AssignmentCheckerError';
import fs from 'fs-extra'; import fs from 'fs-extra';
import ClientsSharedConfig from '../../config/ClientsSharedConfig'; import ClientsSharedConfig from '../../config/ClientsSharedConfig';
import YAML from 'yaml'; import YAML from 'yaml';
import DojoDockerCompose from '../../types/Dojo/DojoDockerCompose'; import DojoDockerCompose from '../../types/Dojo/DojoDockerCompose';
import { exec, spawn } from 'child_process'; import { exec, spawn } from 'child_process';
import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile'; import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile';
import ExerciseDockerCompose from './ExerciseDockerCompose'; import ExerciseDockerCompose from './ExerciseDockerCompose';
import util from 'util'; import util from 'util';
import Assignment, { Language } from '../../models/Assignment';
import ClientsSharedAssignmentHelper from './ClientsSharedAssignmentHelper';
import SonarAnalyzer from './SonarAnalyzer';
const execAsync = util.promisify(exec); const execAsync = util.promisify(exec);
class AssignmentValidator { class AssignmentValidator {
private readonly folderAssignment: string;
private readonly doDown: boolean;
private readonly runSonar: boolean;
readonly events: TypedEmitter<AssignmentValidatorEvents> = new TypedEmitter<AssignmentValidatorEvents>(); readonly events: TypedEmitter<AssignmentValidatorEvents> = new TypedEmitter<AssignmentValidatorEvents>();
public displayableLogs: string = ''; public displayableLogs: string = '';
...@@ -30,7 +37,15 @@ class AssignmentValidator { ...@@ -30,7 +37,15 @@ class AssignmentValidator {
private currentStep: string = 'NOT_RUNNING'; private currentStep: string = 'NOT_RUNNING';
private currentSubStep: string = 'NOT_RUNNING'; private currentSubStep: string = 'NOT_RUNNING';
constructor(private folderAssignment: string) { private dockerComposeFile!: DojoDockerCompose;
private assignmentFile!: AssignmentFile;
private assignment!: Assignment;
constructor(folderAssignment: string, doDown: boolean = false, runSonar: boolean = false) {
this.folderAssignment = folderAssignment;
this.doDown = doDown;
this.runSonar = runSonar;
this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => { this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
this.allLogs += log; this.allLogs += log;
this.displayableLogs += displayable ? log : ''; this.displayableLogs += displayable ? log : '';
...@@ -77,212 +92,305 @@ class AssignmentValidator { ...@@ -77,212 +92,305 @@ class AssignmentValidator {
this.finished(false, code); this.finished(false, code);
} }
run(doDown: boolean = false) { /**
(async () => { * Step 1: Check requirements
let dockerComposeFile: DojoDockerCompose; * - Check if Docker daemon is running
let assignmentFile: AssignmentFile; * - Check if required files exists
* @private
*/
private async checkRequirements() {
this.newStep('REQUIREMENTS_CHECKING', 'Please wait while we are checking requirements...');
/* // Check requirements
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1: Check requirements this.newSubStep('DOCKER_RUNNING', 'Checking if Docker daemon is running');
- Check if Docker daemon is running try {
- Check if required files exists await execAsync(`docker ps`);
*/ } catch ( error ) {
{ this.emitError(`Docker daemon isn't running`, `Some requirements are not satisfied.`, AssignmentCheckerError.DOCKER_DAEMON_NOT_RUNNING);
this.newStep('REQUIREMENTS_CHECKING', 'Please wait while we are checking requirements...'); throw new Error();
}
this.endSubStep('Docker daemon is running', false);
// Check requirements // Check if required files exists
this.newSubStep('DOCKER_RUNNING', 'Checking if Docker daemon is running'); this.newSubStep('REQUIRED_FILES_EXISTS', 'Checking if required files exists');
try { const files = fs.readdirSync(this.folderAssignment);
await execAsync(`docker ps`); const missingFiles = ClientsSharedConfig.assignment.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]);
} catch ( error ) {
this.emitError(`Docker daemon isn't running`, `Some requirements are not satisfied.`, AssignmentCheckerError.DOCKER_DAEMON_NOT_RUNNING);
return;
}
this.endSubStep('Docker daemon is running', false);
if ( missingFiles.length > 0 ) {
this.emitError(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`, 'Some requirements are not satisfied', AssignmentCheckerError.REQUIRED_FILES_MISSING);
throw new Error();
}
this.endSubStep('All required files exists', false);
// Check if required files exists
this.newSubStep('REQUIRED_FILES_EXISTS', 'Checking if required files exists');
const files = fs.readdirSync(this.folderAssignment);
const missingFiles = ClientsSharedConfig.assignment.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]);
if ( missingFiles.length > 0 ) { this.endStep('All requirements are satisfied', false);
this.emitError(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`, 'Some requirements are not satisfied', AssignmentCheckerError.REQUIRED_FILES_MISSING); }
return;
} /**
this.endSubStep('All required files exists', false); * Step 2: Check assignment
* - Check if assignment exists in backend
* @private
*/
private async checkAssignment() {
this.newStep('ASSIGNMENT_CHECKING', 'Please wait while we are checking the assignment...');
const resp = await ClientsSharedAssignmentHelper.getAssignmentByName(ClientsSharedConfig.assignment.name);
if ( resp == undefined ) {
this.emitError(`The assignment doesn't exist. An assignment must be created with "assignment create" before checking it.`, `Assignment doesn't exists`, AssignmentCheckerError.ASSIGNMENT_MISSING);
throw new Error();
} else {
this.assignment = resp;
}
this.endStep('All requirements are satisfied', false);
this.endStep('Assignment exists', false);
}
/**
* Step 3: dojo_assignment.json file validation
* - Structure validation
* - Immutable files validation (Check if exists and if the given type is correct)
* - Build line validation (for C-derived languages and sonar activated projects)
* @private
*/
private dojoAssignmentFileValidation() {
this.newStep('ASSIGNMENT_FILE_VALIDATION', 'Please wait while we are validating dojo_assignment.json file...');
const assignmentFileValidationError = `${ ClientsSharedConfig.assignment.filename } file is invalid`;
// Structure validation
this.newSubStep('ASSIGNMENT_FILE_SCHEMA_VALIDATION', 'Validating dojo_assignment.json file schema');
const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.folderAssignment, ClientsSharedConfig.assignment.filename));
if ( !validationResults.isValid ) {
this.emitError(`dojo_assignment.json file schema is invalid.\nHere are the errors:\n${ validationResults.error }`, assignmentFileValidationError, AssignmentCheckerError.ASSIGNMENT_FILE_SCHEMA_ERROR);
throw new Error();
}
this.assignmentFile = validationResults.content!;
this.endSubStep('dojo_assignment.json file schema is valid', false);
// Immutable files validation (Check if exists and if the given type is correct)
this.newSubStep('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Validating immutable files');
for ( const immutable of this.assignmentFile.immutable ) {
const immutablePath = path.join(this.folderAssignment, immutable.path);
if ( !fs.existsSync(immutablePath) ) {
this.emitError(`Immutable path not found: ${ immutable.path }`, assignmentFileValidationError, AssignmentCheckerError.IMMUTABLE_PATH_NOT_FOUND);
throw new Error();
} }
const isDirectory = fs.lstatSync(immutablePath).isDirectory();
if ( isDirectory && !immutable.isDirectory ) {
this.emitError(`Immutable (${ immutable.path }) is declared as a file but is a directory.`, assignmentFileValidationError, AssignmentCheckerError.IMMUTABLE_PATH_IS_NOT_DIRECTORY);
throw new Error();
} else if ( !isDirectory && immutable.isDirectory === true ) {
this.emitError(`Immutable (${ immutable.path }) is declared as a directory but is a file.`, assignmentFileValidationError, AssignmentCheckerError.IMMUTABLE_PATH_IS_DIRECTORY);
throw new Error();
}
}
this.endSubStep('Immutable files are valid', false);
// Build line validation (only if language is C/CPP/OBJ-C and sonar activated)
if ( [ Language.c, Language.cpp, Language.objc ].includes(this.assignment.language) && this.assignment.useSonar ) {
this.newSubStep('ASSIGNMENT_FILE_BUILD_LINE_VALIDATION', 'Validating build line');
const build = validationResults.content!.buildLine;
if ( build == undefined || build.trim() == '' ) {
this.emitError(`BuildLine is required for this language`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.BUILD_LINE_MISSING);
return;
}
this.endSubStep('Build line is valid', false);
}
/* this.endStep('dojo_assignment.json file is valid', false);
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 2: dojo_assignment.json file validation }
- Structure validation
- Immutable files validation (Check if exists and if the given type is correct)
*/
{
this.newStep('ASSIGNMENT_FILE_VALIDATION', 'Please wait while we are validating dojo_assignment.json file...');
/**
* Step 4: Docker Compose file validation
* - Global validation
* - Validation of the containers and volumes named in dojo_assignment.json
* @private
*/
private async dockerComposeFileValidation() {
this.newStep('DOCKER_COMPOSE_VALIDATION', 'Please wait while we are validating docker compose file...');
const composeFileValidationError = `Docker compose file is invalid`;
// Global validation
this.newSubStep('DOCKER_COMPOSE_STRUCTURE_VALIDATION', 'Docker compose file structure validation');
try {
this.dockerComposeFile = YAML.parse(fs.readFileSync(path.join(this.folderAssignment, 'docker-compose.yml'), 'utf8')) as DojoDockerCompose;
} catch ( error ) {
this.emitError(`Docker compose file yaml structure is invalid.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_YAML_ERROR);
throw new Error();
}
try {
await new Promise<void>((resolve, reject) => {
const dockerComposeValidation = spawn(`docker compose -f docker-compose.yml config --quiet`, {
cwd : this.folderAssignment,
shell: true
});
dockerComposeValidation.on('exit', code => {
code !== null && code === 0 ? resolve() : reject(code);
});
});
} catch ( error ) {
this.emitError(`Docker compose file structure is invalid.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_SCHEMA_ERROR);
throw new Error();
}
this.endSubStep('Docker compose file structure is valid', false);
// Validation of the containers and volumes named in dojo_assignment.json
this.newSubStep('DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content validation');
if ( !(this.assignmentFile.result.container in this.dockerComposeFile.services) ) {
this.emitError(`Container specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_CONTAINER_MISSING);
throw new Error();
}
if ( this.assignmentFile.result.volume && (!this.dockerComposeFile.volumes || !(this.assignmentFile.result.volume in this.dockerComposeFile.volumes)) ) {
this.emitError(`Volume specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_VOLUME_MISSING);
throw new Error();
}
this.endSubStep('Docker compose file content is valid', false);
this.endStep('Docker compose file is valid', false);
}
// Structure validation /**
this.newSubStep('ASSIGNMENT_FILE_SCHEMA_VALIDATION', 'Validating dojo_assignment.json file schema'); * Step 5: Dockerfiles validation
const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.folderAssignment, ClientsSharedConfig.assignment.filename)); * - Check if file exists
if ( !validationResults.isValid ) { * - TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint
this.emitError(`dojo_assignment.json file schema is invalid.\nHere are the errors:\n${ validationResults.error }`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.ASSIGNMENT_FILE_SCHEMA_ERROR); * @private
return; */
} private dockerfilesValidation() {
assignmentFile = validationResults.content!; this.newStep('DOCKERFILE_VALIDATION', 'Please wait while we are validating dockerfiles...');
this.endSubStep('dojo_assignment.json file schema is valid', false);
// Immutable files validation (Check if exists and if the given type is correct)
this.newSubStep('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Validating immutable files');
for ( const immutable of validationResults.content!.immutable ) {
const immutablePath = path.join(this.folderAssignment, immutable.path);
if ( !fs.existsSync(immutablePath) ) {
this.emitError(`Immutable path not found: ${ immutable.path }`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_NOT_FOUND);
return;
}
const isDirectory = fs.lstatSync(immutablePath).isDirectory();
if ( isDirectory && !immutable.isDirectory ) {
this.emitError(`Immutable (${ immutable.path }) is declared as a file but is a directory.`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_IS_NOT_DIRECTORY);
return;
} else if ( !isDirectory && immutable.isDirectory === true ) {
this.emitError(`Immutable (${ immutable.path }) is declared as a directory but is a file.`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_IS_DIRECTORY);
return;
}
}
this.endSubStep('Immutable files are valid', false);
this.endStep('dojo_assignment.json file is valid', false); this.newSubStep('DOCKERFILE_EXIST', 'Docker compose file content validation');
} const dockerfilesPaths = Object.values(this.dockerComposeFile.services).filter(service => service.build).map(service => path.join(this.folderAssignment, service.build!.context ?? '', service.build!.dockerfile!));
const filesNotFound = dockerfilesPaths.filter(dockerfilePath => !fs.existsSync(dockerfilePath));
if ( filesNotFound.length > 0 ) {
this.emitError(`Dockerfiles not found: ${ filesNotFound.join(', ') }`, 'Dockerfiles are invalid', AssignmentCheckerError.DOCKERFILE_NOT_FOUND);
throw new Error();
}
this.endSubStep('Docker compose file content is valid', false);
/* this.endStep('Dockerfiles are valid', false);
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 3: Docker Compose file validation }
- Global validation
- Validation of the containers and volumes named in dojo_assignment.json
*/
{
this.newStep('DOCKER_COMPOSE_VALIDATION', 'Please wait while we are validating docker compose file...');
/**
* Step 6: Sonar analysis
* - Analyse the project with SonarCube
* @private
*/
private async sonarAnalysis() {
if ( this.assignment.useSonar && this.runSonar ) {
this.newStep('ASSIGNMENT_SONAR', 'Please wait while we are running Sonar analysis on the assignment...');
this.newSubStep('SONAR_BUILD', 'Build files');
const buildSuccess = SonarAnalyzer.buildDocker();
if ( !buildSuccess ) {
this.emitError(`Build sonar image failed`, 'Sonar analysis failure', AssignmentCheckerError.SONAR_ANALYSIS_FAILED);
throw new Error();
}
// Global validation if ( SonarAnalyzer.mustRunBuild(this.assignment.language, this.assignmentFile.buildLine) ) {
this.newSubStep('DOCKER_COMPOSE_STRUCTURE_VALIDATION', 'Docker compose file structure validation'); const buildSuccess = SonarAnalyzer.runBuildStep(this.assignmentFile.buildLine as string);
try { if ( !buildSuccess ) {
dockerComposeFile = YAML.parse(fs.readFileSync(path.join(this.folderAssignment, 'docker-compose.yml'), 'utf8')) as DojoDockerCompose; this.emitError(`Failed to build files using buildLine`, 'Sonar analysis failure', AssignmentCheckerError.SONAR_ANALYSIS_FAILED);
} catch ( error ) { throw new Error();
this.emitError(`Docker compose file yaml structure is invalid.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_YAML_ERROR);
return;
} }
}
this.endSubStep('Sonar files build success', false);
try { this.newSubStep('SONAR_RUN', 'Run sonar analysis');
await new Promise<void>((resolve, reject) => {
const dockerComposeValidation = spawn(`docker compose -f docker-compose.yml config --quiet`, {
cwd : this.folderAssignment,
shell: true
});
dockerComposeValidation.on('exit', (code) => {
code !== null && code == 0 ? resolve() : reject();
});
});
} catch ( error ) {
this.emitError(`Docker compose file structure is invalid.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_SCHEMA_ERROR);
return;
}
this.endSubStep('Docker compose file structure is valid', false);
// Validation of the containers and volumes named in dojo_assignment.json const runSuccess = SonarAnalyzer.runAnalysis(this.assignment.sonarKey, this.assignment.language, this.assignmentFile.buildLine);
this.newSubStep('DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content validation'); if ( !runSuccess ) {
if ( !(assignmentFile.result.container in dockerComposeFile!.services) ) { if ( !this.assignment.allowSonarFailure ) {
this.emitError(`Container specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_CONTAINER_MISSING); this.emitError(`Sonar gate failed`, 'Sonar analysis failure', AssignmentCheckerError.SONAR_ANALYSIS_FAILED);
return;
}
if ( assignmentFile.result.volume && (!dockerComposeFile!.volumes || !(assignmentFile.result.volume in dockerComposeFile!.volumes)) ) {
this.emitError(`Volume specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_VOLUME_MISSING);
return; return;
} else {
this.endSubStep('Sonar gate failed, you should check the sonar project', false);
} }
this.endSubStep('Docker compose file content is valid', false); } else {
this.endSubStep('Sonar gate passed', false);
this.endStep('Docker compose file is valid', false);
} }
this.endStep('Sonar analysis finished', false);
}
}
/* /**
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 4: Dockerfiles validation * Step 7: Run
- Check if file exists * - Make a run of the assignment (If the return code is 0, the assignment is not valid because it means that there no need of modification for succeed the exercise)
- TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint * @private
*/ */
{ private async runAssignment() {
this.newStep('DOCKERFILE_VALIDATION', 'Please wait while we are validating dockerfiles...'); this.newStep('ASSIGNMENT_RUN', 'Please wait while we are running the assignment...');
this.newSubStep('DOCKERFILE_EXIST', 'Docker compose file content validation'); const exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, this.assignmentFile, this.folderAssignment);
const dockerfilesPaths = Object.values(dockerComposeFile!.services).filter((service) => service.build).map((service) => path.join(this.folderAssignment, service.build!.context ?? '', service.build!.dockerfile!));
const filesNotFound = dockerfilesPaths.filter((dockerfilePath) => !fs.existsSync(dockerfilePath));
if ( filesNotFound.length > 0 ) {
this.emitError(`Dockerfiles not found: ${ filesNotFound.join(', ') }`, 'Dockerfiles are invalid', AssignmentCheckerError.DOCKERFILE_NOT_FOUND);
return;
}
this.endSubStep('Docker compose file content is valid', false);
try {
await new Promise<void>((resolve, reject) => {
exerciseDockerCompose.events.on('logs', (log: string, error: boolean, displayable: boolean) => {
this.log(log, error, displayable);
});
this.endStep('Dockerfiles are valid', false); exerciseDockerCompose.events.on('step', (name: string, message: string) => {
} this.newSubStep(name, message);
});
exerciseDockerCompose.events.on('endStep', (_stepName: string, message: string, error: boolean) => {
this.endSubStep(message, error);
});
/* exerciseDockerCompose.events.on('finished', (_success: boolean, exitCode: number) => {
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 5: Run exitCode !== 0 ? resolve() : reject(exitCode);
- Make a run of the assignment (If the return code is 0, the assignment is not valid because it means that there no need of modification for succeed the exercise) });
*/
{
this.newStep('ASSIGNMENT_RUN', 'Please wait while we are running the assignment...');
exerciseDockerCompose.run(this.doDown);
});
} catch ( error ) {
this.fatalErrorMessage = 'Assignment is already solved';
this.endStep(this.fatalErrorMessage, true);
this.finished(false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY);
throw new Error();
}
const exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, assignmentFile, this.folderAssignment);
try { this.endStep('Assignment run successfully', false);
await new Promise<void>((resolve, reject) => { }
exerciseDockerCompose.events.on('logs', (log: string, error: boolean, displayable: boolean) => {
this.log(log, error, displayable);
});
exerciseDockerCompose.events.on('step', (name: string, message: string) => { run() {
this.newSubStep(name, message); (async () => {
}); try {
await this.checkRequirements();
exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => { await this.checkAssignment();
this.endSubStep(message, error);
});
exerciseDockerCompose.events.on('finished', (success: boolean, exitCode: number) => { this.dojoAssignmentFileValidation();
exitCode != 0 ? resolve() : reject();
});
exerciseDockerCompose.run(doDown); await this.dockerComposeFileValidation();
});
} catch ( error ) {
this.fatalErrorMessage = 'Assignment is already solved';
this.endStep(this.fatalErrorMessage, true);
this.finished(false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY);
return;
}
this.dockerfilesValidation();
this.endStep('Assignment run successfully', false); await this.sonarAnalysis();
}
await this.runAssignment();
this.finished(true, 0); this.finished(true, 0);
} catch ( error ) {
return;
}
})(); })();
} }
} }
......
import chalk from 'chalk'; import { existsSync, readFileSync } from 'fs';
import boxen from 'boxen'; import { join } from 'path';
import Icon from '../../../shared/types/Icon'; import chalk from 'chalk';
import AssignmentValidator from './AssignmentValidator'; import boxen from 'boxen';
import Icon from '../../../shared/types/Icon';
import AssignmentValidator from './AssignmentValidator';
import Assignment from '../../models/Assignment';
import axios from 'axios';
import DojoBackendResponse from '../../../shared/types/Dojo/DojoBackendResponse';
import ApiRoute from '../../types/Dojo/ApiRoute';
import ClientsSharedConfig from '../../config/ClientsSharedConfig';
class ClientsSharedAssignmentHelper { class ClientsSharedAssignmentHelper {
displayExecutionResults(validator: AssignmentValidator, successMessage: string, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }) { displayExecutionResults(validator: AssignmentValidator, successMessage: string, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }) {
const finalLogGlobalResult = `${ Style.INFO('Global result') } : ${ validator.success ? Style.SUCCESS(`${ Icon.SUCCESS } Success`) : Style.FAILURE(`${ Icon.FAILURE } Failure`) }`; const globalResult = validator.success ? Style.SUCCESS(`${ Icon.SUCCESS } Success`) : Style.FAILURE(`${ Icon.FAILURE } Failure`);
const finalLogGlobalResult = `${ Style.INFO('Global result') } : ${ globalResult }`;
const finalLogSuccessMessage = validator.success ? `${ successMessage }` : ''; const finalLogSuccessMessage = validator.success ? `${ successMessage }` : '';
const finalLogErrorMessage = !validator.success ? `${ Style.INFO('Error message') } :\n${ Style.FAILURE(validator.fatalErrorMessage) }` : ''; const finalLogErrorMessage = !validator.success ? `${ Style.INFO('Error message') } :\n${ Style.FAILURE(validator.fatalErrorMessage) }` : '';
...@@ -21,6 +29,34 @@ class ClientsSharedAssignmentHelper { ...@@ -21,6 +29,34 @@ class ClientsSharedAssignmentHelper {
textAlignment : 'left' textAlignment : 'left'
})); }));
} }
private async getAssignment(nameOrUrl: string): Promise<Assignment | undefined> {
try {
return (await axios.get<DojoBackendResponse<Assignment>>(`${ ClientsSharedConfig.apiURL }${ ApiRoute.ASSIGNMENT_GET }`.replace('{{nameOrUrl}}', encodeURIComponent(nameOrUrl)))).data.data;
} catch ( error ) {
console.log(error);
return undefined;
}
}
private async extractOriginUrl(content: string): Promise<string> {
const regexp = /\[remote "origin"]\r?\n\s*url\s*=\s*(.*)\s*\n/gm;
return Array.from(content.matchAll(regexp), m => m[1])[0];
}
async getAssignmentByName(name: string): Promise<Assignment | undefined> {
return await this.getAssignment(name);
}
async getAssignmentFromPath(path: string): Promise<Assignment | undefined> {
const fullPath = join(path, "./.git/config");
if (!existsSync(fullPath)) {
return undefined;
}
const content = readFileSync(fullPath, 'utf-8');
const url = await this.extractOriginUrl(content);
return await this.getAssignment(url);
}
} }
......
import ExerciseResultsFile from '../../../shared/types/Dojo/ExerciseResultsFile'; import ExerciseResultsFile from '../../../shared/types/Dojo/ExerciseResultsFile.js';
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import Icon from '../../../shared/types/Icon'; import Icon from '../../../shared/types/Icon.js';
class ClientsSharedExerciseHelper { class ClientsSharedExerciseHelper {
displayExecutionResults(exerciseResults: ExerciseResultsFile, containerExitCode: number, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }, additionalText: string = '') {
const finalLogGlobalResult = `${ Style.INFO('Global result: ') }${ exerciseResults.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) }`; private getOtherInformations(exerciseResults: ExerciseResultsFile, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }) {
return exerciseResults.otherInformations ? [ '', ...exerciseResults.otherInformations.map(information => {
const informationTitle = Style.INFO(`${ information.icon && information.icon !== '' ? Icon[information.icon] + ' ' : '' }${ information.name }: `);
const informationItems = typeof information.itemsOrInformations == 'string' ? information.itemsOrInformations : information.itemsOrInformations.map(item => `- ${ item }`).join('\n');
const finalLogResultNumbers = exerciseResults.successfulTests || exerciseResults.failedTests ? `\n\n${ Style.INFO(Style.SUCCESS('Tests passed: ')) }${ exerciseResults.successfulTests ?? '--' }\n${ Style.INFO(Style.FAILURE('Tests failed: ')) }${ exerciseResults.failedTests ?? '--' }` : ''; return `${ informationTitle }\n${ informationItems }`;
}) ].join('\n\n') : '';
}
const finalLogSuccessResultDetails = (exerciseResults.successfulTestsList ?? []).map(testName => `- ${ Icon.SUCCESS } ${ testName }`).join('\n'); displayExecutionResults(exerciseResults: ExerciseResultsFile, containerExitCode: number, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }, additionalText: string = '') {
const finalLogFailedResultDetails = (exerciseResults.failedTestsList ?? []).map(testName => `- ${ Icon.FAILURE } ${ testName }`).join('\n'); const globalResult = exerciseResults.success ? Style.SUCCESS(`${ Icon.SUCCESS } Success`) : Style.FAILURE(`${ Icon.FAILURE } Failure`);
const finalLogResultDetails = exerciseResults.successfulTestsList || exerciseResults.failedTestsList ? `\n\n${ Style.INFO('Tests: ') }${ finalLogSuccessResultDetails != '' ? '\n' + finalLogSuccessResultDetails : '' }${ finalLogFailedResultDetails != '' ? '\n' + finalLogFailedResultDetails : '' }` : ''; const finalLogGlobalResult = `${ Style.INFO('Global result: ') }${ globalResult }`;
const finalLogExecutionExitCode = `${ Style.INFO('Execution exit code: ') }${ (containerExitCode === 0 ? Style.SUCCESS : Style.FAILURE)(containerExitCode) }`;
let finalLogInformations = ''; const finalLogResultNumbers = exerciseResults.successfulTests || exerciseResults.failedTests ? `\n\n${ Style.INFO(Style.SUCCESS('Tests passed: ')) }${ exerciseResults.successfulTests ?? '--' }\n${ Style.INFO(Style.FAILURE('Tests failed: ')) }${ exerciseResults.failedTests ?? '--' }` : '';
if ( exerciseResults.otherInformations ) {
finalLogInformations = [ '', ...exerciseResults.otherInformations.map(information => {
const informationTitle = Style.INFO(`${ information.icon && information.icon != '' ? Icon[information.icon] + ' ' : '' }${ information.name }: `);
const informationItems = typeof information.itemsOrInformations == 'string' ? information.itemsOrInformations : information.itemsOrInformations.map(item => `- ${ item }`).join('\n');
return `${ informationTitle }\n${ informationItems }`; let finalLogSuccessResultDetails = (exerciseResults.successfulTestsList ?? []).map(testName => `- ${ Icon.SUCCESS } ${ testName }`).join('\n');
}) ].join('\n\n'); finalLogSuccessResultDetails = finalLogSuccessResultDetails !== '' ? '\n' + finalLogSuccessResultDetails : '';
} let finalLogFailedResultDetails = (exerciseResults.failedTestsList ?? []).map(testName => `- ${ Icon.FAILURE } ${ testName }`).join('\n');
finalLogFailedResultDetails = finalLogFailedResultDetails !== '' ? '\n' + finalLogFailedResultDetails : '';
const finalLogResultDetails = exerciseResults.successfulTestsList || exerciseResults.failedTestsList ? `\n\n${ Style.INFO('Tests: ') }${ finalLogSuccessResultDetails }${ finalLogFailedResultDetails }` : '';
const finalLogInformations = this.getOtherInformations(exerciseResults, Style);
console.log(boxen(`${ finalLogGlobalResult }\n\n${ finalLogExecutionExitCode }${ finalLogResultNumbers }${ finalLogResultDetails }${ finalLogInformations }${ additionalText }`, { console.log(boxen(`${ finalLogGlobalResult }\n\n${ finalLogExecutionExitCode }${ finalLogResultNumbers }${ finalLogResultDetails }${ finalLogInformations }${ additionalText }`, {
title : 'Results', title : 'Results',
......
import ApiRoute from '../../types/Dojo/ApiRoute.js';
import ClientsSharedConfig from '../../config/ClientsSharedConfig';
class DojoBackendHelper {
public getApiUrl(route: ApiRoute, options?: Partial<{ userId: number, assignmentNameOrUrl: string, exerciseIdOrUrl: string, gitlabProjectId: string, tagName: string }>): string {
let url = `${ ClientsSharedConfig.apiURL }${ route }`;
if ( options ) {
if ( options.userId ) {
url = url.replace('{{userId}}', encodeURIComponent(options.userId.toString()));
}
if ( options.assignmentNameOrUrl ) {
url = url.replace('{{assignmentNameOrUrl}}', encodeURIComponent(options.assignmentNameOrUrl));
}
if ( options.exerciseIdOrUrl ) {
url = url.replace('{{exerciseIdOrUrl}}', encodeURIComponent(options.exerciseIdOrUrl));
}
if ( options.gitlabProjectId ) {
url = url.replace('{{gitlabProjectId}}', encodeURIComponent(options.gitlabProjectId));
}
if ( options.tagName ) {
url = url.replace('{{tagName}}', encodeURIComponent(options.tagName));
}
}
return url;
}
}
export default new DojoBackendHelper();
\ No newline at end of file
import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile'; import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile.js';
import { TypedEmitter } from 'tiny-typed-emitter'; import { TypedEmitter } from 'tiny-typed-emitter';
import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents'; import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents.js';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError'; import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError.js';
import { ChildProcessWithoutNullStreams } from 'node:child_process'; import { ChildProcessWithoutNullStreams } from 'node:child_process';
...@@ -18,7 +18,17 @@ class ExerciseDockerCompose { ...@@ -18,7 +18,17 @@ class ExerciseDockerCompose {
private currentStep: string = 'NOT_RUNNING'; private currentStep: string = 'NOT_RUNNING';
constructor(private projectName: string, private assignmentFile: AssignmentFile, private executionFolder: string, private composeFileOverride: Array<string> = []) { private readonly projectName: string;
private readonly assignmentFile: AssignmentFile;
private readonly executionFolder: string;
private readonly composeFileOverride: Array<string> = [];
constructor(projectName: string, assignmentFile: AssignmentFile, executionFolder: string, composeFileOverride: Array<string> = []) {
this.projectName = projectName;
this.assignmentFile = assignmentFile;
this.executionFolder = executionFolder;
this.composeFileOverride = composeFileOverride;
this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => { this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
this.allLogs += log; this.allLogs += log;
this.displayableLogs += displayable ? log : ''; this.displayableLogs += displayable ? log : '';
...@@ -49,15 +59,17 @@ class ExerciseDockerCompose { ...@@ -49,15 +59,17 @@ class ExerciseDockerCompose {
} }
private registerChildProcess(childProcess: ChildProcessWithoutNullStreams, resolve: (value: (number | PromiseLike<number>)) => void, reject: (reason?: unknown) => void, displayable: boolean, rejectIfCodeIsNotZero: boolean) { private registerChildProcess(childProcess: ChildProcessWithoutNullStreams, resolve: (value: (number | PromiseLike<number>)) => void, reject: (reason?: unknown) => void, displayable: boolean, rejectIfCodeIsNotZero: boolean) {
childProcess.stdout.on('data', (data) => { childProcess.stdout.on('data', data => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-call
this.log(data.toString(), false, displayable); this.log(data.toString(), false, displayable);
}); });
childProcess.stderr.on('data', (data) => { childProcess.stderr.on('data', data => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-call
this.log(data.toString(), true, displayable); this.log(data.toString(), true, displayable);
}); });
childProcess.on('exit', (code) => { childProcess.on('exit', code => {
code === null || (rejectIfCodeIsNotZero && code !== 0) ? reject(code) : resolve(code); code === null || (rejectIfCodeIsNotZero && code !== 0) ? reject(code) : resolve(code);
}); });
} }
...@@ -66,7 +78,8 @@ class ExerciseDockerCompose { ...@@ -66,7 +78,8 @@ class ExerciseDockerCompose {
(async () => { (async () => {
let containerExitCode: number = -1; 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(' ') }`; const filesOverrideArguments = this.composeFileOverride.map(file => `--file "${ file }"`).join(' ');
const dockerComposeCommand = `docker compose --project-name ${ this.projectName } --progress plain --file docker-compose.yml ${ filesOverrideArguments }`;
// Run the service // Run the service
{ {
......
import { TypedEmitter } from 'tiny-typed-emitter'; import { TypedEmitter } from 'tiny-typed-emitter';
import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents'; import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents.js';
import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError'; import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError.js';
import path from 'node:path'; import path from 'node:path';
import ClientsSharedConfig from '../../config/ClientsSharedConfig'; import Toolbox from '../../../shared/helpers/Toolbox.js';
import Toolbox from '../../../shared/helpers/Toolbox';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import ExerciseResultsFile from '../../../shared/types/Dojo/ExerciseResultsFile'; import ExerciseResultsFile from '../../../shared/types/Dojo/ExerciseResultsFile.js';
import JSON5 from 'json5'; import JSON5 from 'json5';
import Json5FileValidator from '../../../shared/helpers/Json5FileValidator'; import Json5FileValidator from '../../../shared/helpers/Json5FileValidator.js';
import ClientsSharedConfig from '../../config/ClientsSharedConfig';
class ExerciseResultsSanitizerAndValidator { class ExerciseResultsSanitizerAndValidator {
...@@ -17,9 +17,17 @@ class ExerciseResultsSanitizerAndValidator { ...@@ -17,9 +17,17 @@ class ExerciseResultsSanitizerAndValidator {
private resultsFilePath: string = ''; private resultsFilePath: string = '';
constructor(private folderResultsDojo: string, private folderResultsExercise: string, private containerExitCode: number) { } private readonly folderResultsDojo: string;
private readonly folderResultsExercise: string;
private readonly containerExitCode: number;
constructor(folderResultsDojo: string, folderResultsExercise: string, containerExitCode: number) {
this.folderResultsDojo = folderResultsDojo;
this.folderResultsExercise = folderResultsExercise;
this.containerExitCode = containerExitCode;
}
private async resultsFileSanitization() { private resultsFileSanitization() {
this.events.emit('step', 'RESULTS_FILE_SANITIZATION', 'Sanitizing results file'); this.events.emit('step', 'RESULTS_FILE_SANITIZATION', 'Sanitizing results file');
if ( this.exerciseResults.success === undefined ) { if ( this.exerciseResults.success === undefined ) {
...@@ -33,11 +41,11 @@ class ExerciseResultsSanitizerAndValidator { ...@@ -33,11 +41,11 @@ class ExerciseResultsSanitizerAndValidator {
this.events.emit('endStep', 'RESULTS_FILE_SANITIZATION', 'Results file sanitized', false); this.events.emit('endStep', 'RESULTS_FILE_SANITIZATION', 'Results file sanitized', false);
} }
private async resultsFileProvided(path: string): Promise<boolean> { private async resultsFileProvided(resultFilePath: string): Promise<boolean> {
// Results file schema validation // Results file schema validation
{ {
this.events.emit('step', 'VALIDATE_RESULTS_FILE', 'Validating results file schema'); this.events.emit('step', 'VALIDATE_RESULTS_FILE', 'Validating results file schema');
const validationResults = Json5FileValidator.validateFile(ExerciseResultsFile, path); const validationResults = Json5FileValidator.validateFile(ExerciseResultsFile, resultFilePath);
if ( !validationResults.isValid ) { if ( !validationResults.isValid ) {
this.events.emit('endStep', 'VALIDATE_RESULTS_FILE', `Results file is not valid. Here are the errors :\n${ validationResults.error }`, true); this.events.emit('endStep', 'VALIDATE_RESULTS_FILE', `Results file is not valid. Here are the errors :\n${ validationResults.error }`, true);
this.events.emit('finished', false, ExerciseCheckerError.EXERCISE_RESULTS_FILE_SCHEMA_NOT_VALID); this.events.emit('finished', false, ExerciseCheckerError.EXERCISE_RESULTS_FILE_SCHEMA_NOT_VALID);
...@@ -49,7 +57,7 @@ class ExerciseResultsSanitizerAndValidator { ...@@ -49,7 +57,7 @@ class ExerciseResultsSanitizerAndValidator {
// Results file content sanitization // Results file content sanitization
await this.resultsFileSanitization(); this.resultsFileSanitization();
// Results folder size // Results folder size
...@@ -68,11 +76,6 @@ class ExerciseResultsSanitizerAndValidator { ...@@ -68,11 +76,6 @@ class ExerciseResultsSanitizerAndValidator {
return true; return true;
} }
private async resultsFileNotProvided(): Promise<boolean> {
await this.resultsFileSanitization();
return true;
}
run() { run() {
(async () => { (async () => {
// Results file existence // Results file existence
...@@ -80,7 +83,7 @@ class ExerciseResultsSanitizerAndValidator { ...@@ -80,7 +83,7 @@ class ExerciseResultsSanitizerAndValidator {
const resultsFileOriginPath = path.join(this.folderResultsExercise, ClientsSharedConfig.filenames.results); const resultsFileOriginPath = path.join(this.folderResultsExercise, ClientsSharedConfig.filenames.results);
this.resultsFilePath = path.join(this.folderResultsDojo, ClientsSharedConfig.filenames.results); this.resultsFilePath = path.join(this.folderResultsDojo, ClientsSharedConfig.filenames.results);
let result: boolean; let result: boolean = true;
if ( fs.existsSync(resultsFileOriginPath) ) { if ( fs.existsSync(resultsFileOriginPath) ) {
this.events.emit('endStep', 'CHECK_RESULTS_FILE_EXIST', 'Results file found', false); this.events.emit('endStep', 'CHECK_RESULTS_FILE_EXIST', 'Results file found', false);
...@@ -94,7 +97,7 @@ class ExerciseResultsSanitizerAndValidator { ...@@ -94,7 +97,7 @@ class ExerciseResultsSanitizerAndValidator {
} else { } else {
this.events.emit('endStep', 'CHECK_RESULTS_FILE_EXIST', 'Results file not found', false); this.events.emit('endStep', 'CHECK_RESULTS_FILE_EXIST', 'Results file not found', false);
result = await this.resultsFileNotProvided(); this.resultsFileSanitization();
} }
if ( result ) { if ( result ) {
......
import { spawnSync } from 'node:child_process';
import { Language } from '../../models/Assignment';
import SharedConfig from '../../../shared/config/SharedConfig';
const IMAGE_NAME = 'dojo-sonar-scanner'
const OUT_DIR = 'bw-output';
class SonarAnalyzer {
buildDocker = () => {
const buildProcess = spawnSync('docker', ['build', '--tag', IMAGE_NAME, '/sonar']);
if ( buildProcess.status !== 0 ) {
console.log(buildProcess.stdout.toString())
console.log(buildProcess.stderr.toString())
return false;
}
return true;
}
mustRunBuild = (language: Language, buildLine: string | undefined) => {
return [Language.c, Language.cpp, Language.objc].includes(language) && buildLine != undefined;
}
runBuildStep = (buildLine: string) => {
const process = spawnSync(`docker run -v ./:/usr/src ${IMAGE_NAME} /usr/local/bin/build-wrapper-linux-x86-64 --out-dir ${OUT_DIR} ` + buildLine, [], { shell: true })
if ( process.status !== 0 ) {
console.log(process.stdout.toString())
console.log(process.stderr.toString())
return false;
}
return true;
}
runAnalysis = (sonarKey: string, language: Language, buildLine: string | undefined) => {
let addParam: string[] = [];
if (this.mustRunBuild(language, buildLine)) {
addParam = [ `-Dsonar.cfamily.build-wrapper-output=/usr/src/${OUT_DIR}`];
}
const process = spawnSync(
'docker',
['run', '-v', './:/usr/src',
IMAGE_NAME , 'sonar-scanner',
'-Dsonar.qualitygate.wait=true',
'-Dsonar.projectKey=' + sonarKey,
'-Dsonar.sources=.',
'-Dsonar.host.url=' + SharedConfig.sonar.url,
'-Dsonar.login=' + SharedConfig.sonar.token,
...addParam])
return process.status === 0;
}
}
export default new SonarAnalyzer();
\ No newline at end of file
import GitlabRepository from '../../shared/types/Gitlab/GitlabRepository'; import User from './User.js';
import User from './User'; import Exercise from './Exercise.js';
import Exercise from './Exercise'; import * as Gitlab from '@gitbeaker/rest';
import SonarProjectCreation from '../../shared/types/Sonar/SonarProjectCreation';
interface Assignment { interface Assignment {
name: string; name: string;
gitlabId: number; gitlabId: number;
gitlabLink: string; gitlabLink: string;
gitlabCreationInfo: GitlabRepository; gitlabCreationInfo: Gitlab.ProjectSchema;
gitlabLastInfo: GitlabRepository; gitlabLastInfo: Gitlab.ProjectSchema;
gitlabLastInfoDate: string; gitlabLastInfoDate: string;
published: boolean; published: boolean;
useSonar: boolean;
allowSonarFailure: boolean;
sonarKey: string;
sonarCreationInfo: SonarProjectCreation;
language: Language;
staff: Array<User>; staff: Array<User>;
exercises: Array<Exercise>; exercises: Array<Exercise>;
...@@ -19,4 +25,74 @@ interface Assignment { ...@@ -19,4 +25,74 @@ interface Assignment {
} }
export default Assignment; export enum Language {
\ No newline at end of file abap = "abap",
ada = "ada",
asm = "asm",
bash = "bash",
bqn = "bqn",
c = "c",
caml = "caml",
cloudformation = "cloudformation",
cpp = "cpp",
csharp = "csharp",
css = "css",
cuda = "cuda",
dart = "dart",
delphi = "delphi",
docker = "docker",
erlang = "erlang",
f = "f",
fsharp = "fsharp",
flex = "flex",
fortran = "fortran",
futhark = "futhark",
go = "go",
groovy = "groovy",
haskell = "haskell",
hepial = "hepial",
json = "json",
jsp = "jsp",
java = "java",
js = "js",
julia = "julia",
kotlin = "kotlin",
kubernetes = "kubernetes",
latex = "latex",
lisp = "lisp",
lua = "lua",
matlab = "matlab",
objc = "objc",
ocaml = "ocaml",
pascal = "pascal",
pearl = "pearl",
perl = "perl",
php = "php",
postscript = "postscript",
powershell = "powershell",
prolog = "prolog",
promela = "promela",
python = "python",
r = "r",
ruby = "ruby",
rust = "rust",
scala = "scala",
sql = "sql",
smalltalk = "smalltalk",
swift = "swift",
terraform = "terraform",
text = "text",
ts = "ts",
tsql = "tsql",
typst = "typst",
vba = "vba",
vbnet = "vbnet",
web = "web",
xml = "xml",
yaml = "yaml",
other = "other"
}
export default Assignment;
import GitlabRepository from '../../shared/types/Gitlab/GitlabRepository'; import User from './User.js';
import { CommitSchema } from '@gitbeaker/rest'; import Assignment from './Assignment.js';
import User from './User'; import * as Gitlab from '@gitbeaker/rest';
import Assignment from './Assignment'; import SonarProjectCreation from '../../shared/types/Sonar/SonarProjectCreation';
interface Exercise { interface Exercise {
...@@ -10,15 +10,19 @@ interface Exercise { ...@@ -10,15 +10,19 @@ interface Exercise {
name: string; name: string;
gitlabId: number; gitlabId: number;
gitlabLink: string; gitlabLink: string;
gitlabCreationInfo: GitlabRepository; gitlabCreationInfo: Gitlab.ProjectSchema;
gitlabLastInfo: GitlabRepository; gitlabLastInfo: Gitlab.ProjectSchema;
gitlabLastInfoDate: string; gitlabLastInfoDate: string;
sonarKey: string;
sonarCreationInfo: SonarProjectCreation;
members: Array<User> | undefined; members: Array<User> | undefined;
assignment: Assignment | undefined; assignment: Assignment | undefined;
isCorrection: boolean; isCorrection: boolean;
correctionCommit: CommitSchema | undefined; correctionCommit: Gitlab.CommitSchema | undefined;
correctionDescription: string | undefined;
} }
......
import AssignmentFile from '../../shared/types/Dojo/AssignmentFile'; import AssignmentFile from '../../shared/types/Dojo/AssignmentFile.js';
import Assignment from './Assignment'; import Assignment from './Assignment.js';
import GitlabFile from '../../shared/types/Gitlab/GitlabFile'; import * as Gitlab from '@gitbeaker/rest';
interface ExerciseAssignment { interface ExerciseAssignment {
assignment: Assignment; assignment: Assignment;
assignmentFile: AssignmentFile; assignmentFile: AssignmentFile;
immutable: Array<GitlabFile>; immutable: Array<Gitlab.RepositoryFileSchema>;
} }
......
import { CommitSchema } from '@gitbeaker/rest';
import ExerciseResultsFile from '../../shared/types/Dojo/ExerciseResultsFile';
import { IFileDirStat } from '../../shared/helpers/recursiveFilesStats/RecursiveFilesStats';
export interface Result {
exerciseId: string;
dateTime: string;
commit: CommitSchema;
exitCode: number;
files: Array<IFileDirStat>;
results: ExerciseResultsFile;
success: boolean;
}
export default Result;
\ No newline at end of file
import { CommitSchema } from '@gitbeaker/rest';
import Assignment from './Assignment';
import Exercise from './Exercise';
interface Tags {
name: string;
type: 'Language' | 'Framework' | 'Theme' | 'UserDefined';
exercise: Exercise | undefined;
assignment: Assignment | undefined;
correctionCommit: CommitSchema | undefined;
}
export default Tags;
\ No newline at end of file
interface TagProposal {
name: string;
type: 'Language' | 'Framework' | 'Theme' | 'UserDefined';
state: 'PendingApproval' | 'Declined' | 'Approved';
details: string;
}
export default TagProposal;
import UserRole from './UserRole'; import UserRole from './UserRole.js';
import Exercise from './Exercise'; import Exercise from './Exercise.js';
import Assignment from './Assignment'; import Assignment from './Assignment.js';
import GitlabProfile from '../../shared/types/Gitlab/GitlabProfile'; import * as Gitlab from '@gitbeaker/rest';
interface User { interface User {
...@@ -10,7 +10,7 @@ interface User { ...@@ -10,7 +10,7 @@ interface User {
mail: string; mail: string;
role: UserRole; role: UserRole;
gitlabUsername: string; gitlabUsername: string;
gitlabLastInfo: GitlabProfile; gitlabLastInfo: Gitlab.ExpandedUserSchema;
isTeachingStaff: boolean; isTeachingStaff: boolean;
isAdmin: boolean; isAdmin: boolean;
deleted: boolean; deleted: boolean;
......
enum ApiRoute { enum ApiRoute {
LOGIN = '/login', CLIENTS_CONFIG = '/clients_config',
REFRESH_TOKENS = '/refresh_tokens', LOGIN = '/login',
TEST_SESSION = '/test_session', REFRESH_TOKENS = '/refresh_tokens',
GITLAB_CHECK_TEMPLATE_ACCESS = '/gitlab/project/{{id}}/checkTemplateAccess', TEST_SESSION = '/test_session',
ASSIGNMENT_GET = '/assignments/{{nameOrUrl}}', GITLAB_CHECK_TEMPLATE_ACCESS = '/gitlab/project/{{gitlabProjectId}}/checkTemplateAccess',
ASSIGNMENT_CREATE = '/assignments', ASSIGNMENT_GET = '/assignments/{{assignmentNameOrUrl}}',
ASSIGNMENT_PUBLISH = '/assignments/{{nameOrUrl}}/publish', ASSIGNMENT_CREATE = '/assignments',
ASSIGNMENT_UNPUBLISH = '/assignments/{{nameOrUrl}}/unpublish', ASSIGNMENT_PUBLISH = '/assignments/{{assignmentNameOrUrl}}/publish',
ASSIGNMENT_CORRECTION_LINK = '/assignments/{{assignmentNameOrUrl}}/corrections', ASSIGNMENT_UNPUBLISH = '/assignments/{{assignmentNameOrUrl}}/unpublish',
ASSIGNMENT_CORRECTION_UPDATE = '/assignments/{{assignmentNameOrUrl}}/corrections/{{exerciseIdOrUrl}}', ASSIGNMENT_CORRECTION_LINK = '/assignments/{{assignmentNameOrUrl}}/corrections',
EXERCISE_CREATE = '/assignments/{{nameOrUrl}}/exercises', ASSIGNMENT_CORRECTION_UPDATE_DELETE = '/assignments/{{assignmentNameOrUrl}}/corrections/{{exerciseIdOrUrl}}',
EXERCISE_ASSIGNMENT = '/exercises/{{id}}/assignment', EXERCISE_ASSIGNMENT = '/exercises/{{exerciseIdOrUrl}}/assignment',
EXERCISE_RESULTS = '/exercises/{{id}}/results' EXERCISE_CREATE = '/assignments/{{assignmentNameOrUrl}}/exercises',
EXERCISE_RESULTS = '/exercises/{{exerciseIdOrUrl}}/results',
EXERCISE_LIST = '/exercises',
EXERCISE_GET_DELETE = '/exercises/{{exerciseIdOrUrl}}',
EXERCISE_DETAILS_GET = '/exercises/{{exerciseIdOrUrl}}/details',
EXERCISE_MEMBERS_GET = '/exercises/{{exerciseIdOrUrl}}/members',
USER_EXERCISES_LIST = '/users/{{userId}}/exercises',
USER_LIST = '/users',
TAG_CREATE = '/tags',
TAG_DELETE = '/tags/{{tagName}}',
TAG_PROPOSAL_GET_CREATE = '/tags/proposals',
TAG_PROPOSAL_UPDATE = '/tags/proposals/{{tagName}}',
SONAR = '/sonar',
SONAR_QUALITIES = '/sonar/testqualities',
LANGUAGES = '/assignments/languages',
} }
......