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/nodesharedcode
1 result
Select Git revision
Show changes
Commits on Source (50)
Showing
with 260 additions and 204 deletions
import * as process from 'process';
class SharedConfig {
public readonly production: boolean;
public debug: boolean = false;
public readonly logsFolder: string;
public gitlab: {
URL: string, apiURL: string
};
public readonly login: {
gitlab: {
client: {
id: string
}, url: {
redirect: string, token: string
}
}
};
public sonar: {
enabled: boolean
url: string
token: string
}
constructor() {
this.production = process.env.NODE_ENV === 'production';
this.logsFolder = process.env.LOGS_FOLDER || '';
this.gitlab = {
URL : process.env.GITLAB_URL || '',
apiURL: process.env.GITLAB_API_URL || ''
this.sonar = {
enabled: ['yes', 'true', '1', 'on'].includes(process.env.SONAR_ENABLED?.trim()?.toLowerCase() ?? ''),
url: process.env.SONAR_URL || '',
token: process.env.SONAR_TOKEN || ''
};
this.login = {
gitlab: {
client: {
id: process.env.LOGIN_GITLAB_CLIENT_ID || ''
},
url : {
redirect: process.env.LOGIN_GITLAB_URL_REDIRECT || '',
token : process.env.LOGIN_GITLAB_URL_TOKEN || ''
}
}
};
this.logsFolder = process.env.LOGS_FOLDER ?? '';
}
}
......
......@@ -8,26 +8,26 @@ import zlib from 'zlib';
class ArchiveHelper {
private async explore(absoluteBasePath: string, rootPath: string, pack: tar.Pack) {
for ( let file of await fs.promises.readdir(rootPath) ) {
if ( file === 'output.tar' ) {
continue;
}
file = path.join(rootPath, file);
const stat = await fs.promises.stat(file);
if ( stat.isDirectory() ) {
await this.explore(absoluteBasePath, file, pack);
continue;
}
const entry = pack.entry({
name: file.replace(absoluteBasePath, ''),
size: stat.size
}, (err) => {
if ( err ) {
throw err;
for ( const file of await fs.promises.readdir(rootPath) ) {
let filename = file;
if ( filename !== 'output.tar' ) {
filename = path.join(rootPath, filename);
const stat = await fs.promises.stat(filename);
if ( stat.isDirectory() ) {
await this.explore(absoluteBasePath, filename, pack);
} else {
const entry = pack.entry({
name: filename.replace(absoluteBasePath, ''),
size: stat.size
}, err => {
if ( err ) {
throw err;
}
});
const readStream = fs.createReadStream(filename);
readStream.pipe(entry);
}
});
const stream = fs.createReadStream(file);
stream.pipe(entry);
}
}
}
......@@ -55,7 +55,7 @@ class ArchiveHelper {
await this.compress(folderPath, tarDataStream);
data = await (new Promise((resolve) => {
data = await (new Promise(resolve => {
tarDataStream.on('close', () => {
resolve(data);
});
......
import AssignmentFile from '../../types/Dojo/AssignmentFile';
import GitlabPipelineStatus from '../../types/Gitlab/GitlabPipelineStatus';
import DojoStatusCode from '../../types/Dojo/DojoStatusCode';
import GitlabPipeline from '../../types/Gitlab/GitlabPipeline';
import AssignmentFile from '../../types/Dojo/AssignmentFile.js';
import DojoStatusCode from '../../types/Dojo/DojoStatusCode.js';
import Json5FileValidator from '../Json5FileValidator.js';
import * as Gitlab from '@gitbeaker/rest';
import GitlabPipelineStatus from '../../types/Gitlab/GitlabPipelineStatus.js';
import SharedGitlabManager from '../../managers/SharedGitlabManager';
import Json5FileValidator from '../Json5FileValidator';
class SharedAssignmentHelper {
private gitlabManager!: SharedGitlabManager;
init(gitlabManager: SharedGitlabManager) {
this.gitlabManager = gitlabManager;
}
validateDescriptionFile(filePathOrStr: string, isFile: boolean = true, version: number = 1): { content: AssignmentFile | undefined, isValid: boolean, error: string | null } {
switch ( version ) {
case 1:
return Json5FileValidator.validateFile(AssignmentFile, filePathOrStr, isFile);
default:
return {
content: undefined,
isValid: false,
error : `Version ${ version } not supported`
};
if ( version === 1 ) {
return Json5FileValidator.validateFile(AssignmentFile, filePathOrStr, isFile);
} else {
return {
content: undefined,
isValid: false,
error : `Version ${ version } not supported`
};
}
}
async isPublishable(repositoryId: number): Promise<{ isPublishable: boolean, lastPipeline: GitlabPipeline | null, status?: { code: DojoStatusCode, message: string } }> {
const pipelines = await SharedGitlabManager.getRepositoryPipelines(repositoryId, 'main');
async isPublishable(repositoryId: number): Promise<{ isPublishable: boolean, lastPipeline: Gitlab.PipelineSchema | null, status?: { code: DojoStatusCode, message: string } }> {
const pipelines = await this.gitlabManager.getRepositoryPipelines(repositoryId, 'main');
if ( pipelines.length > 0 ) {
const lastPipeline = pipelines[0];
if ( lastPipeline.status != GitlabPipelineStatus.SUCCESS ) {
if ( lastPipeline.status !== GitlabPipelineStatus.SUCCESS.valueOf() ) {
return {
isPublishable: false,
lastPipeline : lastPipeline,
......
class SharedExerciseHelper {}
export default new SharedExerciseHelper();
\ No newline at end of file
class LazyVal<T> {
private val: T | undefined = undefined;
private readonly valLoader: () => Promise<T> | T;
constructor(private valLoader: () => Promise<T> | T) {}
constructor(valLoader: () => Promise<T> | T) {
this.valLoader = valLoader;
}
get value(): Promise<T> {
return new Promise<T>((resolve) => {
return new Promise<T>(resolve => {
if ( this.val === undefined ) {
Promise.resolve(this.valLoader()).then((value: T) => {
this.val = value;
......
......@@ -14,8 +14,9 @@ class Toolbox {
const files = await fs.readdir(dirPath);
await Promise.all(files.map(async file => {
if ( (await fs.stat(dirPath + '/' + file)).isDirectory() ) {
arrayOfFiles = await this.getAllFiles(dirPath + '/' + file, arrayOfFiles);
const filePath = path.join(dirPath, file);
if ( (await fs.stat(filePath)).isDirectory() ) {
arrayOfFiles = await this.getAllFiles(filePath, arrayOfFiles);
} else {
arrayOfFiles.push(path.join(dirPath, file));
}
......@@ -50,6 +51,10 @@ class Toolbox {
public getKeysWithPrefix(obj: object, prefix: string): Array<string> {
return Object.keys(obj).filter(key => key.startsWith(prefix));
}
public isString(value: unknown): value is string {
return typeof value === 'string' || value instanceof String;
}
}
......
......@@ -64,9 +64,7 @@ function registerStringCapitalizeName() {
function registerStringConvertWithEnvVars() {
String.prototype.convertWithEnvVars = function (this: string): string {
return this.replace(/\${?([a-zA-Z0-9_]+)}?/g, (_match: string, p1: string) => {
return process.env[p1] || '';
});
return this.replace(/\${?([a-zA-Z0-9_]+)}?/g, (_match: string, p1: string) => process.env[p1] || '');
};
}
......
......@@ -51,32 +51,36 @@ class RecursiveFilesStats {
return this.getFiles(`${ path.resolve(rootPath) }/`, rootPath, options, [], callback);
}
private async getFiles(absoluteBasePath: string, rootPath: string, options: RecursiveReaddirFilesOptions = {}, files: IFileDirStat[] = [], callback?: Callback): Promise<IFileDirStat[]> {
const {
ignored, include, exclude, filter
} = options;
private async getFilesDirsStat(rootPath: string, options: RecursiveReaddirFilesOptions): Promise<Array<IFileDirStat>> {
const filesData = await fs.promises.readdir(rootPath);
const fileDir: IFileDirStat[] = filesData.map((file) => ({
name: file, path: path.join(rootPath, file)
})).filter((item) => {
if ( include && include.test(item.path) ) {
return filesData.map(file => ({
name: file,
path: path.join(rootPath, file)
})).filter(item => {
if ( options.include && options.include.test(item.path) ) {
return true;
}
if ( exclude && exclude.test(item.path) ) {
if ( options.exclude && options.exclude.test(item.path) ) {
return false;
}
if ( ignored ) {
return !ignored.test(item.path);
if ( options.ignored ) {
return !options.ignored.test(item.path);
}
return true;
});
}
private async getFiles(absoluteBasePath: string, rootPath: string, options: RecursiveReaddirFilesOptions = {}, files: IFileDirStat[] = [], callback?: Callback): Promise<IFileDirStat[]> {
const fileDir: Array<IFileDirStat> = await this.getFilesDirsStat(rootPath, options);
if ( callback ) {
fileDir.map(async (item: IFileDirStat) => {
const stat = await this.getStat(item.path, absoluteBasePath, options);
if ( stat.isDirectory!() ) {
await this.getFiles(absoluteBasePath, item.path, options, [], callback);
}
callback(item.path, stat);
fileDir.forEach(item => {
this.getStat(item.path, absoluteBasePath, options).then(stat => {
if ( stat.isDirectory!() ) {
this.getFiles(absoluteBasePath, item.path, options, [], callback).then();
}
callback(item.path, stat);
});
});
} else {
await Promise.all(fileDir.map(async (item: IFileDirStat) => {
......@@ -89,9 +93,9 @@ class RecursiveFilesStats {
}
}));
}
return files.filter((item) => {
if ( filter && typeof filter === 'function' ) {
return filter(item);
return files.filter(item => {
if ( options.filter && typeof options.filter === 'function' ) {
return options.filter(item);
}
return true;
});
......@@ -124,10 +128,7 @@ class RecursiveFilesStats {
delete stat.ctimeMs;
delete stat.birthtimeMs;
delete stat.atime;
//delete stat.mtime;
delete stat.ctime;
//delete stat.birthtime;
//delete stat.mode;
}
return stat;
......
import winston from 'winston';
import SharedConfig from '../config/SharedConfig';
import SharedConfig from '../config/SharedConfig.js';
import * as Transport from 'winston-transport';
......@@ -23,7 +23,11 @@ winston.addColors(colors);
const format = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format(info => ({
...info,
level: info.level.toUpperCase()
}))(), SharedConfig.production ? winston.format.uncolorize() : winston.format.colorize({ all: true }), winston.format.prettyPrint(), winston.format.errors({ stack: true }), winston.format.align(), winston.format.printf((info) => `[${ info.timestamp }] (${ process.pid }) ${ info.level } ${ info.message } ${ info.metadata ? `\n${ JSON.stringify(info.metadata) }` : '' } ${ info.stack ? `\n${ info.stack }` : '' } `));
}))(), SharedConfig.production ? winston.format.uncolorize() : winston.format.colorize({ all: true }), winston.format.prettyPrint(), winston.format.errors({ stack: true }), winston.format.align(), winston.format.printf(info => {
const metadata = info.metadata ? `\n${ JSON.stringify(info.metadata) }` : '';
const stack = info.stack ? `\n${ JSON.stringify(info.stack, undefined, 4) }` : '';
return `[${ info.timestamp }] (${ process.pid }) ${ info.level } ${ info.message } ${ metadata } ${ stack } `;
}));
const commonTransportOptions = {
handleRejections: true,
......@@ -54,11 +58,11 @@ if ( SharedConfig.production ) {
}) ]);
}
const logger = winston.createLogger({
levels,
format,
transports,
exitOnError: false
});
const WinstonLogger = winston.createLogger({
levels,
format,
transports,
exitOnError: false
});
export default logger;
export default WinstonLogger;
import axios from 'axios';
import GitlabPipeline from '../types/Gitlab/GitlabPipeline';
import GitlabRoute from '../types/Gitlab/GitlabRoute';
import SharedConfig from '../config/SharedConfig';
import GitlabToken from '../types/Gitlab/GitlabToken';
import axios from 'axios';
import * as GitlabCore from '@gitbeaker/core';
import { GitbeakerRequestError } from '@gitbeaker/requester-utils';
import { Gitlab, PipelineSchema, ProjectSchema, SimpleUserSchema, UserSchema } from '@gitbeaker/rest';
import GitlabToken from '../types/Gitlab/GitlabToken.js';
class GitlabManager {
private getApiUrl(route: GitlabRoute): string {
return `${ SharedConfig.gitlab.apiURL }${ route }`;
class SharedGitlabManager {
protected api!: GitlabCore.Gitlab<false>;
private readonly refreshTokenFunction?: () => Promise<string>;
setToken(token: string) {
this.api = new Gitlab(Object.assign({
host: this.gitlabUrl ?? ''
}, this.refreshTokenFunction ? { oauthToken: token } : { token: token }));
}
constructor(public gitlabUrl: string, token: string, public clientId?: string, public urlRedirect?: string, public urlToken?: string, refreshTokenFunction?: () => Promise<string>) {
this.refreshTokenFunction = refreshTokenFunction;
this.setToken(token);
}
protected async executeGitlabRequest<T>(request: () => Promise<T>, refreshTokenIfNeeded: boolean = true): Promise<T> {
try {
return await request();
} catch ( error ) {
if ( this.refreshTokenFunction && refreshTokenIfNeeded && error instanceof GitbeakerRequestError ) {
this.setToken(await this.refreshTokenFunction());
return this.executeGitlabRequest(request, false);
} else {
throw error;
}
}
}
async getTokens(codeOrRefresh: string, isRefresh: boolean = false, clientSecret: string = ''): Promise<GitlabToken> {
const response = await axios.post<GitlabToken>(SharedConfig.login.gitlab.url.token, {
client_id : SharedConfig.login.gitlab.client.id,
if ( !this.urlToken ) {
throw new Error('Error when initializing GitlabManager');
}
const response = await axios.post<GitlabToken>(this.urlToken, {
client_id : this.clientId,
client_secret: clientSecret,
grant_type : isRefresh ? 'refresh_token' : 'authorization_code',
refresh_token: codeOrRefresh,
code : codeOrRefresh,
redirect_uri : SharedConfig.login.gitlab.url.redirect
redirect_uri : this.urlRedirect
});
return response.data;
}
async getRepositoryPipelines(repoId: number, branch: string = 'main'): Promise<Array<GitlabPipeline>> {
const response = await axios.get<Array<GitlabPipeline>>(this.getApiUrl(GitlabRoute.REPOSITORY_PIPELINES).replace('{{id}}', String(repoId)), {
params: {
ref: branch
}
});
public async getUserById(id: number): Promise<UserSchema | undefined> {
try {
return await this.executeGitlabRequest(async () => {
const user = await this.api.Users.show(id);
return response.data;
return user.id === id ? user : undefined;
});
} catch ( e ) {
return undefined;
}
}
public async getUserByUsername(username: string): Promise<SimpleUserSchema | undefined> {
try {
return await this.executeGitlabRequest(async () => {
const user = await this.api.Users.all({
username: username,
maxPages: 1,
perPage : 1
});
return user.length > 0 && user[0].username === username ? user[0] : undefined;
});
} catch ( e ) {
return undefined;
}
}
async getRepository(projectIdOrNamespace: string): Promise<ProjectSchema> {
return this.executeGitlabRequest(() => this.api.Projects.show(projectIdOrNamespace));
}
async getRepositoryPipelines(repoId: number, branch: string = 'main'): Promise<Array<PipelineSchema>> {
return this.executeGitlabRequest(() => this.api.Pipelines.all(repoId, { ref: branch }));
}
}
export default new GitlabManager();
export default SharedGitlabManager;
import axios from 'axios';
import https from 'https';
import SharedConfig from '../config/SharedConfig';
class SharedSonarManager {
// Use custom instance to allow self-signed certificates
private instance = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
});
async isSonarSupported(): Promise<boolean> {
if (!SharedConfig.sonar.enabled) {
return false;
}
try {
const sonar = await this.instance.get(SharedConfig.sonar.url);
return sonar.status == 200;
} catch ( error ) {
return false;
}
}
/**
* Map a language name to the equivalent language ID in Sonar
* Most language have the same name, so by default the same name is returned, even for languages that doesn't exist in sonar.
* @param language
*/
mapLanguage(language: string): string {
switch (language) {
case "csharp":
return "cs";
case "python":
return "py";
default:
return language;
}
}
}
export default new SharedSonarManager();
\ No newline at end of file
......@@ -11,6 +11,9 @@ enum ExerciseCheckerError {
COMPOSE_FILE_VOLUME_MISSING = 209,
DOCKERFILE_NOT_FOUND = 210,
COMPOSE_RUN_SUCCESSFULLY = 211, // Yes, this is an error
ASSIGNMENT_MISSING = 212,
BUILD_LINE_MISSING = 213,
SONAR_ANALYSIS_FAILED = 214
}
......
import ImmutableFileDescriptor from './ImmutableFileDescriptor';
import ImmutableFileDescriptor from './ImmutableFileDescriptor.js';
import { z } from 'zod';
const AssignmentFile = z.object({
dojoAssignmentVersion: z.number(),
version : z.number(),
immutable : z.array(ImmutableFileDescriptor.transform(value => value as ImmutableFileDescriptor)),
buildLine : z.string().optional(),
immutable : z.array(ImmutableFileDescriptor),
result : z.object({
container: z.string(),
volume : z.string().optional()
......
enum DojoStatusCode {
LOGIN_FAILED = 1,
REFRESH_TOKENS_FAILED = 2,
CLIENT_NOT_SUPPORTED = 100,
CLIENT_VERSION_NOT_SUPPORTED = 110,
ASSIGNMENT_PUBLISH_NO_PIPELINE = 200,
ASSIGNMENT_PUBLISH_PIPELINE_FAILED = 201,
ASSIGNMENT_CREATION_GITLAB_ERROR = 202,
ASSIGNMENT_CREATION_INTERNAL_ERROR = 203,
ASSIGNMENT_EXERCISE_NOT_RELATED = 204,
ASSIGNMENT_NOT_PUBLISHED = 205,
EXERCISE_CORRECTION_NOT_EXIST = 206,
EXERCISE_CORRECTION_ALREADY_EXIST = 207,
EXERCISE_CREATION_GITLAB_ERROR = 302,
EXERCISE_CREATION_INTERNAL_ERROR = 303,
MAX_EXERCISE_PER_ASSIGNMENT_REACHED = 304
LOGIN_FAILED = 10001,
REFRESH_TOKENS_FAILED = 10002,
CLIENT_NOT_SUPPORTED = 10100,
CLIENT_VERSION_NOT_SUPPORTED = 10110,
ASSIGNMENT_PUBLISH_NO_PIPELINE = 10200,
ASSIGNMENT_PUBLISH_PIPELINE_FAILED = 10201,
ASSIGNMENT_CREATION_GITLAB_ERROR = 10202,
ASSIGNMENT_CREATION_INTERNAL_ERROR = 10203,
ASSIGNMENT_EXERCISE_NOT_RELATED = 10204,
ASSIGNMENT_NOT_PUBLISHED = 10205,
EXERCISE_CORRECTION_NOT_EXIST = 10206,
EXERCISE_CORRECTION_ALREADY_EXIST = 10207,
ASSIGNMENT_NAME_CONFLICT = 10208,
EXERCISE_CREATION_GITLAB_ERROR = 10302,
EXERCISE_CREATION_INTERNAL_ERROR = 10303,
MAX_EXERCISE_PER_ASSIGNMENT_REACHED = 10304,
GITLAB_TEMPLATE_NOT_FOUND = 10401,
GITLAB_TEMPLATE_ACCESS_UNAUTHORIZED = 10402,
TAG_ONLY_ADMIN_CREATION = 11101,
TAG_WITH_ACTIVE_LINK_DELETION = 11102,
TAG_PROPOSAL_ANSWER_NOT_PENDING = 11103,
ASSIGNMENT_CREATION_SONAR_ERROR = 208,
ASSIGNMENT_CREATION_SONAR_MEMBER = 209,
EXERCISE_CREATION_SONAR_ERROR = 305,
ASSIGNMENT_SONAR_GATE_NOT_FOUND = 401,
ASSIGNMENT_SONAR_PROFILE_NOT_FOUND = 402
}
......
......@@ -6,7 +6,10 @@ enum ExerciseCheckerError {
EXERCISE_RESULTS_FOLDER_TOO_BIG = 204,
EXERCISE_RESULTS_FILE_SCHEMA_NOT_VALID = 206,
UPLOAD = 207,
DOCKER_COMPOSE_REMOVE_DANGLING_ERROR = 208
DOCKER_COMPOSE_REMOVE_DANGLING_ERROR = 208,
SONAR_DOCKER_ERROR = 209,
SONAR_BUILD_ERROR = 210,
SONAR_GATE_FAILED = 211
}
......
import Icon from '../Icon';
import Icon from '../Icon.js';
import { z } from 'zod';
......@@ -19,7 +19,7 @@ const ExerciseResultsFile = z.object({
icon : z.enum(Object.keys(Icon) as [ firstKey: string, ...otherKeys: Array<string> ]).optional(),
itemsOrInformations: z.union([ z.array(z.string()), z.string() ])
}))
.optional()
.optional()
}).strict().transform(value => {
if ( value.successfulTests === undefined && value.successfulTestsList !== undefined ) {
value.successfulTests = value.successfulTestsList.length;
......
enum GitlabAccessLevel {
GUEST = 10,
REPORTER = 20,
DEVELOPER = 30,
MAINTAINER = 40,
OWNER = 50,
ADMIN = 60
}
export default GitlabAccessLevel;
interface GitlabCommit {
id: string;
short_id: string;
created_at: string;
parent_ids: Array<string>;
title: string;
message: string;
author_name: string;
author_email: string;
authored_date: string;
committer_name: string;
committer_email: string;
committed_date: string;
}
export default GitlabCommit;
\ No newline at end of file
interface GitlabFile {
file_name: string,
file_path: string,
size: number,
encoding: string,
content_sha256: string,
ref: string,
blob_id: string,
commit_id: string,
last_commit_id: string,
execute_filemode: boolean,
content: string,
}
export default GitlabFile;
\ No newline at end of file
interface GitlabGroup {
group_id: number,
group_name: string,
group_full_path: string,
group_access_level: number,
expires_at: string
}
export default GitlabGroup;
\ No newline at end of file