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 (16)
...@@ -7,7 +7,8 @@ interface ClientsConfig { ...@@ -7,7 +7,8 @@ interface ClientsConfig {
gitlabUrl: string, gitlabUrl: string,
gitlabAccountId: number, gitlabAccountId: number,
gitlabAccountUsername: string, gitlabAccountUsername: string,
loginGitlabClientId: string loginGitlabClientId: string,
exerciseMaxPerAssignment: number
} }
...@@ -31,9 +32,12 @@ class ClientsSharedConfig { ...@@ -31,9 +32,12 @@ class ClientsSharedConfig {
} }
}; };
public assignment!: { public assignment!: {
filename: string, neededFiles: Array<string> filename: string, neededFiles: Array<string>, name: string, secret: string;
};
public exercise!: {
maxPerAssignment: number
}; };
public dockerCompose!: { public dockerCompose!: {
...@@ -97,6 +101,10 @@ class ClientsSharedConfig { ...@@ -97,6 +101,10 @@ class ClientsSharedConfig {
}; };
this.login.gitlab.client.id = downloadedConfig.loginGitlabClientId; this.login.gitlab.client.id = downloadedConfig.loginGitlabClientId;
this.exercise = {
maxPerAssignment: downloadedConfig.exerciseMaxPerAssignment
};
} }
async init(apiUrl: string) { async init(apiUrl: string) {
...@@ -112,7 +120,9 @@ class ClientsSharedConfig { ...@@ -112,7 +120,9 @@ class ClientsSharedConfig {
this.assignment = { this.assignment = {
filename : getEnvVar('ASSIGNMENT_FILENAME', ''), filename : getEnvVar('ASSIGNMENT_FILENAME', ''),
neededFiles: JSON.parse(getEnvVar('EXERCISE_NEEDED_FILES', '[]')) neededFiles: JSON.parse(getEnvVar('EXERCISE_NEEDED_FILES', '[]')),
name : process.env.DOJO_ASSIGNMENT_NAME || '',
secret : process.env.DOJO_ASSIGNMENT_SECRET || ''
}; };
this.dockerCompose = { this.dockerCompose = {
......
import { TypedEmitter } from 'tiny-typed-emitter'; import { TypedEmitter } from 'tiny-typed-emitter';
import AssignmentValidatorEvents from '../../types/Dojo/AssignmentValidatorEvents.js'; import AssignmentValidatorEvents from '../../types/Dojo/AssignmentValidatorEvents';
import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper.js'; import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper';
import path from 'node:path'; import path from 'node:path';
import AssignmentCheckerError from '../../../shared/types/Dojo/AssignmentCheckerError.js'; import AssignmentCheckerError from '../../../shared/types/Dojo/AssignmentCheckerError';
import fs from 'fs-extra'; import fs from 'fs-extra';
import YAML from 'yaml'; import ClientsSharedConfig from '../../config/ClientsSharedConfig';
import DojoDockerCompose from '../../types/Dojo/DojoDockerCompose.js'; import YAML from 'yaml';
import { exec, spawn } from 'child_process'; import DojoDockerCompose from '../../types/Dojo/DojoDockerCompose';
import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile.js'; import { exec, spawn } from 'child_process';
import ExerciseDockerCompose from './ExerciseDockerCompose.js'; import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile';
import util from 'util'; import ExerciseDockerCompose from './ExerciseDockerCompose';
import ClientsSharedConfig from '../../config/ClientsSharedConfig'; 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);
...@@ -19,6 +22,7 @@ const execAsync = util.promisify(exec); ...@@ -19,6 +22,7 @@ const execAsync = util.promisify(exec);
class AssignmentValidator { class AssignmentValidator {
private readonly folderAssignment: string; private readonly folderAssignment: string;
private readonly doDown: boolean; private readonly doDown: boolean;
private readonly runSonar: boolean;
readonly events: TypedEmitter<AssignmentValidatorEvents> = new TypedEmitter<AssignmentValidatorEvents>(); readonly events: TypedEmitter<AssignmentValidatorEvents> = new TypedEmitter<AssignmentValidatorEvents>();
...@@ -35,10 +39,12 @@ class AssignmentValidator { ...@@ -35,10 +39,12 @@ class AssignmentValidator {
private dockerComposeFile!: DojoDockerCompose; private dockerComposeFile!: DojoDockerCompose;
private assignmentFile!: AssignmentFile; private assignmentFile!: AssignmentFile;
private assignment!: Assignment;
constructor(folderAssignment: string, doDown: boolean = false) { constructor(folderAssignment: string, doDown: boolean = false, runSonar: boolean = false) {
this.folderAssignment = folderAssignment; this.folderAssignment = folderAssignment;
this.doDown = doDown; 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;
...@@ -123,9 +129,31 @@ class AssignmentValidator { ...@@ -123,9 +129,31 @@ class AssignmentValidator {
} }
/** /**
* Step 2: dojo_assignment.json file validation * 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('Assignment exists', false);
}
/**
* Step 3: dojo_assignment.json file validation
* - Structure validation * - Structure validation
* - Immutable files validation (Check if exists and if the given type is correct) * - 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
*/ */
private dojoAssignmentFileValidation() { private dojoAssignmentFileValidation() {
...@@ -164,12 +192,22 @@ class AssignmentValidator { ...@@ -164,12 +192,22 @@ class AssignmentValidator {
} }
this.endSubStep('Immutable files are valid', false); 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); this.endStep('dojo_assignment.json file is valid', false);
} }
/** /**
* Step 3: Docker Compose file validation * Step 4: Docker Compose file validation
* - Global validation * - Global validation
* - Validation of the containers and volumes named in dojo_assignment.json * - Validation of the containers and volumes named in dojo_assignment.json
* @private * @private
...@@ -223,7 +261,7 @@ class AssignmentValidator { ...@@ -223,7 +261,7 @@ class AssignmentValidator {
} }
/** /**
* Step 4: Dockerfiles validation * Step 5: Dockerfiles validation
* - Check if file exists * - Check if file exists
* - TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint * - TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint
* @private * @private
...@@ -246,7 +284,52 @@ class AssignmentValidator { ...@@ -246,7 +284,52 @@ class AssignmentValidator {
} }
/** /**
* Step 5: Run * 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();
}
if ( SonarAnalyzer.mustRunBuild(this.assignment.language, this.assignmentFile.buildLine) ) {
const buildSuccess = SonarAnalyzer.runBuildStep(this.assignmentFile.buildLine as string);
if ( !buildSuccess ) {
this.emitError(`Failed to build files using buildLine`, 'Sonar analysis failure', AssignmentCheckerError.SONAR_ANALYSIS_FAILED);
throw new Error();
}
}
this.endSubStep('Sonar files build success', false);
this.newSubStep('SONAR_RUN', 'Run sonar analysis');
const runSuccess = SonarAnalyzer.runAnalysis(this.assignment.sonarKey, this.assignment.language, this.assignmentFile.buildLine);
if ( !runSuccess ) {
if ( !this.assignment.allowSonarFailure ) {
this.emitError(`Sonar gate failed`, 'Sonar analysis failure', AssignmentCheckerError.SONAR_ANALYSIS_FAILED);
return;
} else {
this.endSubStep('Sonar gate failed, you should check the sonar project', false);
}
} else {
this.endSubStep('Sonar gate passed', false);
}
this.endStep('Sonar analysis finished', false);
}
}
/**
* Step 7: 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) * - 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)
* @private * @private
*/ */
...@@ -292,12 +375,16 @@ class AssignmentValidator { ...@@ -292,12 +375,16 @@ class AssignmentValidator {
try { try {
await this.checkRequirements(); await this.checkRequirements();
await this.checkAssignment();
this.dojoAssignmentFileValidation(); this.dojoAssignmentFileValidation();
await this.dockerComposeFileValidation(); await this.dockerComposeFileValidation();
this.dockerfilesValidation(); this.dockerfilesValidation();
await this.sonarAnalysis();
await this.runAssignment(); await this.runAssignment();
this.finished(true, 0); this.finished(true, 0);
......
import chalk from 'chalk'; import { existsSync, readFileSync } from 'fs';
import boxen from 'boxen'; import { join } from 'path';
import Icon from '../../../shared/types/Icon.js'; import chalk from 'chalk';
import AssignmentValidator from './AssignmentValidator.js'; 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 {
...@@ -22,6 +29,34 @@ class ClientsSharedAssignmentHelper { ...@@ -22,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);
}
} }
......
...@@ -3,10 +3,14 @@ import ClientsSharedConfig from '../../config/ClientsSharedConfig'; ...@@ -3,10 +3,14 @@ import ClientsSharedConfig from '../../config/ClientsSharedConfig';
class DojoBackendHelper { class DojoBackendHelper {
public getApiUrl(route: ApiRoute, options?: Partial<{ assignmentNameOrUrl: string, exerciseIdOrUrl: string, gitlabProjectId: string, tagName: string }>): string { public getApiUrl(route: ApiRoute, options?: Partial<{ userId: number, assignmentNameOrUrl: string, exerciseIdOrUrl: string, gitlabProjectId: string, tagName: string }>): string {
let url = `${ ClientsSharedConfig.apiURL }${ route }`; let url = `${ ClientsSharedConfig.apiURL }${ route }`;
if ( options ) { if ( options ) {
if ( options.userId ) {
url = url.replace('{{userId}}', encodeURIComponent(options.userId.toString()));
}
if ( options.assignmentNameOrUrl ) { if ( options.assignmentNameOrUrl ) {
url = url.replace('{{assignmentNameOrUrl}}', encodeURIComponent(options.assignmentNameOrUrl)); url = url.replace('{{assignmentNameOrUrl}}', encodeURIComponent(options.assignmentNameOrUrl));
} }
......
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): boolean => {
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 User from './User.js'; import User from './User.js';
import Exercise from './Exercise.js'; import Exercise from './Exercise.js';
import * as Gitlab from '@gitbeaker/rest'; import * as Gitlab from '@gitbeaker/rest';
import SonarProjectCreation from '../../shared/types/Sonar/SonarProjectCreation';
interface Assignment { interface Assignment {
...@@ -11,6 +12,11 @@ interface Assignment { ...@@ -11,6 +12,11 @@ interface Assignment {
gitlabLastInfo: Gitlab.ProjectSchema; 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 User from './User.js'; import User from './User.js';
import Assignment from './Assignment.js'; import Assignment from './Assignment.js';
import * as Gitlab from '@gitbeaker/rest'; import * as Gitlab from '@gitbeaker/rest';
import SonarProjectCreation from '../../shared/types/Sonar/SonarProjectCreation';
interface Exercise { interface Exercise {
...@@ -13,6 +14,9 @@ interface Exercise { ...@@ -13,6 +14,9 @@ interface Exercise {
gitlabLastInfo: Gitlab.ProjectSchema; 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;
......
...@@ -17,11 +17,15 @@ enum ApiRoute { ...@@ -17,11 +17,15 @@ enum ApiRoute {
EXERCISE_GET_DELETE = '/exercises/{{exerciseIdOrUrl}}', EXERCISE_GET_DELETE = '/exercises/{{exerciseIdOrUrl}}',
EXERCISE_DETAILS_GET = '/exercises/{{exerciseIdOrUrl}}/details', EXERCISE_DETAILS_GET = '/exercises/{{exerciseIdOrUrl}}/details',
EXERCISE_MEMBERS_GET = '/exercises/{{exerciseIdOrUrl}}/members', EXERCISE_MEMBERS_GET = '/exercises/{{exerciseIdOrUrl}}/members',
USER_EXERCISES_LIST = '/users/{{userId}}/exercises',
USER_LIST = '/users', USER_LIST = '/users',
TAG_CREATE = '/tags', TAG_CREATE = '/tags',
TAG_DELETE = '/tags/{{tagName}}', TAG_DELETE = '/tags/{{tagName}}',
TAG_PROPOSAL_GET_CREATE = '/tags/proposals', TAG_PROPOSAL_GET_CREATE = '/tags/proposals',
TAG_PROPOSAL_UPDATE = '/tags/proposals/{{tagName}}', TAG_PROPOSAL_UPDATE = '/tags/proposals/{{tagName}}',
SONAR = '/sonar',
SONAR_QUALITIES = '/sonar/testqualities',
LANGUAGES = '/assignments/languages',
} }
......