diff --git a/deadlock-plugins/deadlock-extension/package.json b/deadlock-plugins/deadlock-extension/package.json index cfd3d4b0397ecd22584c5d33ef5604f3bc6630c0..02c85772cbab66fa6902ab75200e0c1d5abb55c7 100644 --- a/deadlock-plugins/deadlock-extension/package.json +++ b/deadlock-plugins/deadlock-extension/package.json @@ -112,29 +112,30 @@ "simple-git": "^3.7.0" }, "devDependencies": { - "@types/glob": "^7.2.0", - "@types/mocha": "^9.1.0", "@types/crypto-js": "^4.1.1", + "@types/express": "^4.17.13", + "@types/glob": "^7.2.0", "@types/marked": "^4.0.1", + "@types/mocha": "^9.1.0", "@types/node": "14.x", "@types/node-fetch": "^2.6.1", "@types/vscode": "^1.66.0", + "@typescript-eslint/eslint-plugin": "^3.10.1", + "@typescript-eslint/parser": "^3.10.1", + "@vscode/test-electron": "^2.1.3", "auto-changelog": "^2.4.0", "esbuild": "^0.14.2", + "eslint": "^7.32.0", + "glob": "^7.2.0", + "mocha": "^9.2.2", "prettier": "2.6.2", "terser-webpack-plugin": "^5.2.5", + "ts-loader": "^9.2.8", "ts-node": "^9.1.1", - "vsce": "^2.7.0", "typescript": "^3.9.10", - "ts-loader": "^9.2.8", + "vsce": "^2.7.0", "webpack": "^5.70.0", - "webpack-cli": "^4.9.2", - "@typescript-eslint/eslint-plugin": "^3.10.1", - "@typescript-eslint/parser": "^3.10.1", - "eslint": "^7.32.0", - "glob": "^7.2.0", - "mocha": "^9.2.2", - "@vscode/test-electron": "^2.1.3" + "webpack-cli": "^4.9.2" }, "extensionPack": [ "ms-vscode-remote.remote-containers" diff --git a/deadlock-plugins/deadlock-extension/src/core/commandHandler.ts b/deadlock-plugins/deadlock-extension/src/core/commandHandler.ts index bcaea34d75703232e460fd717821db4245b80f54..75f5518f904cb3848ddc01608e06a714724c3c8f 100644 --- a/deadlock-plugins/deadlock-extension/src/core/commandHandler.ts +++ b/deadlock-plugins/deadlock-extension/src/core/commandHandler.ts @@ -8,15 +8,15 @@ export class CommandHandler { initCommandHandler() { commands.registerCommand( - CHOOSE_MISSION_WORKDIR_COMMAND.cmd, + chooseMissionWorkdirCommand.cmd, this.controller.chooseMissionWorkdir.bind(this.controller), ); - commands.registerCommand(AUTHENTICATE_COMMAND.cmd, this.controller.authenticate.bind(this.controller)); - commands.registerCommand(CLEAR_COMMAND.cmd, this.controller.clear.bind(this.controller)); + commands.registerCommand(authenticateCommand.cmd, this.controller.authenticate.bind(this.controller)); + commands.registerCommand(clearCommand.cmd, this.controller.clear.bind(this.controller)); } } -export const CHOOSE_MISSION_WORKDIR_COMMAND = new Command('Choose mission workdir', 'deadlock.chooseMissionWorkdir'); -export const AUTHENTICATE_COMMAND = new Command('Authenticate', 'deadlock.authenticate'); -export const CLEAR_COMMAND = new Command('Clear', 'deadlock.clear'); -export const OPEN_URL_IN_BROWSER_COMMAND = new Command('Open url in browser', 'vscode.open'); +export const chooseMissionWorkdirCommand = new Command('Choose mission workdir', 'deadlock.chooseMissionWorkdir'); +export const authenticateCommand = new Command('Authenticate', 'deadlock.authenticate'); +export const clearCommand = new Command('Clear', 'deadlock.clear'); +export const openUrlInBrowserCommand = new Command('Open url in browser', 'vscode.open'); diff --git a/deadlock-plugins/deadlock-extension/src/core/controller.ts b/deadlock-plugins/deadlock-extension/src/core/controller.ts index 00d6a14d1ecc6a766f4dd6d4990317cb2d780b98..574c7c7df9c3a82800727144523f8fe71827d066 100644 --- a/deadlock-plugins/deadlock-extension/src/core/controller.ts +++ b/deadlock-plugins/deadlock-extension/src/core/controller.ts @@ -3,7 +3,7 @@ import { KEYCLOAK_DEVICE_AUTH_URL, KEYCLOAK_TOKEN_CREATE_URL, KEYCLOAK_USER_INFO import { log } from '../recorder/utils'; import BriefingView from '../view/briefingView'; import QuickSetupView from '../view/quickSetupView'; -import { CHOOSE_MISSION_WORKDIR_COMMAND, CommandHandler, OPEN_URL_IN_BROWSER_COMMAND } from './commandHandler'; +import { chooseMissionWorkdirCommand, CommandHandler, openUrlInBrowserCommand } from './commandHandler'; import ExtensionStore from './extensionStore'; import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; import ApiService from './api.service'; @@ -108,14 +108,14 @@ export default class Controller { } public static openBrowserWithUrl(url: string) { - vscode.commands.executeCommand(OPEN_URL_IN_BROWSER_COMMAND.cmd, vscode.Uri.parse(url)); + vscode.commands.executeCommand(openUrlInBrowserCommand.cmd, vscode.Uri.parse(url)); } public async launchMission(missionId: string, missionVersion: string) { vscode.window.showInformationMessage(`vous lancez la mission ${missionId}`); const hadMissionWorkdir = this.extensionStore.getMissionWorkdir() !== undefined; if (!hadMissionWorkdir) { - await vscode.commands.executeCommand(CHOOSE_MISSION_WORKDIR_COMMAND.cmd); + await vscode.commands.executeCommand(chooseMissionWorkdirCommand.cmd); } const hadBeenConnected = (await this.extensionStore.getAccessToken()) !== undefined; diff --git a/deadlock-plugins/deadlock-extension/src/core/gitMission.ts b/deadlock-plugins/deadlock-extension/src/core/gitMission.ts index 3bed6fd3f7d48a2dfcfa785cbf38a84cc76af3d0..057592405b2efe545070fa9eac337b9ef3e127e5 100644 --- a/deadlock-plugins/deadlock-extension/src/core/gitMission.ts +++ b/deadlock-plugins/deadlock-extension/src/core/gitMission.ts @@ -6,7 +6,7 @@ import UserConfig from './userConfig'; const util = require('util'); const exec = util.promisify(require('child_process').exec); -const DEFAULT_REMOTE = 'origin'; +const defaultRemote = 'origin'; export enum Branch { MASTER = 'master', @@ -53,7 +53,7 @@ export default class GitMission { const remote = await this.readRemote(); - if (remote === DEFAULT_REMOTE) { + if (remote === defaultRemote) { return Promise.resolve(this); } @@ -61,7 +61,7 @@ export default class GitMission { await this.git.init(); - await this.git.addRemote(DEFAULT_REMOTE, remotePath); + await this.git.addRemote(defaultRemote, remotePath); await this.git.addConfig('user.email', this.userConfig.getCurrentUserDetails().email, false, 'system'); await this.git.addConfig( 'user.name', @@ -91,11 +91,11 @@ export default class GitMission { } fetch() { - return this.git.fetch(DEFAULT_REMOTE); + return this.git.fetch(defaultRemote); } pull(branch?: string) { - return this.git.pull(DEFAULT_REMOTE, branch ?? Branch.DEFAULT, { '--rebase': 'true' }); + return this.git.pull(defaultRemote, branch ?? Branch.DEFAULT, { '--rebase': 'true' }); } addAll() { @@ -107,7 +107,7 @@ export default class GitMission { } push() { - return this.git.push(DEFAULT_REMOTE); + return this.git.push(defaultRemote); } createLocalBranch(branch: string) { @@ -115,11 +115,11 @@ export default class GitMission { } createRemoteBranch(branch: string) { - return this.git.push(['-u', DEFAULT_REMOTE, branch]); + return this.git.push(['-u', defaultRemote, branch]); } setUpstream(branch: string) { - return this.git.branch(['-u', DEFAULT_REMOTE, branch]); + return this.git.branch(['-u', defaultRemote, branch]); } createBranch(branch: string) { diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts index a8dc306c1de9a674287cc0974451379e8275a12e..e40df49c8b2c01559bbb84d06d5ef0bc5689aebd 100644 --- a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts +++ b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts @@ -1,17 +1,23 @@ import { userSshKeyFolderPath } from '../config'; import { Base, DockerfileSpecific, LifecycleScripts, VSCodespecific } from './devContainer'; -import { existsSync } from 'fs'; import { mkdir, writeFile } from 'fs/promises'; import { Mission } from './mission'; import { User, UserChallengeJson } from '../../model/user.model'; import { GiteaPublicProperties } from '../../model/giteaPublicProperties.model'; import { commands, Uri } from 'vscode'; +import { existsSync, copyFileSync } from 'fs'; +import { join } from 'path'; + import assert = require('assert'); -const DOCKER_IMAGE_URL = 'registry.takima.io/deadlock/deadlock-challenges'; +const remoteUserHomeDir = '/home/deadlock'; +const remoteMissionDir = `${remoteUserHomeDir}/mission/`; export class MissionDevContainer { private isInit = false; + private mounts: string[] = []; + private readonly dockerImageUrl = 'registry.takima.io/deadlock/deadlock-challenges'; + private readonly remoteGiteaWorkDir = `/workdir`; private readonly dirs = { missionWorkdir: `${this.missionsWorkdir}/${this.mission.id}`, @@ -64,13 +70,32 @@ export class MissionDevContainer { ); } + private addInMountRCs(tempFolder: string, ...rcs: string[]): void { + if (process.env.HOME === undefined) { + throw new Error('HOME environment variable is not defined'); + } + for (const rc of rcs) { + if (existsSync(rc)) { + const rcPath = join(process.env.HOME, rc); + const tempRC = join(tempFolder, rc); + const mountRC = join(remoteUserHomeDir, rc); + copyFileSync(rcPath, tempRC); + this.mounts.push(`source=${tempRC},target=${mountRC},type=bind,consistency=cached,readonly`); + } + } + } + private createDevContainerFile(options?: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts>) { - const remoteUserHomeDir = '/home/deadlock'; - const remoteMissionDir = `${remoteUserHomeDir}/mission/`; - const remoteGiteaWorkDir = `/workdir`; const hostMissionDevcontainerFileDir = `${this.dirs.devcontainer}/devcontainer.json`; - const image = `${DOCKER_IMAGE_URL}/${this.mission.id}:${this.mission.version}`; + const image = `${this.dockerImageUrl}/${this.mission.id}:${this.mission.version}`; + this.mounts.push( + `source=${userSshKeyFolderPath},target=/tmp/.ssh,type=bind,consistency=cached,readonly`, + `source=${this.dirs.config},target=/home/config/,type=bind,consistency=cached,readonly`, + 'source=/etc/hosts,target=/etc/hosts,type=bind,consistency=cached,readonly', + ); + + this.addInMountRCs(this.dirs.missionWorkdir, '.bashrc', '.zshrc'); return writeFile( hostMissionDevcontainerFileDir, @@ -80,11 +105,7 @@ export class MissionDevContainer { image, extensions: ['Deadlock.deadlock-coding'], remoteUser: 'deadlock', - mounts: [ - `source=${userSshKeyFolderPath},target=/tmp/.ssh,type=bind,consistency=cached,readonly`, - `source=${this.dirs.config},target=/home/config/,type=bind,consistency=cached,readonly`, - 'source=/etc/hosts,target=/etc/hosts,type=bind,consistency=cached,readonly', - ], + mounts: this.mounts, userEnvProbe: 'interactiveShell', settings: { 'terminal.integrated.defaultProfile.linux': 'bash', @@ -98,7 +119,7 @@ export class MissionDevContainer { shutdownAction: 'stopContainer', workspaceMount: `source=${this.dirs.mounted},target=${remoteMissionDir},type=bind`, workspaceFolder: `${remoteMissionDir}`, - onCreateCommand: `cp -R ${remoteGiteaWorkDir}/* ${remoteMissionDir}`, + onCreateCommand: `cp -R ${this.remoteGiteaWorkDir}/* ${remoteMissionDir}`, runArgs: ['--privileged'], ...options, }; diff --git a/deadlock-plugins/deadlock-extension/src/core/sshKeyManager.ts b/deadlock-plugins/deadlock-extension/src/core/sshKeyManager.ts index 94d37e732cb725b21a9faf2c0213c25584e769ba..65340b05dfd080e13a5b30b3df033b928fe15a3e 100644 --- a/deadlock-plugins/deadlock-extension/src/core/sshKeyManager.ts +++ b/deadlock-plugins/deadlock-extension/src/core/sshKeyManager.ts @@ -1,31 +1,32 @@ -import * as fs from 'fs'; +import { existsSync, promises } from 'fs'; import { userSshKeyFolderPath } from './config'; -export function isSshKeyPairExist(): boolean { - return isPrivateKeyExist() && isPublicKeyExist(); -} - -function isPublicKeyExist(): boolean { - return fs.existsSync(`${userSshKeyFolderPath}/id_rsa.pub`); -} +export default class SshKeyManager { + public isSshKeyPairExist(): boolean { + return this.isPrivateKeyExist() && this.isPublicKeyExist(); + } -function isPrivateKeyExist(): boolean { - return fs.existsSync(`${userSshKeyFolderPath}/id_rsa`); -} + private isPublicKeyExist(): boolean { + return existsSync(`${userSshKeyFolderPath}/id_rsa.pub`); + } -export async function createSshKeyFiles(publicKey: string, privateKey: string) { - await createSshKeyFolderIfNotExist(userSshKeyFolderPath); - await fs.promises.writeFile(`${userSshKeyFolderPath}/id_rsa.pub`, publicKey); + private isPrivateKeyExist(): boolean { + return existsSync(`${userSshKeyFolderPath}/id_rsa`); + } - await fs.promises.writeFile(`${userSshKeyFolderPath}/id_rsa`, privateKey, { mode: 0o600 }); -} + public async createSshKeyFiles(publicKey: string, privateKey: string) { + await this.createSshKeyFolderIfNotExist(userSshKeyFolderPath); + await promises.writeFile(`${userSshKeyFolderPath}/id_rsa.pub`, publicKey); -async function createSshKeyFolderIfNotExist(sshKeyFolderPath) { - if (!isSshKeyFolderExist(sshKeyFolderPath)) { - await fs.promises.mkdir(sshKeyFolderPath, { recursive: true }); + await promises.writeFile(`${userSshKeyFolderPath}/id_rsa`, privateKey, { mode: 0o600 }); + } + private async createSshKeyFolderIfNotExist(sshKeyFolderPath) { + if (!this.isSshKeyFolderExist(sshKeyFolderPath)) { + await promises.mkdir(sshKeyFolderPath, { recursive: true }); + } } -} -export function isSshKeyFolderExist(sshKeyFolderPath: string) { - return fs.existsSync(sshKeyFolderPath); + private isSshKeyFolderExist(sshKeyFolderPath: string) { + return existsSync(sshKeyFolderPath); + } } diff --git a/deadlock-plugins/deadlock-extension/src/recorder/command-recorder.ts b/deadlock-plugins/deadlock-extension/src/recorder/command-recorder.ts index 1a68a0b8ea208688101e267ae24b329a31885af6..b2c167c465b02147f70380e97e8efa0030536d27 100644 --- a/deadlock-plugins/deadlock-extension/src/recorder/command-recorder.ts +++ b/deadlock-plugins/deadlock-extension/src/recorder/command-recorder.ts @@ -1,7 +1,7 @@ import GitMission from '../core/gitMission'; import { CommitFrom, error, updateRemote } from './utils'; -import * as async from 'async'; -import * as fs from 'fs'; +import { queue } from 'async'; +import { readFileSync } from 'fs'; class Command { public still: boolean; @@ -20,11 +20,11 @@ export default class CommandRecorder { constructor(private gitMission: GitMission) { // create a queue object with concurrency 1 - this.queue = async.queue(async function (task) { + this.queue = queue(async function (task) { await task.apply(); }, 1); - this.queue.error((err, _task) => { + this.queue.error((err) => { error('Error on task: ', err); }); } @@ -47,7 +47,7 @@ export default class CommandRecorder { let lastLineIndexWatched = 0; setInterval(() => { try { - const trace = fs.readFileSync('/home/deadlock/.bash_history', 'utf8'); + const trace = readFileSync('/home/deadlock/.bash_history', 'utf8'); const lines = trace.split(/\r?\n/); this.commandsInProgress.forEach((command) => (command.still = false)); diff --git a/deadlock-plugins/deadlock-extension/src/theia/command.ts b/deadlock-plugins/deadlock-extension/src/theia/command.ts index d8fd787c7e2289c7ee84215df89509b5ba0f19f4..aa88aad0bee4350989d4ac73cd0be5dedc93dafb 100644 --- a/deadlock-plugins/deadlock-extension/src/theia/command.ts +++ b/deadlock-plugins/deadlock-extension/src/theia/command.ts @@ -16,5 +16,5 @@ export class Command { } } -export const OPEN_BRIEFING_COMMAND = new Command('Open Briefing', 'deadlock.openBriefing'); -export const OPEN_QUICK_SETUP_COMMAND = new Command('Open Deadlock quick setup page', 'deadlock.openQuickSetup'); +export const openBriefingCommand = new Command('Open Briefing', 'deadlock.openBriefing'); +export const openQuickSetupCommand = new Command('Open Deadlock quick setup page', 'deadlock.openQuickSetup'); diff --git a/deadlock-plugins/deadlock-extension/src/theia/deadlockPanel.ts b/deadlock-plugins/deadlock-extension/src/theia/deadlockPanel.ts index 80ba03a989b88d6dea66723c732b19644c0514b8..ff52b5732aa9b32013a4861a8c420c8e166a8e0d 100644 --- a/deadlock-plugins/deadlock-extension/src/theia/deadlockPanel.ts +++ b/deadlock-plugins/deadlock-extension/src/theia/deadlockPanel.ts @@ -1,9 +1,9 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { TreeItemCollapsibleState } from 'vscode'; -import { OPEN_BRIEFING_COMMAND } from './command'; +import { openBriefingCommand } from './command'; -const DOCUMENTATION_LABEL = 'Documentation'; +const documentationLabel = 'Documentation'; export class DepNodeProvider implements vscode.TreeDataProvider<Action> { private _onDidChangeTreeData: vscode.EventEmitter<Action | undefined | void> = new vscode.EventEmitter< @@ -19,7 +19,7 @@ export class DepNodeProvider implements vscode.TreeDataProvider<Action> { } private root(): Action[] { - return [new Action(DOCUMENTATION_LABEL, TreeItemCollapsibleState.Collapsed, 'folder')]; + return [new Action(documentationLabel, TreeItemCollapsibleState.Collapsed, 'folder')]; } getTreeItem(element: Action): vscode.TreeItem { @@ -29,9 +29,9 @@ export class DepNodeProvider implements vscode.TreeDataProvider<Action> { getChildren(element?: Action): Thenable<Action[]> { if (!element) { return Promise.resolve(this.root()); - } else if (element.label === DOCUMENTATION_LABEL) { + } else if (element.label === documentationLabel) { return Promise.resolve([ - new Action('Briefing', TreeItemCollapsibleState.None, 'document', OPEN_BRIEFING_COMMAND.asVsCodeCommand()), + new Action('Briefing', TreeItemCollapsibleState.None, 'document', openBriefingCommand.asVsCodeCommand()), ]); } diff --git a/deadlock-plugins/deadlock-extension/src/view/briefingView.ts b/deadlock-plugins/deadlock-extension/src/view/briefingView.ts index d7c9141e5e929d5536805faa56f26630044e6f67..b044e9570a5243b6b7a6c2b70dbbc76b0856c550 100644 --- a/deadlock-plugins/deadlock-extension/src/view/briefingView.ts +++ b/deadlock-plugins/deadlock-extension/src/view/briefingView.ts @@ -1,15 +1,15 @@ -import * as fs from 'fs'; +import { readFileSync } from 'fs'; import isDocker from 'is-docker'; import { marked } from 'marked'; -import * as path from 'path'; +import { join } from 'path'; import { setInterval } from 'timers'; -import * as vscode from 'vscode'; +import { Uri, workspace } from 'vscode'; import { BRIEFING_FILE_NAME, DOCS_PATH, ENV_FILE_PATH, SERVICES_PATHS_PATH } from '../core/config'; import { userConfig } from '../extension'; -import { OPEN_BRIEFING_COMMAND } from '../theia/command'; +import { openBriefingCommand } from '../theia/command'; import { WebviewBase } from './webviewBase'; -export const BRIEFING_ID = 'brefingView'; +export const briefingId = 'brefingView'; export default class BriefingView extends WebviewBase { private briefingContent: string | undefined; @@ -18,11 +18,11 @@ export default class BriefingView extends WebviewBase { private loadingBriefing = false; constructor() { - super(BRIEFING_ID, 'Briefing', OPEN_BRIEFING_COMMAND); + super(briefingId, 'Briefing', openBriefingCommand); } toBase64(file: string) { - const result = fs.readFileSync(path.join(DOCS_PATH, file), { + const result = readFileSync(join(DOCS_PATH, file), { encoding: 'base64', }); return result; @@ -42,11 +42,11 @@ export default class BriefingView extends WebviewBase { load(): void { const intervalId = setInterval(() => { - if (vscode.workspace && !this.loadingBriefing) { + if (workspace && !this.loadingBriefing) { this.loadingBriefing = true; clearInterval(intervalId); - vscode.workspace.openTextDocument(vscode.Uri.parse(path.join(DOCS_PATH, BRIEFING_FILE_NAME))).then( + workspace.openTextDocument(Uri.parse(join(DOCS_PATH, BRIEFING_FILE_NAME))).then( (document) => { try { this.briefingContent = this.setupImages(document.getText()); diff --git a/deadlock-plugins/deadlock-extension/src/view/quickSetupView.ts b/deadlock-plugins/deadlock-extension/src/view/quickSetupView.ts index 26a90fbb943c452938ea79a3208ad63a381b7c5e..7b277be15527346be8b471d10305a4a3c72d0db2 100644 --- a/deadlock-plugins/deadlock-extension/src/view/quickSetupView.ts +++ b/deadlock-plugins/deadlock-extension/src/view/quickSetupView.ts @@ -1,11 +1,11 @@ import * as vscode from 'vscode'; -import { AUTHENTICATE_COMMAND, CHOOSE_MISSION_WORKDIR_COMMAND } from '../core/commandHandler'; +import { authenticateCommand, chooseMissionWorkdirCommand } from '../core/commandHandler'; import ExtensionStore from '../core/extensionStore'; import { log } from '../recorder/utils'; -import { OPEN_QUICK_SETUP_COMMAND } from '../theia/command'; +import { openQuickSetupCommand } from '../theia/command'; import { WebviewBase } from './webviewBase'; -export const QUICK_SETUP_ID = 'quickSetup'; +export const quickSetupId = 'quickSetup'; export default class QuickSetupView extends WebviewBase { private extensionUri: vscode.Uri; @@ -13,7 +13,7 @@ export default class QuickSetupView extends WebviewBase { private _isAlreadyConnected: boolean; constructor(extensionUri: vscode.Uri) { - super(QUICK_SETUP_ID, 'QuickSetup', OPEN_QUICK_SETUP_COMMAND); + super(quickSetupId, 'QuickSetup', openQuickSetupCommand); this.extensionUri = extensionUri; this.extensionStore = ExtensionStore.getInstance(); this._isAlreadyConnected = false; @@ -121,10 +121,10 @@ export default class QuickSetupView extends WebviewBase { onMessageReceive(message: any): void { switch (message.command) { case 'launchChooseMissionWorkdirAction': - vscode.commands.executeCommand(CHOOSE_MISSION_WORKDIR_COMMAND.cmd).then(() => this.reload()); + vscode.commands.executeCommand(chooseMissionWorkdirCommand.cmd).then(() => this.reload()); return; case 'openAuthenticationPageAction': - vscode.commands.executeCommand(AUTHENTICATE_COMMAND.cmd); + vscode.commands.executeCommand(authenticateCommand.cmd); return; } }