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

Add Assignment validator

parent 50ef2e87
No related branches found
No related tags found
No related merge requests found
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;
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.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 if Docker daemon is running...');
// 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.events.emit('endStep', 'ASSIGNMENT_RUN', 'Assignment is already solved', true);
this.events.emit('finished', false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY);
}
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
interface AssignmentValidatorEvents {
step: (name: string, message: string) => void;
subStep: (name: string, message: string) => void;
endStep: (subStepName: 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment