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 (9)
...@@ -13,6 +13,7 @@ These packages are needed : ...@@ -13,6 +13,7 @@ These packages are needed :
- `boxen@5.1` - `boxen@5.1`
- `chalk@4.1` - `chalk@4.1`
- `fs-extra` - `fs-extra`
- `yaml`
## How to use it ## How to use it
......
class ClientsSharedConfig { class ClientsSharedConfig {
public apiURL: string; public apiURL: string;
public assignment: {
filename: string, neededFiles: Array<string>
};
public gitlab: { public gitlab: {
apiURL: string
dojoAccount: { id: number; username: string; }; dojoAccount: { id: number; username: string; };
}; };
...@@ -20,8 +23,12 @@ class ClientsSharedConfig { ...@@ -20,8 +23,12 @@ class ClientsSharedConfig {
constructor() { constructor() {
this.apiURL = process.env.API_URL || ''; this.apiURL = process.env.API_URL || '';
this.assignment = {
filename : process.env.ASSIGNMENT_FILENAME || '',
neededFiles: JSON.parse(process.env.EXERCISE_NEEDED_FILES || '[]')
};
this.gitlab = { this.gitlab = {
apiURL : process.env.GITLAB_API_URL || '',
dojoAccount: { dojoAccount: {
id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID) || -1, id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID) || -1,
username: process.env.GITLAB_DOJO_ACCOUNT_USERNAME || '' username: process.env.GITLAB_DOJO_ACCOUNT_USERNAME || ''
......
import { TypedEmitter } from 'tiny-typed-emitter';
import AssignmentValidatorEvents from '../../types/Dojo/AssignmentValidatorEvents';
import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper';
import path from 'node:path';
import AssignmentCheckerError from '../../../shared/types/Dojo/AssignmentCheckerError';
import fs from 'fs-extra';
import JSON5 from 'json5';
import ClientsSharedConfig from '../../config/ClientsSharedConfig';
import YAML from 'yaml';
import DojoDockerCompose from '../../types/Dojo/DojoDockerCompose';
import { exec, spawn } from 'child_process';
import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile';
import ExerciseDockerCompose from './ExerciseDockerCompose';
import util from 'util';
const execAsync = util.promisify(exec);
class AssignmentValidator {
readonly events: TypedEmitter<AssignmentValidatorEvents> = new TypedEmitter<AssignmentValidatorEvents>();
public displayableLogs: string = '';
public allLogs: string = '';
public isFinished: boolean = false;
public success: boolean = false;
public exitCode: number = -1;
public fatalErrorMessage: string = '';
constructor(private folderAssignment: 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 dockerComposeFile: DojoDockerCompose;
let assignmentFile: AssignmentFile;
const emitError = (subStepName: string, subStepMessage: string, stepName: string, stepMessage: string, code: AssignmentCheckerError) => {
this.fatalErrorMessage = stepMessage;
this.events.emit('endSubStep', subStepName, subStepMessage, true);
this.events.emit('endStep', stepName, stepMessage, true);
this.events.emit('finished', false, code);
};
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1: Check requirements
- Check if Docker daemon is running
- Check if required files exists
*/
{
this.events.emit('step', 'REQUIREMENTS_CHECKING', 'Please wait while we are checking requirements...');
// Check requirements
this.events.emit('subStep', 'DOCKER_RUNNING', 'Checking if Docker daemon is running');
try {
await execAsync(`cd "${ this.folderAssignment }";docker ps`);
} catch ( error ) {
emitError('DOCKER_RUNNING', `Docker daemon isn't running`, 'REQUIREMENTS_CHECKING', `Some requirements are not satisfied.`, AssignmentCheckerError.DOCKER_DAEMON_NOT_RUNNING);
return;
}
this.events.emit('endSubStep', 'DOCKER_RUNNING', 'Docker daemon is running', false);
// Check if required files exists
this.events.emit('subStep', '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 ) {
emitError('REQUIRED_FILES_EXISTS', `The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`, 'REQUIREMENTS_CHECKING', 'Some requirements are not satisfied', AssignmentCheckerError.REQUIRED_FILES_MISSING);
return;
}
this.events.emit('endSubStep', 'REQUIRED_FILES_EXISTS', 'All required files exists', false);
this.events.emit('endStep', 'REQUIREMENTS_CHECKING', 'All requirements are satisfied', false);
}
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 2: dojo_assignment.json file validation
- Structure validation
- Immutable files validation (Check if exists and if the given type is correct)
*/
{
this.events.emit('step', 'ASSIGNMENT_FILE_VALIDATION', 'Please wait while we are validating dojo_assignment.json file...');
// Structure validation
this.events.emit('subStep', 'ASSIGNMENT_FILE_SCHEMA_VALIDATION', 'Validating dojo_assignment.json file schema');
const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.folderAssignment, ClientsSharedConfig.assignment.filename));
if ( !validationResults.isValid ) {
emitError('ASSIGNMENT_FILE_SCHEMA_VALIDATION', `dojo_assignment.json file schema is invalid.\nHere are the errors:\n${ JSON5.stringify(validationResults.errors) }`, 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is invalid', AssignmentCheckerError.ASSIGNMENT_FILE_SCHEMA_ERROR);
return;
}
assignmentFile = validationResults.results!;
this.events.emit('endSubStep', 'ASSIGNMENT_FILE_SCHEMA_VALIDATION', 'dojo_assignment.json file schema is valid', false);
// Immutable files validation (Check if exists and if the given type is correct)
this.events.emit('subStep', 'ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Validating immutable files');
for ( const immutable of validationResults.results!.immutable ) {
const immutablePath = path.join(this.folderAssignment, immutable.path);
if ( !fs.existsSync(immutablePath) ) {
emitError('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', `Immutable path not found: ${ immutable.path }`, 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_NOT_FOUND);
return;
}
const isDirectory = fs.lstatSync(immutablePath).isDirectory();
if ( isDirectory && !immutable.isDirectory ) {
emitError('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', `Immutable (${ immutable.path }) is declared as a file but is a directory.`, 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_IS_NOT_DIRECTORY);
return;
} else if ( !isDirectory && immutable.isDirectory === true ) {
emitError('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', `Immutable (${ immutable.path }) is declared as a directory but is a file.`, 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_IS_DIRECTORY);
return;
}
}
this.events.emit('endSubStep', 'ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Immutable files are valid', false);
this.events.emit('endStep', 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is valid', false);
}
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 3: Docker Compose file validation
- Global validation
- Validation of the containers and volumes named in dojo_assignment.json
*/
{
this.events.emit('step', 'DOCKER_COMPOSE_VALIDATION', 'Please wait while we are validating docker compose file...');
// Global validation
this.events.emit('subStep', 'DOCKER_COMPOSE_STRUCTURE_VALIDATION', 'Docker compose file structure validation');
try {
dockerComposeFile = YAML.parse(fs.readFileSync(path.join(this.folderAssignment, 'docker-compose.yml'), 'utf8')) as DojoDockerCompose;
} catch ( error ) {
emitError('DOCKER_COMPOSE_STRUCTURE_VALIDATION', `Docker compose file yaml structure is invalid.`, 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_YAML_ERROR);
return;
}
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();
});
});
} catch ( error ) {
emitError('DOCKER_COMPOSE_STRUCTURE_VALIDATION', `Docker compose file structure is invalid.`, 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_SCHEMA_ERROR);
return;
}
this.events.emit('endSubStep', 'DOCKER_COMPOSE_STRUCTURE_VALIDATION', 'Docker compose file structure is valid', false);
// Validation of the containers and volumes named in dojo_assignment.json
this.events.emit('subStep', 'DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content validation');
if ( !(assignmentFile.result.container in dockerComposeFile!.services) ) {
emitError('DOCKER_COMPOSE_CONTENT_VALIDATION', `Container specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_CONTAINER_MISSING);
return;
}
if ( assignmentFile.result.volume && (!dockerComposeFile!.volumes || !(assignmentFile.result.volume in dockerComposeFile!.volumes)) ) {
emitError('DOCKER_COMPOSE_CONTENT_VALIDATION', `Volume specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_VOLUME_MISSING);
return;
}
this.events.emit('endSubStep', 'DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content is valid', false);
this.events.emit('endStep', 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is valid', false);
}
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 4: Dockerfiles validation
- Check if file exists
- TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint
*/
{
this.events.emit('step', 'DOCKERFILE_VALIDATION', 'Please wait while we are validating dockerfiles...');
this.events.emit('subStep', 'DOCKERFILE_EXIST', 'Docker compose file content validation');
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 ) {
emitError('DOCKERFILE_VALIDATION', `Dockerfiles not found: ${ filesNotFound.join(', ') }`, 'DOCKERFILE_VALIDATION', 'Dockerfiles are invalid', AssignmentCheckerError.DOCKERFILE_NOT_FOUND);
return;
}
this.events.emit('endSubStep', 'DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content is valid', false);
this.events.emit('endStep', 'DOCKERFILE_VALIDATION', 'Dockerfiles are valid', false);
}
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 5: Run
- 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.events.emit('step', 'ASSIGNMENT_RUN', 'Please wait while we are running the assignment...');
const exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, assignmentFile, this.folderAssignment);
try {
await new Promise<void>((resolve, reject) => {
exerciseDockerCompose.events.on('logs', (log: string, error: boolean, displayable: boolean) => {
this.events.emit('logs', log, error, displayable);
});
exerciseDockerCompose.events.on('step', (name: string, message: string) => {
this.events.emit('subStep', name, message);
});
exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => {
this.events.emit('endSubStep', stepName, message, error);
});
exerciseDockerCompose.events.on('finished', (success: boolean, exitCode: number) => {
exitCode != 0 ? resolve() : reject();
});
exerciseDockerCompose.run(doDown);
});
} catch ( error ) {
this.fatalErrorMessage = 'Assignment is already solved';
this.events.emit('endStep', 'ASSIGNMENT_RUN', this.fatalErrorMessage, true);
this.events.emit('finished', false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY);
return;
}
this.events.emit('endStep', 'ASSIGNMENT_RUN', 'Assignment run successfully', false);
}
this.events.emit('finished', true, 0);
})();
}
}
export default AssignmentValidator;
\ No newline at end of file
import chalk from 'chalk';
import boxen from 'boxen';
import Icon from '../../types/Icon';
import AssignmentValidator from './AssignmentValidator';
class ClientsSharedAssignmentHelper {
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 finalLogSuccessMessage = validator.success ? `${ successMessage }` : '';
const finalLogErrorMessage = !validator.success ? `${ Style.INFO('Error message') } :\n${ Style.FAILURE(validator.fatalErrorMessage) }` : '';
console.log(boxen(`${ finalLogGlobalResult }\n\n${ finalLogSuccessMessage }${ finalLogErrorMessage }`, {
title : 'Results',
titleAlignment: 'center',
borderColor : validator.success ? 'green' : 'red',
borderStyle : 'bold',
margin : 1,
padding : 1,
textAlignment : 'left'
}));
}
}
export default new ClientsSharedAssignmentHelper();
\ No newline at end of file
interface AssignmentValidatorEvents {
step: (name: string, message: string) => void;
subStep: (name: string, message: string) => void;
endStep: (stepName: string, message: string, error: boolean) => void;
endSubStep: (subStepName: string, message: string, error: boolean) => void;
logs: (log: string, error: boolean, displayable: boolean) => void;
finished: (success: boolean, exitCode: number) => void;
}
export default AssignmentValidatorEvents;
\ No newline at end of file
interface DojoDockerCompose {
services: {
[serviceName: string]: Partial<{
container_name: string; image: string; build: Partial<{
context: string; dockerfile: string;
}>
}>
};
volumes?: {
[volumeName: string]: null
};
}
export default DojoDockerCompose;
\ No newline at end of file
enum Icon { enum Icon {
CAT_INFO = '▶️',
INFO = 'ℹ️', INFO = 'ℹ️',
ERROR = '⛔️', ERROR = '⛔️',
SUCCESS = '', SUCCESS = '',
......