diff --git a/deadlock-plugins/deadlock-extension/package.json b/deadlock-plugins/deadlock-extension/package.json index 69aa8ecd31ee4a426c3aee800ade330a3baf4327..dce35c0d30feaa32b3c55174d0ee5a748939c0dc 100644 --- a/deadlock-plugins/deadlock-extension/package.json +++ b/deadlock-plugins/deadlock-extension/package.json @@ -64,7 +64,7 @@ { "id": "help", "name": "Help", - "visibility": "collapsed" + "visibility": "visible" } ] }, diff --git a/deadlock-plugins/deadlock-extension/src/config.prod.ts b/deadlock-plugins/deadlock-extension/src/config.prod.ts index 46ee6889446bcf153f9619125c3ab2bd4d753d65..40beee92f6ff88f45ecca11fd9946b6da96571cf 100644 --- a/deadlock-plugins/deadlock-extension/src/config.prod.ts +++ b/deadlock-plugins/deadlock-extension/src/config.prod.ts @@ -2,4 +2,4 @@ export const KEYCLOAK_DEVICE_AUTH_URL = 'https://auth.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/auth/device'; export const KEYCLOAK_TOKEN_CREATE_URL = 'https://auth.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/token'; export const KEYCLOAK_USER_INFO_URL = 'https://auth.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/userinfo'; -export const REJECT_UNAUTHORIZED = true; +export const REGISTRY_MISSION_URL = 'registry.e-biz.fr/deadlock/deadlock-challenges'; diff --git a/deadlock-plugins/deadlock-extension/src/config.staging.ts b/deadlock-plugins/deadlock-extension/src/config.staging.ts index e8ddb5e90e01601adc2b4f79ee07d241551c535b..5030cdd73e8e942bf5f0ebe0906c96b3088feeac 100644 --- a/deadlock-plugins/deadlock-extension/src/config.staging.ts +++ b/deadlock-plugins/deadlock-extension/src/config.staging.ts @@ -4,4 +4,4 @@ export const KEYCLOAK_TOKEN_CREATE_URL = 'https://auth.staging.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/token'; export const KEYCLOAK_USER_INFO_URL = 'https://auth.staging.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/userinfo'; -export const REJECT_UNAUTHORIZED = true; +export const REGISTRY_MISSION_URL = 'registry.e-biz.fr/deadlock/deadlock-challenges'; diff --git a/deadlock-plugins/deadlock-extension/src/config.ts b/deadlock-plugins/deadlock-extension/src/config.ts index a53b3ab5bba69c5e9d7ed0ef82f7662a8227933f..eb332e792712a5598b88bdab2e38ec43ab137f5a 100644 --- a/deadlock-plugins/deadlock-extension/src/config.ts +++ b/deadlock-plugins/deadlock-extension/src/config.ts @@ -4,4 +4,4 @@ export const KEYCLOAK_TOKEN_CREATE_URL = 'https://auth.dev.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/token'; export const KEYCLOAK_USER_INFO_URL = 'https://auth.dev.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/userinfo'; -export const REJECT_UNAUTHORIZED = false; +export const REGISTRY_MISSION_URL = 'registry.e-biz.fr/deadlock/deadlock-challenges'; diff --git a/deadlock-plugins/deadlock-extension/src/core/controller.ts b/deadlock-plugins/deadlock-extension/src/core/controller.ts index 7d7d3bb15fec7d1d56978bac11aa52d28b86716f..bb205d1317bdb47f244737f34ddccc2ec93ffc81 100644 --- a/deadlock-plugins/deadlock-extension/src/core/controller.ts +++ b/deadlock-plugins/deadlock-extension/src/core/controller.ts @@ -1,11 +1,18 @@ import * as vscode from 'vscode'; -import { KEYCLOAK_DEVICE_AUTH_URL, KEYCLOAK_TOKEN_CREATE_URL, KEYCLOAK_USER_INFO_URL } from '../config'; -import { OPEN_QUICK_SETUP_COMMAND } from '../theia/command'; +import { + KEYCLOAK_DEVICE_AUTH_URL, + KEYCLOAK_TOKEN_CREATE_URL, + KEYCLOAK_USER_INFO_URL, + REGISTRY_MISSION_URL, +} from '../config'; +import { log } from '../recorder/utils'; +import { OPEN_GETTING_STARTED_COMMAND } from '../theia/command'; import BriefingView from '../view/briefingView'; import QuickSetupView from '../view/quickSetupView'; import { CHOOSE_MISSION_WORKDIR_COMMAND, CommandHandler, OPEN_URL_IN_BROWSER_COMMAND } from './commandHandler'; import ExtensionStore from './extensionStore'; import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; +import Mission from './mission'; export default class Controller { public connection: KeycloakOAuth2DeviceFlowConnection; @@ -33,10 +40,11 @@ export default class Controller { handleUri(uri: vscode.Uri) { const queryParams: URLSearchParams = new URLSearchParams(uri.query); const action: string | null = queryParams.get('action'); + log('Opening link', uri); switch (action) { case 'open-challenge': - that.launchMission(queryParams.get('missionId')); + that.launchMission(queryParams.get('missionId'), queryParams.get('missionVersion')); break; default: @@ -86,8 +94,8 @@ export default class Controller { vscode.commands.executeCommand(OPEN_URL_IN_BROWSER_COMMAND.cmd, vscode.Uri.parse(url)); } - public async launchMission(missionId: string | null) { - if (missionId) { + public async launchMission(missionId: string | null, missionVersion: string | null) { + if (missionId && missionVersion) { vscode.window.showInformationMessage(`vous lancez la mission ${missionId}`); const hadMissionWorkdir = this.extensionStore.getMissionWorkdir() !== undefined; if (!hadMissionWorkdir) { @@ -97,13 +105,25 @@ export default class Controller { const hadBeenConnected = (await this.extensionStore.getAccessToken()) !== undefined; if (!hadBeenConnected) { - this.authenticate(); + await this.authenticate(); vscode.window.showInformationMessage('Nouvelle connexion validée'); } else { vscode.window.showInformationMessage('Déjà connecté: session récupérée'); } - vscode.commands.executeCommand(OPEN_QUICK_SETUP_COMMAND.cmd); + // TODO Should I fetch GET api/missions/{missionId} one day instead of passing necessary parameters in vscode xdg-open link ? + const mission = new Mission({ + registryBaseURL: REGISTRY_MISSION_URL, + missionId: missionId, + missionVersion: missionVersion, + }); + vscode.window.showInformationMessage( + 'opening inside folder ' + this.extensionStore.getMissionWorkdir()! + '/' + missionId, + ); + await mission.setup({}); + await mission.openEditorInFolder(); + + vscode.commands.executeCommand(OPEN_GETTING_STARTED_COMMAND.cmd); } } } diff --git a/deadlock-plugins/deadlock-extension/src/core/mission.ts b/deadlock-plugins/deadlock-extension/src/core/mission.ts new file mode 100644 index 0000000000000000000000000000000000000000..a684dbbcfa5f5f80b8050703182a7666c4eecdb9 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/src/core/mission.ts @@ -0,0 +1,135 @@ +import { exec as _exec } from 'child_process'; +import * as fs from 'fs'; +import * as util from 'util'; +import * as vscode from 'vscode'; +import { error as err, log } from '../recorder/utils'; +import ExtensionStore from './extensionStore'; + +/** + * {@link https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback} + */ +const exec = util.promisify(_exec); + +export default class Mission { + private readonly hostBaseWorkDir: string; + private readonly dockerImageURL: string; + private readonly hostMissionDir: string; + private readonly hostMissionDevcontainerDir: string; + private readonly hostMissionDevcontainerFileDir: string; + private readonly hostMissionMountDir: string; + private readonly remoteUserHomeDir: string; + private readonly remoteMissionDir: any; + private readonly remoteGiteaWorkDir: string; + constructor(private params: { registryBaseURL: string; missionId: string; missionVersion: string }) { + const { registryBaseURL, missionId, missionVersion } = params; + this.hostBaseWorkDir = ExtensionStore.getInstance().getMissionWorkdir() ?? ''; + this.hostMissionDir = `${this.hostBaseWorkDir}/${missionId}`; + this.hostMissionDevcontainerDir = `${this.hostMissionDir}/.devcontainer`; + this.hostMissionDevcontainerFileDir = `${this.hostMissionDevcontainerDir}/devcontainer.json`; + this.hostMissionMountDir = `${this.hostMissionDir}/mounted`; + this.dockerImageURL = getDockerImageURL(registryBaseURL, missionId, missionVersion); + this.remoteUserHomeDir = '/home/deadlock'; + this.remoteMissionDir = `${this.remoteUserHomeDir}/mission`; + this.remoteGiteaWorkDir = `/project`; + } + + static async pullImage(url) { + const { stdout, stderr } = await exec(`docker pull ${url}`); + log(stdout); + err(stderr); + } + + public async setup(options?: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts>) { + await fs.promises.mkdir(this.hostMissionDevcontainerDir, { recursive: true }); + await fs.promises.mkdir(this.hostMissionMountDir, { recursive: true }); + await fs.promises.writeFile( + this.hostMissionDevcontainerFileDir, + (() => { + const devcontainer: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts> = { + name: `deadlock-${this.params.missionId}`, + image: this.dockerImageURL, + extensions: ['Deadlock.deadlock-coding'], + remoteUser: 'deadlock', + // mounts: [ + // `source=${this.hostMissionDir}/workspace,target=${'/workspace'},type=${'volume'},consistency=${'cached'}`, + // ], + userEnvProbe: 'interactiveShell', + settings: { + 'terminal.integrated.defaultProfile.linux': 'bash', + 'terminal.integrated.profiles.linux': { + bash: { + path: '/bin/bash', + }, + }, + }, + overrideCommand: false, + shutdownAction: 'stopContainer', + workspaceMount: `source=${this.hostMissionMountDir},target=${this.remoteMissionDir},type=bind`, + workspaceFolder: `${this.remoteMissionDir}`, + onCreateCommand: `cp -R ${this.remoteGiteaWorkDir} ${this.remoteMissionDir}`, + ...options, + }; + return JSON.stringify(devcontainer, null, 2); + })(), + ); + } + + public async openEditorInFolder(arbitraryPath?: string) { + if (arbitraryPath) { + return vscode.commands.executeCommand('remote-containers.openFolder', vscode.Uri.file(arbitraryPath)); + } + if (!fs.existsSync(this.hostMissionDir)) { + log('WARN missing path ', this.hostMissionDir); + await fs.promises.mkdir(this.hostMissionDir, { recursive: true }); + } + await vscode.commands.executeCommand('remote-containers.openFolder', vscode.Uri.file(this.hostMissionDir)); + } +} + +export function getDockerImageURL(base, missionId, missionVersion) { + return `${base}/${missionId}:${missionVersion}`; +} +interface DockerfileSpecific { + image?; + dockerFile?; + context?; + 'build.args'?; + 'build.target'?; + 'build.cacheFrom'?; + containerEnv?; + containerUser?; + mounts?; + workspaceMount?; + workspaceFolder?; + runArgs?; +} + +interface Base { + name?; + forwardPorts?; + portsAttributes?; + otherPortsAttributes?; + remoteEnv?; + remoteUser?; + updateRemoteUserUID?; + userEnvProbe?; + overrideCommand?; + features?; + shutdownAction?; +} + +interface VSCodespecific { + extensions?; + settings?; + devPort?; +} + +interface LifecycleScripts { + initializeCommand?; + onCreateCommand?; + updateContentCommand?; + postCreateCommand?; + postStartCommand?; + postAttachCommand?; + waitFor?; +} diff --git a/deadlock-plugins/deadlock-extension/src/view/webviewBase.ts b/deadlock-plugins/deadlock-extension/src/view/webviewBase.ts index 1639cb5634d43b3209661865ad8e2bb0a3dac4ee..df113cdf39c27f3fd6408f2668706fe7f1b4869d 100644 --- a/deadlock-plugins/deadlock-extension/src/view/webviewBase.ts +++ b/deadlock-plugins/deadlock-extension/src/view/webviewBase.ts @@ -90,17 +90,12 @@ export abstract class WebviewBase implements Disposable { async show(column: ViewColumn = ViewColumn.Beside): Promise<void> { if (this.panel == null) { - this.panel = window.createWebviewPanel( - this.id, - this.title, - { viewColumn: column, preserveFocus: false }, - { - retainContextWhenHidden: true, - enableFindWidget: true, - enableCommandUris: true, - enableScripts: true, - }, - ); + this.panel = window.createWebviewPanel(this.id, this.title, ViewColumn.Active, { + retainContextWhenHidden: true, + enableFindWidget: true, + enableCommandUris: true, + enableScripts: true, + }); this.disposablePanel = Disposable.from( this.panel,