Fix cloud console (#809)
This commit is contained in:
Родитель
a7a365ca5d
Коммит
14b9d28014
|
@ -7,11 +7,13 @@ import { callWithTelemetryAndErrorHandlingSync, IActionContext, IParsedError, pa
|
|||
import * as cp from 'child_process';
|
||||
import * as FormData from 'form-data';
|
||||
import { ReadStream } from 'fs';
|
||||
import * as http from 'http';
|
||||
import { ClientRequest } from 'http';
|
||||
import { DeviceTokenCredentials } from 'ms-rest-azure';
|
||||
import { Socket } from 'net';
|
||||
import { Response } from 'node-fetch';
|
||||
import * as path from 'path';
|
||||
import * as request from 'request-promise';
|
||||
import * as semver from 'semver';
|
||||
import { parse, UrlWithStringQuery } from 'url';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
@ -24,9 +26,11 @@ import { tokenFromRefreshToken } from '../login/adal/tokens';
|
|||
import { getAuthLibrary } from '../login/getAuthLibrary';
|
||||
import { localize } from '../utils/localize';
|
||||
import { logErrorMessage } from '../utils/logErrorMessage';
|
||||
import { HttpLogger } from '../utils/logging/HttpLogger';
|
||||
import { fetchWithLogging } from '../utils/logging/nodeFetch/nodeFetch';
|
||||
import { RequestNormalizer } from '../utils/logging/request/RequestNormalizer';
|
||||
import { Deferred } from '../utils/promiseUtils';
|
||||
import { AccessTokens, connectTerminal, ConsoleUris, Errors, getUserSettings, provisionConsole, resetConsole, Size, UserSettings } from './cloudConsoleLauncher';
|
||||
import { delay } from '../utils/timeUtils';
|
||||
import { CloudShellInternal } from './CloudShellInternal';
|
||||
import { createServer, Queue, readJSON, Server } from './ipc';
|
||||
|
||||
|
@ -645,3 +649,209 @@ async function exec(command: string): Promise<ExecResult> {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const consoleApiVersion = '2017-08-01-preview';
|
||||
|
||||
export enum Errors {
|
||||
DeploymentOsTypeConflict = 'DeploymentOsTypeConflict'
|
||||
}
|
||||
|
||||
function getConsoleUri(armEndpoint: string) {
|
||||
return `${armEndpoint}/providers/Microsoft.Portal/consoles/default?api-version=${consoleApiVersion}`;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
preferredLocation: string;
|
||||
preferredOsType: string; // The last OS chosen in the portal.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
storageProfile: any;
|
||||
}
|
||||
|
||||
export interface AccessTokens {
|
||||
resource: string;
|
||||
graph: string;
|
||||
keyVault?: string;
|
||||
}
|
||||
|
||||
export interface ConsoleUris {
|
||||
consoleUri: string;
|
||||
terminalUri: string;
|
||||
socketUri: string;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
cols: number;
|
||||
rows: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function requestWithLogging(requestOptions: request.Options): Promise<any> {
|
||||
try {
|
||||
const requestLogger = new HttpLogger(ext.outputChannel, 'CloudConsoleLauncher', new RequestNormalizer());
|
||||
requestLogger.logRequest(requestOptions);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const response: http.IncomingMessage & { body: unknown } = await request(requestOptions);
|
||||
requestLogger.logResponse({ response, request: requestOptions });
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return response;
|
||||
} catch (e) {
|
||||
const error = parseError(e);
|
||||
ext.outputChannel.error({...error, name: 'Request Error: CloudConsoleLauncher'});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserSettings(accessToken: string, armEndpoint: string): Promise<UserSettings | undefined> {
|
||||
const targetUri = `${armEndpoint}/providers/Microsoft.Portal/userSettings/cloudconsole?api-version=${consoleApiVersion}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const response = await requestWithLogging({
|
||||
uri: targetUri,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
// if (response.body && response.body.error && response.body.error.message) {
|
||||
// console.log(`${response.body.error.message} (${response.statusCode})`);
|
||||
// } else {
|
||||
// console.log(response.statusCode, response.headers, response.body);
|
||||
// }
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return response.body && response.body.properties;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
||||
export async function provisionConsole(accessToken: string, armEndpoint: string, userSettings: UserSettings, osType: string): Promise<string> {
|
||||
let response = await createTerminal(accessToken, armEndpoint, userSettings, osType, true);
|
||||
for (let i = 0; i < 10; i++ , response = await createTerminal(accessToken, armEndpoint, userSettings, osType, false)) {
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
if (response.statusCode === 409 && response.body && response.body.error && response.body.error.code === Errors.DeploymentOsTypeConflict) {
|
||||
throw new Error(Errors.DeploymentOsTypeConflict);
|
||||
} else if (response.body && response.body.error && response.body.error.message) {
|
||||
throw new Error(`${response.body.error.message} (${response.statusCode})`);
|
||||
} else {
|
||||
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
|
||||
}
|
||||
}
|
||||
|
||||
const consoleResource = response.body;
|
||||
if (consoleResource.properties.provisioningState === 'Succeeded') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return consoleResource.properties.uri;
|
||||
} else if (consoleResource.properties.provisioningState === 'Failed') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new Error(`Sorry, your Cloud Shell failed to provision. Please retry later. Request correlation id: ${response.headers['x-ms-routing-request-id']}`);
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
async function createTerminal(accessToken: string, armEndpoint: string, userSettings: UserSettings, osType: string, initial: boolean) {
|
||||
return requestWithLogging({
|
||||
uri: getConsoleUri(armEndpoint),
|
||||
method: initial ? 'PUT' : 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'x-ms-console-preferred-location': userSettings.preferredLocation
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: initial ? {
|
||||
properties: {
|
||||
osType
|
||||
}
|
||||
} : undefined
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export async function resetConsole(accessToken: string, armEndpoint: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const response = await requestWithLogging({
|
||||
uri: getConsoleUri(armEndpoint),
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
json: true
|
||||
});
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
if (response.body && response.body.error && response.body.error.message) {
|
||||
throw new Error(`${response.body.error.message} (${response.statusCode})`);
|
||||
} else {
|
||||
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
|
||||
}
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
||||
}
|
||||
|
||||
export async function connectTerminal(accessTokens: AccessTokens, consoleUri: string, shellType: string, initialSize: Size, progress: (i: number) => void): Promise<ConsoleUris> {
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
||||
const response = await initializeTerminal(accessTokens, consoleUri, shellType, initialSize);
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
if (response.statusCode !== 503 && response.statusCode !== 504 && response.body && response.body.error) {
|
||||
if (response.body && response.body.error && response.body.error.message) {
|
||||
throw new Error(`${response.body.error.message} (${response.statusCode})`);
|
||||
} else {
|
||||
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
|
||||
}
|
||||
}
|
||||
await delay(1000 * (i + 1));
|
||||
progress(i + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { id, socketUri } = response.body;
|
||||
const terminalUri = `${consoleUri}/terminals/${id}`;
|
||||
return {
|
||||
consoleUri,
|
||||
terminalUri,
|
||||
socketUri
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
||||
}
|
||||
|
||||
throw new Error('Failed to connect to the terminal.');
|
||||
}
|
||||
|
||||
async function initializeTerminal(accessTokens: AccessTokens, consoleUri: string, shellType: string, initialSize: Size) {
|
||||
return requestWithLogging({
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
uri: consoleUri + '/terminals?cols=' + initialSize.cols + '&rows=' + initialSize.rows + '&shell=' + shellType,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${accessTokens.resource}`
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
tokens: accessTokens.keyVault ? [accessTokens.graph, accessTokens.keyVault] : [accessTokens.graph]
|
||||
}
|
||||
});
|
||||
}
|
|
@ -3,33 +3,15 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// this file is run as a child process from the extension host and communicates via IPC
|
||||
import * as http from 'http';
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import * as request from 'request-promise';
|
||||
import * as WS from 'ws';
|
||||
import { ext } from '../extensionVariables';
|
||||
import { HttpLogger } from '../utils/logging/HttpLogger';
|
||||
import { RequestNormalizer } from '../utils/logging/request/RequestNormalizer';
|
||||
import { delay } from '../utils/timeUtils';
|
||||
import { readJSON, sendData } from './ipc';
|
||||
|
||||
const consoleApiVersion = '2017-08-01-preview';
|
||||
|
||||
export enum Errors {
|
||||
DeploymentOsTypeConflict = 'DeploymentOsTypeConflict'
|
||||
}
|
||||
|
||||
function getConsoleUri(armEndpoint: string) {
|
||||
return `${armEndpoint}/providers/Microsoft.Portal/consoles/default?api-version=${consoleApiVersion}`;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
preferredLocation: string;
|
||||
preferredOsType: string; // The last OS chosen in the portal.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
storageProfile: any;
|
||||
}
|
||||
|
||||
export interface AccessTokens {
|
||||
resource: string;
|
||||
graph: string;
|
||||
|
@ -47,172 +29,6 @@ export interface Size {
|
|||
rows: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function requestWithLogging(requestOptions: request.Options): Promise<any> {
|
||||
const requestLogger = new HttpLogger(ext.outputChannel, 'CloudConsoleLauncher', new RequestNormalizer());
|
||||
requestLogger.logRequest(requestOptions);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const response: http.IncomingMessage & { body: unknown } = await request(requestOptions);
|
||||
requestLogger.logResponse({ response, request: requestOptions });
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function getUserSettings(accessToken: string, armEndpoint: string): Promise<UserSettings | undefined> {
|
||||
const targetUri = `${armEndpoint}/providers/Microsoft.Portal/userSettings/cloudconsole?api-version=${consoleApiVersion}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const response = await requestWithLogging({
|
||||
uri: targetUri,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
// if (response.body && response.body.error && response.body.error.message) {
|
||||
// console.log(`${response.body.error.message} (${response.statusCode})`);
|
||||
// } else {
|
||||
// console.log(response.statusCode, response.headers, response.body);
|
||||
// }
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return response.body && response.body.properties;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
||||
export async function provisionConsole(accessToken: string, armEndpoint: string, userSettings: UserSettings, osType: string): Promise<string> {
|
||||
let response = await createTerminal(accessToken, armEndpoint, userSettings, osType, true);
|
||||
for (let i = 0; i < 10; i++ , response = await createTerminal(accessToken, armEndpoint, userSettings, osType, false)) {
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
if (response.statusCode === 409 && response.body && response.body.error && response.body.error.code === Errors.DeploymentOsTypeConflict) {
|
||||
throw new Error(Errors.DeploymentOsTypeConflict);
|
||||
} else if (response.body && response.body.error && response.body.error.message) {
|
||||
throw new Error(`${response.body.error.message} (${response.statusCode})`);
|
||||
} else {
|
||||
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
|
||||
}
|
||||
}
|
||||
|
||||
const consoleResource = response.body;
|
||||
if (consoleResource.properties.provisioningState === 'Succeeded') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return consoleResource.properties.uri;
|
||||
} else if (consoleResource.properties.provisioningState === 'Failed') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new Error(`Sorry, your Cloud Shell failed to provision. Please retry later. Request correlation id: ${response.headers['x-ms-routing-request-id']}`);
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
async function createTerminal(accessToken: string, armEndpoint: string, userSettings: UserSettings, osType: string, initial: boolean) {
|
||||
return requestWithLogging({
|
||||
uri: getConsoleUri(armEndpoint),
|
||||
method: initial ? 'PUT' : 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'x-ms-console-preferred-location': userSettings.preferredLocation
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: initial ? {
|
||||
properties: {
|
||||
osType
|
||||
}
|
||||
} : undefined
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export async function resetConsole(accessToken: string, armEndpoint: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const response = await requestWithLogging({
|
||||
uri: getConsoleUri(armEndpoint),
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
json: true
|
||||
});
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
if (response.body && response.body.error && response.body.error.message) {
|
||||
throw new Error(`${response.body.error.message} (${response.statusCode})`);
|
||||
} else {
|
||||
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
|
||||
}
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
||||
}
|
||||
|
||||
export async function connectTerminal(accessTokens: AccessTokens, consoleUri: string, shellType: string, initialSize: Size, progress: (i: number) => void): Promise<ConsoleUris> {
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
||||
const response = await initializeTerminal(accessTokens, consoleUri, shellType, initialSize);
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
if (response.statusCode !== 503 && response.statusCode !== 504 && response.body && response.body.error) {
|
||||
if (response.body && response.body.error && response.body.error.message) {
|
||||
throw new Error(`${response.body.error.message} (${response.statusCode})`);
|
||||
} else {
|
||||
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
|
||||
}
|
||||
}
|
||||
await delay(1000 * (i + 1));
|
||||
progress(i + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { id, socketUri } = response.body;
|
||||
const terminalUri = `${consoleUri}/terminals/${id}`;
|
||||
return {
|
||||
consoleUri,
|
||||
terminalUri,
|
||||
socketUri
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
||||
}
|
||||
|
||||
throw new Error('Failed to connect to the terminal.');
|
||||
}
|
||||
|
||||
async function initializeTerminal(accessTokens: AccessTokens, consoleUri: string, shellType: string, initialSize: Size) {
|
||||
return requestWithLogging({
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
uri: consoleUri + '/terminals?cols=' + initialSize.cols + '&rows=' + initialSize.rows + '&shell=' + shellType,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${accessTokens.resource}`
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
tokens: accessTokens.keyVault ? [accessTokens.graph, accessTokens.keyVault] : [accessTokens.graph]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getWindowSize(): Size {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const stdout: any = process.stdout;
|
||||
|
@ -236,7 +52,7 @@ async function resize(accessTokens: AccessTokens, terminalUri: string) {
|
|||
|
||||
const { cols, rows } = getWindowSize();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const response = await requestWithLogging({
|
||||
const response = await request({
|
||||
uri: `${terminalUri}/size?cols=${cols}&rows=${rows}`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -334,10 +150,6 @@ function connectSocket(ipcHandle: string, url: string) {
|
|||
}
|
||||
}
|
||||
|
||||
async function delay(ms: number) {
|
||||
return new Promise<void>(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function main() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
|
Загрузка…
Ссылка в новой задаче