mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 11:36:47 +00:00
Compare commits
9 Commits
users/sour
...
release/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b730e2c6fd | ||
|
|
14c9874e5e | ||
|
|
a04eaff6be | ||
|
|
51a412e2c0 | ||
|
|
3fcbdf6152 | ||
|
|
8da078579e | ||
|
|
4ac41031e6 | ||
|
|
d7923db108 | ||
|
|
0170c9e1cc |
@@ -61,6 +61,8 @@
|
|||||||
|
|
||||||
@GalleryBackgroundColor: #fdfdfd;
|
@GalleryBackgroundColor: #fdfdfd;
|
||||||
|
|
||||||
|
@LinkColor: #2d6da4;
|
||||||
|
|
||||||
//Icons
|
//Icons
|
||||||
@InfoIconColor: #0072c6;
|
@InfoIconColor: #0072c6;
|
||||||
@WarningIconColor: #db7500;
|
@WarningIconColor: #db7500;
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -51,7 +51,6 @@
|
|||||||
"@types/mkdirp": "1.0.1",
|
"@types/mkdirp": "1.0.1",
|
||||||
"@types/node-fetch": "2.5.7",
|
"@types/node-fetch": "2.5.7",
|
||||||
"@xmldom/xmldom": "0.7.13",
|
"@xmldom/xmldom": "0.7.13",
|
||||||
"@xterm/xterm": "5.5.0",
|
|
||||||
"allotment": "1.20.2",
|
"allotment": "1.20.2",
|
||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "1.8.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
@@ -13239,11 +13238,6 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xterm/xterm": {
|
|
||||||
"version": "5.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
|
||||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
|
|
||||||
},
|
|
||||||
"node_modules/@xtuc/ieee754": {
|
"node_modules/@xtuc/ieee754": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||||
"@fluentui/react": "8.119.0",
|
"@fluentui/react": "8.119.0",
|
||||||
"@fluentui/react-components": "9.54.2",
|
"@fluentui/react-components": "9.54.2",
|
||||||
"@jupyterlab/terminal": "3.0.3",
|
|
||||||
"@jupyterlab/services": "6.0.2",
|
"@jupyterlab/services": "6.0.2",
|
||||||
|
"@jupyterlab/terminal": "3.0.3",
|
||||||
"@microsoft/applicationinsights-web": "2.6.1",
|
"@microsoft/applicationinsights-web": "2.6.1",
|
||||||
"@nteract/commutable": "7.5.1",
|
"@nteract/commutable": "7.5.1",
|
||||||
"@nteract/connected-components": "6.8.2",
|
"@nteract/connected-components": "6.8.2",
|
||||||
@@ -46,7 +46,6 @@
|
|||||||
"@types/mkdirp": "1.0.1",
|
"@types/mkdirp": "1.0.1",
|
||||||
"@types/node-fetch": "2.5.7",
|
"@types/node-fetch": "2.5.7",
|
||||||
"@xmldom/xmldom": "0.7.13",
|
"@xmldom/xmldom": "0.7.13",
|
||||||
"@xterm/xterm": "5.5.0",
|
|
||||||
"allotment": "1.20.2",
|
"allotment": "1.20.2",
|
||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "1.8.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { PriorityLevel } from "../Common/Constants";
|
|||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { updateUserContext, userContext } from "../UserContext";
|
import { updateUserContext, userContext } from "../UserContext";
|
||||||
|
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
@@ -18,7 +19,7 @@ const _global = typeof self === "undefined" ? window : self;
|
|||||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
|
|
||||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
||||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
publicNetworkAccess?: string;
|
publicNetworkAccess?: string;
|
||||||
enablePriorityBasedExecution?: boolean;
|
enablePriorityBasedExecution?: boolean;
|
||||||
vcoreMongoEndpoint?: string;
|
vcoreMongoEndpoint?: string;
|
||||||
virtualNetworkRules?: VNetRule[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VNetRule {
|
|
||||||
id: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccountResponseLocation {
|
export interface DatabaseAccountResponseLocation {
|
||||||
|
|||||||
@@ -96,17 +96,17 @@ export const createCollectionContextMenuButton = (
|
|||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
|
if (useNotebook.getState().isShellEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
} else {
|
} else {
|
||||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) ? "Open Mongo Shell" : "New Shell",
|
label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) && userContext.apiType === "Cassandra") {
|
if (useNotebook.getState().isShellEnabled && userContext.apiType === "Cassandra") {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
|||||||
@@ -35,12 +35,20 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
onCostAcknowledgeChange,
|
onCostAcknowledgeChange,
|
||||||
}: ThroughputInputProps) => {
|
}: ThroughputInputProps) => {
|
||||||
const defaultThroughput: number =
|
let defaultThroughput: number;
|
||||||
|
const workloadType: Constants.WorkloadType = getWorkloadType();
|
||||||
|
|
||||||
|
if (
|
||||||
isFreeTier ||
|
isFreeTier ||
|
||||||
isQuickstart ||
|
isQuickstart ||
|
||||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType())
|
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
||||||
? AutoPilotUtils.autoPilotThroughput1K
|
) {
|
||||||
: AutoPilotUtils.autoPilotThroughput4K;
|
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
||||||
|
} else if (workloadType === Constants.WorkloadType.Production) {
|
||||||
|
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
|
||||||
|
} else {
|
||||||
|
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
|
||||||
|
}
|
||||||
|
|
||||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||||
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
||||||
|
|||||||
@@ -906,28 +906,25 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
||||||
|
if (useNotebook.getState().isPhoenixFeatures) {
|
||||||
if (userContext.features.enableCloudShell || !useNotebook.getState().isPhoenixFeatures) {
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
this.connectToTerminal(kind);
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
return;
|
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||||
}
|
this.connectToNotebookTerminal(kind);
|
||||||
|
} else {
|
||||||
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
useDialog
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
.getState()
|
||||||
|
.showOkModalDialog(
|
||||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
"Failed to connect",
|
||||||
this.connectToTerminal(kind);
|
"Failed to connect to temporary workspace. This could happen because of network issues. Please refresh the page and try again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
useDialog
|
this.connectToNotebookTerminal(kind);
|
||||||
.getState()
|
|
||||||
.showOkModalDialog(
|
|
||||||
"Failed to connect",
|
|
||||||
"Failed to connect to temporary workspace. This could happen because of network issues. Please refresh the page and try again."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private connectToTerminal(kind: ViewModels.TerminalKind): void {
|
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||||
let title: string;
|
let title: string;
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
|
|||||||
@@ -14,10 +14,6 @@
|
|||||||
.flex-direction(@direction: row);
|
.flex-direction(@direction: row);
|
||||||
padding: 4px 5px;
|
padding: 4px 5px;
|
||||||
|
|
||||||
label {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.valueCol {
|
.valueCol {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
@@ -63,6 +59,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customTrashIcon {
|
||||||
|
padding-top: 33px;
|
||||||
|
}
|
||||||
|
|
||||||
.rightPaneTrashIconImg {
|
.rightPaneTrashIconImg {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,10 +142,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
<div className="labelCol">
|
<div className="labelCol">
|
||||||
<TextField
|
<TextField
|
||||||
className="edgeInput"
|
className="edgeInput"
|
||||||
|
label={index === 0 && "Key"}
|
||||||
type="text"
|
type="text"
|
||||||
id="propertyKeyNewVertexPane"
|
id="propertyKeyNewVertexPane"
|
||||||
componentRef={input}
|
componentRef={input}
|
||||||
aria-required="true"
|
required
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
aria-label={`Enter value for propery ${index + 1}`}
|
aria-label={`Enter value for propery ${index + 1}`}
|
||||||
@@ -153,11 +154,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
|
|
||||||
<div className="valueCol">
|
<div className="valueCol">
|
||||||
<TextField
|
<TextField
|
||||||
className="edgeInput"
|
className="edgeInput"
|
||||||
|
label={index === 0 && "Value"}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -169,6 +170,8 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
<div>
|
<div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
role="combobox"
|
role="combobox"
|
||||||
|
label={index === 0 && "Type"}
|
||||||
|
ariaLabel="Type"
|
||||||
placeholder="Select an option"
|
placeholder="Select an option"
|
||||||
defaultSelectedKey={data.values[0].type}
|
defaultSelectedKey={data.values[0].type}
|
||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
@@ -181,7 +184,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
</div>
|
</div>
|
||||||
<div className="actionCol">
|
<div className="actionCol">
|
||||||
<div
|
<div
|
||||||
className="rightPaneTrashIcon rightPaneBtns"
|
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={`Delete ${data.key}`}
|
aria-label={`Delete ${data.key}`}
|
||||||
|
|||||||
@@ -37,21 +37,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Button should not be visible for Tables API", () => {
|
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
|
||||||
updateUserContext({
|
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
|
||||||
databaseAccount: {
|
// unsupported in jest and needs to be tested with react-hooks-testing-library.
|
||||||
properties: {
|
//
|
||||||
capabilities: [{ name: "EnableTable" }],
|
// it("Button should not be visible for Tables API", () => {
|
||||||
},
|
// updateUserContext({
|
||||||
} as DatabaseAccount,
|
// databaseAccount: {
|
||||||
});
|
// properties: {
|
||||||
|
// capabilities: [{ name: "EnableTable" }],
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
// },
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
// } as DatabaseAccount,
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
// });
|
||||||
);
|
//
|
||||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
});
|
// const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
|
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
|
// );
|
||||||
|
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||||
|
//});
|
||||||
|
|
||||||
it("Button should not be visible for Cassandra API", () => {
|
it("Button should not be visible for Cassandra API", () => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
|
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||||
@@ -61,7 +62,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.apiType === "SQL") {
|
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
@@ -125,13 +126,13 @@ export function createContextCommandBarButtons(
|
|||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||||
const label = (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) ? "Open Mongo Shell" : "New Shell";
|
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
||||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
|
if (useNotebook.getState().isShellEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
} else {
|
} else {
|
||||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||||
@@ -145,7 +146,7 @@ export function createContextCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
|
useNotebook.getState().isShellEnabled &&
|
||||||
!selectedNodeState.isDatabaseNodeOrNoneSelected() &&
|
!selectedNodeState.isDatabaseNodeOrNoneSelected() &&
|
||||||
userContext.apiType === "Cassandra"
|
userContext.apiType === "Cassandra"
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -109,15 +109,15 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<div className="statusBar">
|
<div className="statusBar">
|
||||||
<span className="dataTypeIcons">
|
<span className="dataTypeIcons">
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={LoadingIcon} alt="in progress items" />
|
<img src={LoadingIcon} alt="In progress items" />
|
||||||
<span className="numInProgress">{numInProgress}</span>
|
<span className="numInProgress">{numInProgress}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={ErrorBlackIcon} alt="error items" />
|
<img src={ErrorBlackIcon} alt="Error items" />
|
||||||
<span className="numErroredItems">{numErroredItems}</span>
|
<span className="numErroredItems">{numErroredItems}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={infoBubbleIcon} alt="info items" />
|
<img src={infoBubbleIcon} alt="Info items" />
|
||||||
<span className="numInfoItems">{numInfoItems}</span>
|
<span className="numInfoItems">{numInfoItems}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -134,12 +134,12 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
data-test="NotificationConsole/ExpandCollapseButton"
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
aria-label="Console"
|
||||||
aria-expanded={!this.props.isConsoleExpanded}
|
aria-expanded={this.props.isConsoleExpanded}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||||
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="in progress items"
|
alt="In progress items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -34,7 +34,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="error items"
|
alt="Error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -47,7 +47,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="info items"
|
alt="Info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -71,15 +71,15 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-expanded={true}
|
aria-expanded={false}
|
||||||
aria-label="console button collapsed"
|
aria-label="Console"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
data-test="NotificationConsole/ExpandCollapseButton"
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="ChevronUpIcon"
|
alt="Expand icon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,7 +192,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="in progress items"
|
alt="In progress items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -205,7 +205,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="error items"
|
alt="Error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -218,7 +218,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="info items"
|
alt="Info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -244,15 +244,15 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-expanded={true}
|
aria-expanded={false}
|
||||||
aria-label="console button collapsed"
|
aria-label="Console"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
data-test="NotificationConsole/ExpandCollapseButton"
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="ChevronUpIcon"
|
alt="Expand icon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
padding-left: @MediumSpace;
|
padding-left: @MediumSpace;
|
||||||
|
|
||||||
.paneErrorLink {
|
.paneErrorLink {
|
||||||
|
color: @LinkColor;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: @mediumFontSize;
|
font-size: @mediumFontSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
} from "Shared/StorageUtility";
|
} from "Shared/StorageUtility";
|
||||||
import * as StringUtility from "Shared/StringUtility";
|
import * as StringUtility from "Shared/StringUtility";
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
|
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||||
@@ -183,7 +184,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||||
const showEnableEntraIdRbac =
|
const showEnableEntraIdRbac =
|
||||||
userContext.apiType === "SQL" &&
|
isDataplaneRbacSupported(userContext.apiType) &&
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
configContext.platform !== Platform.Fabric &&
|
configContext.platform !== Platform.Fabric &&
|
||||||
!isEmulator;
|
!isEmulator;
|
||||||
|
|||||||
@@ -393,8 +393,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
disabled={isGeneratingQuery}
|
disabled={isGeneratingQuery}
|
||||||
autoComplete="list"
|
autoComplete="off"
|
||||||
aria-expanded={showSamplePrompts}
|
|
||||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||||
aria-labelledby="copilot-textfield-label"
|
aria-labelledby="copilot-textfield-label"
|
||||||
onRenderSuffix={() => {
|
onRenderSuffix={() => {
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IDisposable, ITerminalAddon, Terminal } from 'xterm';
|
|
||||||
|
|
||||||
interface IAttachOptions {
|
|
||||||
bidirectional?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AttachAddon implements ITerminalAddon {
|
|
||||||
private _socket: WebSocket;
|
|
||||||
private _bidirectional: boolean;
|
|
||||||
private _disposables: IDisposable[] = [];
|
|
||||||
private _socketData: string;
|
|
||||||
|
|
||||||
constructor(socket: WebSocket, options?: IAttachOptions) {
|
|
||||||
this._socket = socket;
|
|
||||||
// always set binary type to arraybuffer, we do not handle blobs
|
|
||||||
this._socket.binaryType = 'arraybuffer';
|
|
||||||
this._bidirectional = !(options && options.bidirectional === false);
|
|
||||||
this._socketData = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public activate(terminal: Terminal): void {
|
|
||||||
this._disposables.push(
|
|
||||||
addSocketListener(this._socket, 'message', ev => {
|
|
||||||
let data: ArrayBuffer | string = ev.data;
|
|
||||||
const startStatusJson = 'ie_us';
|
|
||||||
const endStatusJson = 'ie_ue';
|
|
||||||
|
|
||||||
if (typeof data === 'object') {
|
|
||||||
const enc = new TextDecoder("utf-8");
|
|
||||||
data = enc.decode(ev.data as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
// for example of json object look in TerminalHelper in the socket.onMessage
|
|
||||||
if (data.includes(startStatusJson) && data.includes(endStatusJson)) {
|
|
||||||
// process as one line
|
|
||||||
const statusData = data.split(startStatusJson)[1].split(endStatusJson)[0];
|
|
||||||
data = data.replace(statusData, '');
|
|
||||||
data = data.replace(startStatusJson, '');
|
|
||||||
data = data.replace(endStatusJson, '');
|
|
||||||
} else if (data.includes(startStatusJson)) {
|
|
||||||
// check for start
|
|
||||||
const partialStatusData = data.split(startStatusJson)[1];
|
|
||||||
this._socketData += partialStatusData;
|
|
||||||
data = data.replace(partialStatusData, '');
|
|
||||||
data = data.replace(startStatusJson, '');
|
|
||||||
} else if (data.includes(endStatusJson)) {
|
|
||||||
// check for end and process the command
|
|
||||||
const partialStatusData = data.split(endStatusJson)[0];
|
|
||||||
this._socketData += partialStatusData;
|
|
||||||
data = data.replace(partialStatusData, '');
|
|
||||||
data = data.replace(endStatusJson, '');
|
|
||||||
this._socketData = '';
|
|
||||||
} else if (this._socketData.length > 0) {
|
|
||||||
// check if the line is all data then just concatenate
|
|
||||||
this._socketData += data;
|
|
||||||
data = '';
|
|
||||||
}
|
|
||||||
terminal.write(data);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this._bidirectional) {
|
|
||||||
this._disposables.push(terminal.onData(data => this._sendData(data)));
|
|
||||||
this._disposables.push(terminal.onBinary(data => this._sendBinary(data)));
|
|
||||||
}
|
|
||||||
|
|
||||||
this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose()));
|
|
||||||
this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void {
|
|
||||||
for (const d of this._disposables) {
|
|
||||||
d.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _sendData(data: string): void {
|
|
||||||
if (!this._checkOpenSocket()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._socket.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _sendBinary(data: string): void {
|
|
||||||
if (!this._checkOpenSocket()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const buffer = new Uint8Array(data.length);
|
|
||||||
for (let i = 0; i < data.length; ++i) {
|
|
||||||
buffer[i] = data.charCodeAt(i) & 255;
|
|
||||||
}
|
|
||||||
this._socket.send(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _checkOpenSocket(): boolean {
|
|
||||||
switch (this._socket.readyState) {
|
|
||||||
case WebSocket.OPEN:
|
|
||||||
return true;
|
|
||||||
case WebSocket.CONNECTING:
|
|
||||||
throw new Error('Attach addon was loaded before socket was open');
|
|
||||||
case WebSocket.CLOSING:
|
|
||||||
return false;
|
|
||||||
case WebSocket.CLOSED:
|
|
||||||
throw new Error('Attach addon socket is closed');
|
|
||||||
default:
|
|
||||||
throw new Error('Unexpected socket state');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
|
|
||||||
socket.addEventListener(type, handler);
|
|
||||||
return {
|
|
||||||
dispose: () => {
|
|
||||||
if (!handler) {
|
|
||||||
// Already disposed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socket.removeEventListener(type, handler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
|
||||||
import "xterm/css/xterm.css";
|
|
||||||
import { TerminalKind } from "../../../Contracts/ViewModels";
|
|
||||||
import { startCloudShellTerminal } from "./Core/CloudShellTerminalCore";
|
|
||||||
|
|
||||||
export interface CloudShellTerminalProps {
|
|
||||||
shellType: TerminalKind;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CloudShellTerminalComponent: React.FC<CloudShellTerminalProps> = ({
|
|
||||||
shellType
|
|
||||||
}: CloudShellTerminalProps) => {
|
|
||||||
const terminalRef = useRef(null); // Reference for terminal container
|
|
||||||
const xtermRef = useRef(null); // Reference for XTerm instance
|
|
||||||
const socketRef = useRef(null); // Reference for WebSocket
|
|
||||||
const fitAddon = new FitAddon();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize XTerm instance
|
|
||||||
const term = new Terminal({
|
|
||||||
cursorBlink: true,
|
|
||||||
cursorStyle: 'bar',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: 14,
|
|
||||||
theme: {
|
|
||||||
background: "#1e1e1e",
|
|
||||||
foreground: "#d4d4d4",
|
|
||||||
cursor: "#ffcc00"
|
|
||||||
},
|
|
||||||
scrollback: 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
term.loadAddon(fitAddon);
|
|
||||||
|
|
||||||
// Attach terminal to the DOM
|
|
||||||
if (terminalRef.current) {
|
|
||||||
term.open(terminalRef.current);
|
|
||||||
xtermRef.current = term;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fitAddon) {
|
|
||||||
fitAddon.fit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust terminal size on window resize
|
|
||||||
const handleResize = () => fitAddon.fit();
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
|
|
||||||
try {
|
|
||||||
socketRef.current = startCloudShellTerminal(term, shellType);
|
|
||||||
term.onData((data) => {
|
|
||||||
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
socketRef.current.send(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to initialize CloudShell terminal:", error);
|
|
||||||
term.writeln(`\x1B[31mError initializing terminal: ${error.message}\x1B[0m`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup function to close WebSocket and dispose terminal
|
|
||||||
return () => {
|
|
||||||
if (!socketRef.current) return;
|
|
||||||
if (socketRef.current) {
|
|
||||||
socketRef.current.close(); // Close WebSocket connection
|
|
||||||
}
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
term.dispose(); // Clean up XTerm instance
|
|
||||||
};
|
|
||||||
|
|
||||||
}, [shellType]);
|
|
||||||
|
|
||||||
return <div ref={terminalRef} style={{ width: "100%", height: "500px"}} />;
|
|
||||||
};
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { TerminalKind } from "../../../Contracts/ViewModels";
|
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
|
|
||||||
export const getCommands = (terminalKind: TerminalKind, key: string) => {
|
|
||||||
let dbAccount = userContext.databaseAccount;
|
|
||||||
let endpoint;
|
|
||||||
switch (terminalKind) {
|
|
||||||
case TerminalKind.Postgres:
|
|
||||||
endpoint = dbAccount.properties.postgresqlEndpoint;
|
|
||||||
break;
|
|
||||||
case TerminalKind.Mongo:
|
|
||||||
endpoint = dbAccount.properties.mongoEndpoint;
|
|
||||||
break;
|
|
||||||
case TerminalKind.VCoreMongo:
|
|
||||||
endpoint = dbAccount.properties.vcoreMongoEndpoint;
|
|
||||||
break;
|
|
||||||
case TerminalKind.Cassandra:
|
|
||||||
endpoint = dbAccount.properties.cassandraEndpoint;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown Terminal Kind");
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = {
|
|
||||||
host: getHostFromUrl(endpoint),
|
|
||||||
name: dbAccount.name,
|
|
||||||
password: key,
|
|
||||||
endpoint: endpoint
|
|
||||||
};
|
|
||||||
|
|
||||||
return commands(terminalKind, config).join("\n").concat("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CommandConfig {
|
|
||||||
host: string,
|
|
||||||
name: string,
|
|
||||||
password: string,
|
|
||||||
endpoint: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const commands = (terminalKind: TerminalKind, config?: CommandConfig): string[] => {
|
|
||||||
switch (terminalKind) {
|
|
||||||
case TerminalKind.Postgres:
|
|
||||||
return [
|
|
||||||
// 1. Fetch and display location details in a readable format
|
|
||||||
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
|
|
||||||
// 2. Check if psql is installed; if not, proceed with installation
|
|
||||||
"if ! command -v psql &> /dev/null; then echo '⚠️ psql not found. Installing...'; fi",
|
|
||||||
// 3. Download PostgreSQL if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then curl -LO https://ftp.postgresql.org/pub/source/v15.2/postgresql-15.2.tar.bz2; fi",
|
|
||||||
// 4. Extract PostgreSQL package if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then tar -xvjf postgresql-15.2.tar.bz2; fi",
|
|
||||||
// 5. Create a directory for PostgreSQL installation if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then mkdir -p ~/pgsql; fi",
|
|
||||||
// 6. Download readline (dependency for PostgreSQL) if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then curl -LO https://ftp.gnu.org/gnu/readline/readline-8.1.tar.gz; fi",
|
|
||||||
// 7. Extract readline package if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then tar -xvzf readline-8.1.tar.gz; fi",
|
|
||||||
// 8. Configure readline if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then cd readline-8.1 && ./configure --prefix=$HOME/pgsql; fi",
|
|
||||||
// 9. Add PostgreSQL to PATH if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then echo 'export PATH=$HOME/pgsql/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
// 10. Source .bashrc to update PATH (even if psql was already installed)
|
|
||||||
"source ~/.bashrc",
|
|
||||||
// 11. Verify PostgreSQL installation
|
|
||||||
"psql --version",
|
|
||||||
`psql 'read -p "Enter Database Name: " dbname && read -p "Enter Username: " username && host=${config.endpoint} port=5432 dbname=$dbname user=$username sslmode=require'`
|
|
||||||
];
|
|
||||||
case TerminalKind.Mongo:
|
|
||||||
return [
|
|
||||||
// 1. Fetch and display location details in a readable format
|
|
||||||
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
|
|
||||||
// 2. Check if mongosh is installed; if not, proceed with installation
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
|
||||||
// 3. Download mongosh if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz; fi",
|
|
||||||
// 4. Extract mongosh package if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-2.3.8-linux-x64.tgz; fi",
|
|
||||||
// 5. Move mongosh binaries if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-2.3.8-linux-x64/* ~/mongosh/; fi",
|
|
||||||
// 6. Add mongosh to PATH if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
// 7. Source .bashrc to update PATH (even if mongosh was already installed)
|
|
||||||
"source ~/.bashrc",
|
|
||||||
// 8. Verify mongosh installation
|
|
||||||
"mongosh --version",
|
|
||||||
// 9. Login to MongoDB
|
|
||||||
`mongosh --host ${config.host} --port 10255 --username ${config.name} --password ${config.password} --tls --tlsAllowInvalidCertificates`
|
|
||||||
];
|
|
||||||
case TerminalKind.VCoreMongo:
|
|
||||||
return [
|
|
||||||
// 1. Fetch and display location details in a readable format
|
|
||||||
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
|
|
||||||
// 2. Check if mongosh is installed; if not, proceed with installation
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
|
||||||
// 3. Download mongosh if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz; fi",
|
|
||||||
// 4. Extract mongosh package if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-2.3.8-linux-x64.tgz; fi",
|
|
||||||
// 5. Move mongosh binaries if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-2.3.8-linux-x64/* ~/mongosh/; fi",
|
|
||||||
// 6. Add mongosh to PATH if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
// 7. Source .bashrc to update PATH (even if mongosh was already installed)
|
|
||||||
"source ~/.bashrc",
|
|
||||||
// 8. Verify mongosh installation
|
|
||||||
"mongosh --version",
|
|
||||||
// 10. Login to MongoDBmongosh mongodb+srv://<credentials>@neesharma-stage-mongo-vcore.mongocluster.cosmos.azure.com/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000\u0007
|
|
||||||
`read -p "Enter username: " username && mongosh "mongodb+srv://$username:@${config.endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000" --tls --tlsAllowInvalidCertificates`
|
|
||||||
];
|
|
||||||
case TerminalKind.Cassandra:
|
|
||||||
return [
|
|
||||||
// 1. Fetch and display location details in a readable format
|
|
||||||
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
|
|
||||||
// 2. Check if cqlsh is installed; if not, proceed with installation
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then echo '⚠️ cqlsh not found. Installing...'; fi",
|
|
||||||
// 3. Download Cassandra if not installed
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then curl -LO https://archive.apache.org/dist/cassandra/5.0.3/apache-cassandra-5.0.3-bin.tar.gz; fi",
|
|
||||||
// 4. Extract Cassandra package if not installed
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then tar -xvzf apache-cassandra-5.0.3-bin.tar.gz; fi",
|
|
||||||
// 5. Move Cassandra binaries if not installed
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then mkdir -p ~/cassandra && mv apache-cassandra-5.0.3/* ~/cassandra/; fi",
|
|
||||||
// 6. Add Cassandra to PATH if not installed
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then echo 'export PATH=$HOME/cassandra/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
// 7. Set environment variables for SSL
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then echo 'export SSL_VERSION=TLSv1_2' >> ~/.bashrc; fi",
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then echo 'export SSL_VALIDATE=false' >> ~/.bashrc; fi",
|
|
||||||
// 8. Source .bashrc to update PATH (even if cqlsh was already installed)
|
|
||||||
"source ~/.bashrc",
|
|
||||||
// 9. Verify cqlsh installation
|
|
||||||
"cqlsh --version",
|
|
||||||
// 10. Login to Cassandra
|
|
||||||
`cqlsh ${config.host} 10350 -u ${config.name} -p ${config.password} --ssl --protocol-version=4`
|
|
||||||
];
|
|
||||||
default:
|
|
||||||
return ["echo Unknown Shell"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getHostFromUrl = (mongoEndpoint: string): string => {
|
|
||||||
try {
|
|
||||||
const url = new URL(mongoEndpoint);
|
|
||||||
return url.hostname;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Invalid Mongo Endpoint URL:", error);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Core functionality for CloudShell terminal management
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { userContext } from "../../../../UserContext";
|
|
||||||
import {
|
|
||||||
authorizeSession,
|
|
||||||
connectTerminal,
|
|
||||||
provisionConsole,
|
|
||||||
putEphemeralUserSettings,
|
|
||||||
registerCloudShellProvider,
|
|
||||||
verifyCloudShellProviderRegistration
|
|
||||||
} from "../Data/CloudShellApiClient";
|
|
||||||
import { getNormalizedRegion } from "../Data/RegionUtils";
|
|
||||||
import { ShellTypeHandler } from "../ShellTypes/ShellTypeFactory";
|
|
||||||
import { AttachAddon } from "../Utils/AttachAddOn";
|
|
||||||
import { wait } from "../Utils/CommonUtils";
|
|
||||||
import { terminalLog } from "../Utils/LogFormatter";
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
|
||||||
const POLLING_INTERVAL_MS = 5000;
|
|
||||||
const MAX_RETRY_COUNT = 10;
|
|
||||||
const MAX_PING_COUNT = 20 * 60; // 20 minutes (60 seconds/minute)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main function to start a CloudShell terminal
|
|
||||||
*/
|
|
||||||
export const startCloudShellTerminal = async (terminal: Terminal, shellType: TerminalKind) => {
|
|
||||||
// Get the shell handler for this type
|
|
||||||
const shellHandler = ShellTypeHandler.getHandler(shellType);
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.header("Initializing Azure CloudShell"));
|
|
||||||
await ensureCloudShellProviderRegistered(terminal);
|
|
||||||
|
|
||||||
const { resolvedRegion, defaultCloudShellRegion } = determineCloudShellRegion(terminal);
|
|
||||||
|
|
||||||
// Ask for user consent for region
|
|
||||||
const consentGranted = await askForRegionConsent(terminal, resolvedRegion);
|
|
||||||
if (!consentGranted) {
|
|
||||||
return {}; // Exit if user declined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check network requirements for this shell type
|
|
||||||
const networkConfig = await shellHandler.configureNetworkAccess(terminal, resolvedRegion);
|
|
||||||
|
|
||||||
terminal.writeln("");
|
|
||||||
// Provision CloudShell session
|
|
||||||
terminal.writeln(terminalLog.cloudshell(`Provisioning Started....`));
|
|
||||||
|
|
||||||
let sessionDetails: {
|
|
||||||
socketUri?: string;
|
|
||||||
provisionConsoleResponse?: any;
|
|
||||||
targetUri?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
sessionDetails = await provisionCloudShellSession(resolvedRegion, terminal, networkConfig.vNetSettings, networkConfig.isAllPublicAccessEnabled);
|
|
||||||
} catch (err) {
|
|
||||||
terminal.writeln(terminalLog.error(err));
|
|
||||||
terminal.writeln(terminalLog.error("Failed to provision in primary region"));
|
|
||||||
terminal.writeln(terminalLog.warning(`Attempting with fallback region: ${defaultCloudShellRegion}`));
|
|
||||||
|
|
||||||
sessionDetails = await provisionCloudShellSession(defaultCloudShellRegion, terminal, networkConfig.vNetSettings, networkConfig.isAllPublicAccessEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionDetails.socketUri) {
|
|
||||||
terminal.writeln(terminalLog.error('Unable to provision console. Please try again later.'));
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure WebSocket connection with shell-specific commands
|
|
||||||
const socket = await establishTerminalConnection(
|
|
||||||
terminal,
|
|
||||||
shellHandler,
|
|
||||||
sessionDetails.socketUri,
|
|
||||||
sessionDetails.provisionConsoleResponse,
|
|
||||||
sessionDetails.targetUri
|
|
||||||
);
|
|
||||||
|
|
||||||
return socket;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures that the CloudShell provider is registered for the current subscription
|
|
||||||
*/
|
|
||||||
export const ensureCloudShellProviderRegistered = async (terminal: Terminal): Promise<void> => {
|
|
||||||
try {
|
|
||||||
terminal.writeln(terminalLog.info("Verifying provider registration..."));
|
|
||||||
const response: any = await verifyCloudShellProviderRegistration(userContext.subscriptionId);
|
|
||||||
|
|
||||||
if (response.registrationState !== "Registered") {
|
|
||||||
terminal.writeln(terminalLog.warning("Registering CloudShell provider..."));
|
|
||||||
await registerCloudShellProvider(userContext.subscriptionId);
|
|
||||||
terminal.writeln(terminalLog.success("Provider registration successful"));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
terminal.writeln(terminalLog.error("Unable to verify provider registration"));
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the appropriate CloudShell region
|
|
||||||
*/
|
|
||||||
export const determineCloudShellRegion = (terminal: Terminal): { resolvedRegion: string; defaultCloudShellRegion: string } => {
|
|
||||||
const region = userContext.databaseAccount?.location;
|
|
||||||
const resolvedRegion = getNormalizedRegion(region, DEFAULT_CLOUDSHELL_REGION);
|
|
||||||
|
|
||||||
return { resolvedRegion, defaultCloudShellRegion: DEFAULT_CLOUDSHELL_REGION };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asks the user for consent to use the specified CloudShell region
|
|
||||||
*/
|
|
||||||
export const askForRegionConsent = async (terminal: Terminal, resolvedRegion: string): Promise<boolean> => {
|
|
||||||
terminal.writeln(terminalLog.header("CloudShell Region Confirmation"));
|
|
||||||
terminal.writeln(terminalLog.info("The CloudShell container will be provisioned in a specific Azure region."));
|
|
||||||
// Data residency and compliance information
|
|
||||||
terminal.writeln(terminalLog.subheader("Important Information"));
|
|
||||||
const dbRegion = userContext.databaseAccount?.location || "unknown";
|
|
||||||
terminal.writeln(terminalLog.item("Database Region", dbRegion));
|
|
||||||
terminal.writeln(terminalLog.item("CloudShell Container Region", resolvedRegion));
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.subheader("What this means to you?"));
|
|
||||||
terminal.writeln(terminalLog.item("Data Residency", "Commands and query results will be processed in this region"));
|
|
||||||
terminal.writeln(terminalLog.item("Network", "Database connections will originate from this region"));
|
|
||||||
|
|
||||||
// Consent question
|
|
||||||
terminal.writeln("");
|
|
||||||
terminal.writeln(terminalLog.prompt("Would you like to provision Azure CloudShell in the '" + resolvedRegion + "' region?"));
|
|
||||||
terminal.writeln(terminalLog.prompt("Press 'Y' to continue or 'N' to cancel (Y/N)"));
|
|
||||||
|
|
||||||
return new Promise<boolean>((resolve) => {
|
|
||||||
const keyListener = terminal.onKey(({ key }: { key: string }) => {
|
|
||||||
keyListener.dispose();
|
|
||||||
terminal.writeln("");
|
|
||||||
|
|
||||||
if (key.toLowerCase() === 'y') {
|
|
||||||
terminal.writeln(terminalLog.success("Proceeding with CloudShell in " + resolvedRegion));
|
|
||||||
terminal.writeln(terminalLog.separator());
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
terminal.writeln(terminalLog.error("CloudShell provisioning canceled"));
|
|
||||||
setTimeout(() => terminal.dispose(), 2000);
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provisions a CloudShell session
|
|
||||||
*/
|
|
||||||
export const provisionCloudShellSession = async (
|
|
||||||
resolvedRegion: string,
|
|
||||||
terminal: Terminal,
|
|
||||||
vNetSettings: object,
|
|
||||||
isAllPublicAccessEnabled: boolean
|
|
||||||
): Promise<{ socketUri?: string; provisionConsoleResponse?: any; targetUri?: string }> => {
|
|
||||||
return new Promise( async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
terminal.writeln(terminalLog.header("Configuring CloudShell Session"));
|
|
||||||
// Check if vNetSettings is available and not empty
|
|
||||||
const hasVNetSettings = vNetSettings && Object.keys(vNetSettings).length > 0;
|
|
||||||
if (hasVNetSettings) {
|
|
||||||
terminal.writeln(terminalLog.vnet("Enabling private network configuration"));
|
|
||||||
displayNetworkSettings(terminal, vNetSettings, resolvedRegion);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
terminal.writeln(terminalLog.warning("No VNet configuration provided"));
|
|
||||||
terminal.writeln(terminalLog.warning("CloudShell will be provisioned with public network access"));
|
|
||||||
|
|
||||||
if (!isAllPublicAccessEnabled) {
|
|
||||||
terminal.writeln(terminalLog.error("Warning: Your database has network restrictions"));
|
|
||||||
terminal.writeln(terminalLog.error("CloudShell may not be able to connect without proper VNet configuration"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
terminal.writeln(terminalLog.warning("Any previous VNet settings will be overridden"));
|
|
||||||
|
|
||||||
// Apply user settings
|
|
||||||
await putEphemeralUserSettings(userContext.subscriptionId, resolvedRegion, vNetSettings);
|
|
||||||
terminal.writeln(terminalLog.success("Session settings applied"));
|
|
||||||
|
|
||||||
// Provision console
|
|
||||||
let provisionConsoleResponse;
|
|
||||||
let attemptCounter = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
provisionConsoleResponse = await provisionConsole(userContext.subscriptionId, resolvedRegion);
|
|
||||||
terminal.writeln(terminalLog.progress("Provisioning", provisionConsoleResponse.properties.provisioningState));
|
|
||||||
|
|
||||||
attemptCounter++;
|
|
||||||
|
|
||||||
if (provisionConsoleResponse.properties.provisioningState !== "Succeeded") {
|
|
||||||
await wait(POLLING_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
} while (provisionConsoleResponse.properties.provisioningState !== "Succeeded" && attemptCounter < 10);
|
|
||||||
|
|
||||||
if (provisionConsoleResponse.properties.provisioningState !== "Succeeded") {
|
|
||||||
const errorMessage = `Provisioning failed: ${provisionConsoleResponse.properties.provisioningState}`;
|
|
||||||
terminal.writeln(terminalLog.error(errorMessage));
|
|
||||||
return reject(new Error(errorMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect terminal
|
|
||||||
const connectTerminalResponse = await connectTerminal(
|
|
||||||
provisionConsoleResponse.properties.uri,
|
|
||||||
{ rows: terminal.rows, cols: terminal.cols }
|
|
||||||
);
|
|
||||||
|
|
||||||
const targetUri = `${provisionConsoleResponse.properties.uri}/terminals?cols=${terminal.cols}&rows=${terminal.rows}&version=2019-01-01&shell=bash`;
|
|
||||||
const termId = connectTerminalResponse.id;
|
|
||||||
|
|
||||||
// Determine socket URI
|
|
||||||
let socketUri = connectTerminalResponse.socketUri.replace(":443/", "");
|
|
||||||
const targetUriBody = targetUri.replace('https://', '').split('?')[0];
|
|
||||||
|
|
||||||
if (socketUri.indexOf(targetUriBody) === -1) {
|
|
||||||
socketUri = `wss://${targetUriBody}/${termId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetUriBody.includes('servicebus')) {
|
|
||||||
const targetUriBodyArr = targetUriBody.split('/');
|
|
||||||
socketUri = `wss://${targetUriBodyArr[0]}/$hc/${targetUriBodyArr[1]}/terminals/${termId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve({ socketUri, provisionConsoleResponse, targetUri });
|
|
||||||
} catch (err) {
|
|
||||||
terminal.writeln(terminalLog.error(`Provisioning failed: ${err.message}`));
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display VNet settings in the terminal
|
|
||||||
*/
|
|
||||||
const displayNetworkSettings = (terminal: Terminal, vNetSettings: any, resolvedRegion: string): void => {
|
|
||||||
if (vNetSettings.networkProfileResourceId) {
|
|
||||||
const profileName = vNetSettings.networkProfileResourceId.split('/').pop();
|
|
||||||
terminal.writeln(terminalLog.item("Network Profile", profileName));
|
|
||||||
|
|
||||||
if (vNetSettings.relayNamespaceResourceId) {
|
|
||||||
const relayName = vNetSettings.relayNamespaceResourceId.split('/').pop();
|
|
||||||
terminal.writeln(terminalLog.item("Relay Namespace", relayName));
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.item("Region", resolvedRegion));
|
|
||||||
terminal.writeln(terminalLog.success("CloudShell will use this VNet to connect to your database"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Establishes a terminal connection via WebSocket
|
|
||||||
*/
|
|
||||||
export const establishTerminalConnection = async (
|
|
||||||
terminal: Terminal,
|
|
||||||
shellHandler: any,
|
|
||||||
socketUri: string,
|
|
||||||
provisionConsoleResponse: any,
|
|
||||||
targetUri: string
|
|
||||||
): Promise<WebSocket> => {
|
|
||||||
let socket = new WebSocket(socketUri);
|
|
||||||
|
|
||||||
// Get shell-specific initial commands
|
|
||||||
const initCommands = await shellHandler.getInitialCommands();
|
|
||||||
|
|
||||||
// Configure the socket
|
|
||||||
socket = configureSocketConnection(socket, socketUri, terminal, initCommands, 0);
|
|
||||||
|
|
||||||
// Attach the terminal addon
|
|
||||||
const attachAddon = new AttachAddon(socket);
|
|
||||||
terminal.loadAddon(attachAddon);
|
|
||||||
terminal.writeln(terminalLog.success("Connection established"));
|
|
||||||
|
|
||||||
// Authorize the session
|
|
||||||
try {
|
|
||||||
const authorizeResponse = await authorizeSession(provisionConsoleResponse.properties.uri);
|
|
||||||
const cookieToken = authorizeResponse.token;
|
|
||||||
|
|
||||||
// Load auth token with a hidden image
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.src = `${targetUri}&token=${encodeURIComponent(cookieToken)}`;
|
|
||||||
terminal.focus();
|
|
||||||
} catch (err) {
|
|
||||||
terminal.writeln(terminalLog.error("Authorization failed"));
|
|
||||||
socket.close();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
return socket;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures a WebSocket connection for the terminal
|
|
||||||
*/
|
|
||||||
export const configureSocketConnection = (
|
|
||||||
socket: WebSocket,
|
|
||||||
uri: string,
|
|
||||||
terminal: Terminal,
|
|
||||||
initCommands: string,
|
|
||||||
socketRetryCount: number
|
|
||||||
): WebSocket => {
|
|
||||||
let jsonData = '';
|
|
||||||
let keepAliveID: NodeJS.Timeout = null;
|
|
||||||
let pingCount = 0;
|
|
||||||
|
|
||||||
sendTerminalStartupCommands(socket, initCommands);
|
|
||||||
|
|
||||||
socket.onclose = () => {
|
|
||||||
if (keepAliveID) {
|
|
||||||
clearTimeout(keepAliveID);
|
|
||||||
pingCount = 0;
|
|
||||||
}
|
|
||||||
terminal.writeln(terminalLog.warning("Session terminated. Refresh the page to start a new session."));
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onerror = () => {
|
|
||||||
if (socketRetryCount < MAX_RETRY_COUNT && socket.readyState !== WebSocket.CLOSED) {
|
|
||||||
configureSocketConnection(socket, uri, terminal, initCommands, socketRetryCount + 1);
|
|
||||||
} else {
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onmessage = (event: MessageEvent<string>) => {
|
|
||||||
pingCount = 0; // Reset ping count on message receipt
|
|
||||||
|
|
||||||
let eventData = '';
|
|
||||||
if (typeof event.data === "object") {
|
|
||||||
try {
|
|
||||||
const enc = new TextDecoder("utf-8");
|
|
||||||
eventData = enc.decode(event.data as any);
|
|
||||||
} catch (e) {
|
|
||||||
// Not an array buffer, ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof event.data === 'string') {
|
|
||||||
eventData = event.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process event data
|
|
||||||
if (eventData.includes("ie_us") && eventData.includes("ie_ue")) {
|
|
||||||
const statusData = eventData.split('ie_us')[1].split('ie_ue')[0];
|
|
||||||
console.log(statusData);
|
|
||||||
} else if (eventData.includes("ie_us")) {
|
|
||||||
jsonData += eventData.split('ie_us')[1];
|
|
||||||
} else if (eventData.includes("ie_ue")) {
|
|
||||||
jsonData += eventData.split('ie_ue')[0];
|
|
||||||
console.log(jsonData);
|
|
||||||
jsonData = '';
|
|
||||||
} else if (jsonData.length > 0) {
|
|
||||||
jsonData += eventData;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return socket;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends startup commands to the terminal
|
|
||||||
*/
|
|
||||||
export const sendTerminalStartupCommands = (socket: WebSocket, initCommands: string): void => {
|
|
||||||
let keepAliveID: NodeJS.Timeout = null;
|
|
||||||
let pingCount = 0;
|
|
||||||
|
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
||||||
socket.send(initCommands);
|
|
||||||
} else {
|
|
||||||
socket.onopen = () => {
|
|
||||||
socket.send(initCommands);
|
|
||||||
|
|
||||||
const keepSocketAlive = (socket: WebSocket) => {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
if (pingCount >= MAX_PING_COUNT) {
|
|
||||||
socket.close();
|
|
||||||
} else {
|
|
||||||
socket.send('');
|
|
||||||
pingCount++;
|
|
||||||
keepAliveID = setTimeout(() => keepSocketAlive(socket), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
keepSocketAlive(socket);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ApiVersionsConfig, ResourceType } from "Explorer/Tabs/CloudShellTab/DataModels";
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { configContext } from "../../../ConfigContext";
|
|
||||||
import { TerminalKind } from "../../../Contracts/ViewModels";
|
|
||||||
import { userContext } from '../../../UserContext';
|
|
||||||
import { armRequest } from "../../../Utils/arm/request";
|
|
||||||
import { Authorization, ConnectTerminalResponse, NetworkType, OsType, ProvisionConsoleResponse, SessionType, Settings, ShellType } from "./DataModels";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API version configuration by terminal type and resource type
|
|
||||||
*/
|
|
||||||
const API_VERSIONS : ApiVersionsConfig = {
|
|
||||||
// Default version for fallback
|
|
||||||
DEFAULT: "2024-07-01",
|
|
||||||
|
|
||||||
// Resource type specific defaults
|
|
||||||
RESOURCE_DEFAULTS: {
|
|
||||||
[ResourceType.NETWORK]: "2023-05-01",
|
|
||||||
[ResourceType.DATABASE]: "2024-07-01",
|
|
||||||
[ResourceType.VNET]: "2023-05-01",
|
|
||||||
[ResourceType.SUBNET]: "2023-05-01",
|
|
||||||
[ResourceType.RELAY]: "2024-01-01",
|
|
||||||
[ResourceType.ROLE]: "2022-04-01"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Shell-type specific versions with resource overrides
|
|
||||||
SHELL_TYPES: {
|
|
||||||
[TerminalKind.Mongo]: {
|
|
||||||
[ResourceType.DATABASE]: "2024-11-15"
|
|
||||||
},
|
|
||||||
[TerminalKind.VCoreMongo]: {
|
|
||||||
[ResourceType.DATABASE]: "2024-07-01"
|
|
||||||
},
|
|
||||||
[TerminalKind.Cassandra]: {
|
|
||||||
[ResourceType.DATABASE]: "2024-11-15"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateUserSettings = (userSettings: Settings) => {
|
|
||||||
if (userSettings.sessionType !== SessionType.Ephemeral && userSettings.osType !== OsType.Linux) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current shell type context
|
|
||||||
let currentShellType: TerminalKind | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the active shell type to determine API version
|
|
||||||
*/
|
|
||||||
export const setShellType = (shellType: TerminalKind): void => {
|
|
||||||
currentShellType = shellType;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the appropriate API version based on shell type and resource type
|
|
||||||
* Uses a cascading fallback approach for maximum flexibility
|
|
||||||
*/
|
|
||||||
export const getApiVersion = (resourceType?: ResourceType): string => {
|
|
||||||
// If no shell type is set, fallback to resource default or global default
|
|
||||||
if (!currentShellType) {
|
|
||||||
return resourceType ?
|
|
||||||
(API_VERSIONS.RESOURCE_DEFAULTS[resourceType] || API_VERSIONS.DEFAULT) :
|
|
||||||
API_VERSIONS.DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shell type is set, try to get specific version in this priority:
|
|
||||||
// 1. Shell-specific + resource-specific
|
|
||||||
if (resourceType &&
|
|
||||||
API_VERSIONS.SHELL_TYPES[currentShellType]) {
|
|
||||||
const shellTypeConfig = API_VERSIONS.SHELL_TYPES[currentShellType];
|
|
||||||
if (resourceType in shellTypeConfig) {
|
|
||||||
return shellTypeConfig[resourceType] as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Resource-specific default
|
|
||||||
if (resourceType && resourceType in API_VERSIONS.RESOURCE_DEFAULTS) {
|
|
||||||
return API_VERSIONS.RESOURCE_DEFAULTS[resourceType];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Global default
|
|
||||||
return API_VERSIONS.DEFAULT;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserRegion = async (subscriptionId: string, resourceGroup: string, accountName: string) => {
|
|
||||||
return await armRequest({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`,
|
|
||||||
method: "GET",
|
|
||||||
apiVersion: "2022-12-01"
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteUserSettings = async (): Promise<void> => {
|
|
||||||
await armRequest<void>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
|
|
||||||
method: "DELETE",
|
|
||||||
apiVersion: "2023-02-01-preview"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserSettings = async (): Promise<Settings> => {
|
|
||||||
const resp = await armRequest<any>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
|
|
||||||
method: "GET",
|
|
||||||
apiVersion: "2023-02-01-preview"
|
|
||||||
});
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putEphemeralUserSettings = async (userSubscriptionId: string, userRegion: string, vNetSettings?: object) => {
|
|
||||||
const ephemeralSettings = {
|
|
||||||
properties: {
|
|
||||||
preferredOsType: OsType.Linux,
|
|
||||||
preferredShellType: ShellType.Bash,
|
|
||||||
preferredLocation: userRegion,
|
|
||||||
networkType: (!vNetSettings || Object.keys(vNetSettings).length === 0) ? NetworkType.Default : (vNetSettings ? NetworkType.Isolated : NetworkType.Default),
|
|
||||||
sessionType: SessionType.Ephemeral,
|
|
||||||
userSubscription: userSubscriptionId,
|
|
||||||
vnetSettings: vNetSettings ?? {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return await armRequest({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
|
|
||||||
method: "PUT",
|
|
||||||
apiVersion: "2023-02-01-preview",
|
|
||||||
body: ephemeralSettings
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const verifyCloudShellProviderRegistration = async(subscriptionId: string) => {
|
|
||||||
return await armRequest({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell`,
|
|
||||||
method: "GET",
|
|
||||||
apiVersion: "2022-12-01"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const registerCloudShellProvider = async (subscriptionId: string) => {
|
|
||||||
return await armRequest({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell/register`,
|
|
||||||
method: "POST",
|
|
||||||
apiVersion: "2022-12-01"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const provisionConsole = async (subscriptionId: string, location: string): Promise<ProvisionConsoleResponse> => {
|
|
||||||
const data = {
|
|
||||||
properties: {
|
|
||||||
osType: OsType.Linux
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return await armRequest<any>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `providers/Microsoft.Portal/consoles/default`,
|
|
||||||
method: "PUT",
|
|
||||||
apiVersion: "2023-02-01-preview",
|
|
||||||
customHeaders: {
|
|
||||||
'x-ms-console-preferred-location': location
|
|
||||||
},
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const connectTerminal = async (consoleUri: string, size: { rows: number, cols: number }): Promise<ConnectTerminalResponse> => {
|
|
||||||
const targetUri = consoleUri + `/terminals?cols=${size.cols}&rows=${size.rows}&version=2019-01-01&shell=bash`;
|
|
||||||
const resp = await fetch(targetUri, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': '2',
|
|
||||||
'Authorization': userContext.authorizationToken,
|
|
||||||
'x-ms-client-request-id': uuidv4(),
|
|
||||||
'Accept-Language': getLocale(),
|
|
||||||
},
|
|
||||||
body: "{}" // empty body is necessary
|
|
||||||
});
|
|
||||||
return resp.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const authorizeSession = async (consoleUri: string): Promise<Authorization> => {
|
|
||||||
const targetUri = consoleUri + "/authorize";
|
|
||||||
const resp = await fetch(targetUri, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Authorization': userContext.authorizationToken,
|
|
||||||
'Accept-Language': getLocale(),
|
|
||||||
"Content-Type": 'application/json'
|
|
||||||
},
|
|
||||||
body: "{}" // empty body is necessary
|
|
||||||
});
|
|
||||||
return resp.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLocale = () => {
|
|
||||||
const langLocale = navigator.language;
|
|
||||||
return (langLocale && langLocale.length === 2 ? langLocale[1] : 'en-us');
|
|
||||||
};
|
|
||||||
|
|
||||||
const validCloudShellRegions = new Set(["westus", "southcentralus", "eastus", "northeurope", "westeurope", "centralindia", "southeastasia", "westcentralus"]);
|
|
||||||
|
|
||||||
export const getNormalizedRegion = (region: string, defaultCloudshellRegion: string) => {
|
|
||||||
if (!region) return defaultCloudshellRegion;
|
|
||||||
|
|
||||||
const regionMap: Record<string, string> = {
|
|
||||||
"centralus": "westcentralus",
|
|
||||||
"eastus2": "eastus"
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizedRegion = regionMap[region.toLowerCase()] || region;
|
|
||||||
return validCloudShellRegions.has(normalizedRegion.toLowerCase()) ? normalizedRegion : defaultCloudshellRegion;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getNetworkProfileInfo<T>(networkProfileResourceId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.NETWORK);
|
|
||||||
return await GetARMCall<T>(networkProfileResourceId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAccountDetails<T>(databaseAccountId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.DATABASE);
|
|
||||||
return await GetARMCall<T>(databaseAccountId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getVnetInformation<T>(vnetId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.VNET);
|
|
||||||
return await GetARMCall<T>(vnetId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSubnetInformation<T>(subnetId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.SUBNET);
|
|
||||||
return await GetARMCall<T>(subnetId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateSubnetInformation<T>(subnetId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.SUBNET);
|
|
||||||
return await PutARMCall(subnetId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateDatabaseAccount<T>(accountId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.DATABASE);
|
|
||||||
return await PutARMCall(accountId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDatabaseOperations<T>(accountId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.DATABASE);
|
|
||||||
return await GetARMCall<T>(`${accountId}/operations`, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateVnet<T>(vnetId: string, request: object, apiVersionOverride?: string) {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.VNET);
|
|
||||||
return await PutARMCall<T>(vnetId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getVnet<T>(vnetId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.VNET);
|
|
||||||
return await GetARMCall<T>(vnetId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createNetworkProfile<T>(networkProfileId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.NETWORK);
|
|
||||||
return await PutARMCall<T>(networkProfileId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRelay<T>(relayId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.RELAY);
|
|
||||||
return await PutARMCall<T>(relayId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRelay<T>(relayId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.RELAY);
|
|
||||||
return await GetARMCall<T>(relayId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRoleOnNetworkProfile<T>(roleid: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.ROLE);
|
|
||||||
return await PutARMCall<T>(roleid, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRoleOnRelay<T>(roleid: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.ROLE);
|
|
||||||
return await PutARMCall<T>(roleid, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GetARMCall<T>(path: string, apiVersion: string = API_VERSIONS.DEFAULT): Promise<T> {
|
|
||||||
return await armRequest<T>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: path,
|
|
||||||
method: "GET",
|
|
||||||
apiVersion: apiVersion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PutARMCall<T>(path: string, request: object, apiVersion: string = API_VERSIONS.DEFAULT): Promise<T> {
|
|
||||||
return await armRequest<T>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: path,
|
|
||||||
method: "PUT",
|
|
||||||
apiVersion: apiVersion,
|
|
||||||
body: request
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* CloudShell API client for various operations
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { configContext } from "../../../../ConfigContext";
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { userContext } from '../../../../UserContext';
|
|
||||||
import { armRequest } from "../../../../Utils/arm/request";
|
|
||||||
import { ApiVersionsConfig, DEFAULT_API_VERSIONS } from "../Models/ApiVersions";
|
|
||||||
import { Authorization, ConnectTerminalResponse, NetworkType, OsType, ProvisionConsoleResponse, ResourceType, SessionType, Settings, ShellType } from "../Models/DataModels";
|
|
||||||
import { getLocale } from '../Data/LocalizationUtils';
|
|
||||||
|
|
||||||
// Current shell type context
|
|
||||||
let currentShellType: TerminalKind | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the active shell type to determine API version
|
|
||||||
*/
|
|
||||||
export const setShellType = (shellType: TerminalKind): void => {
|
|
||||||
currentShellType = shellType;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the appropriate API version based on shell type and resource type
|
|
||||||
*/
|
|
||||||
export const getApiVersion = (resourceType?: ResourceType, apiVersions?: ApiVersionsConfig): string => {
|
|
||||||
if (!apiVersions) {
|
|
||||||
apiVersions = DEFAULT_API_VERSIONS; // Default fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shell type is set, try to get specific version in this priority:
|
|
||||||
// 1. Shell-specific + resource-specific
|
|
||||||
if (resourceType &&
|
|
||||||
apiVersions.SHELL_TYPES[currentShellType]) {
|
|
||||||
const shellTypeConfig = apiVersions.SHELL_TYPES[currentShellType];
|
|
||||||
if (resourceType in shellTypeConfig) {
|
|
||||||
return shellTypeConfig[resourceType] as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Resource-specific default
|
|
||||||
if (resourceType && resourceType in apiVersions.RESOURCE_DEFAULTS) {
|
|
||||||
return apiVersions.RESOURCE_DEFAULTS[resourceType];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Global default
|
|
||||||
return apiVersions.DEFAULT;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserRegion = async (subscriptionId: string, resourceGroup: string, accountName: string) => {
|
|
||||||
return await armRequest({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`,
|
|
||||||
method: "GET",
|
|
||||||
apiVersion: "2022-12-01"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteUserSettings = async (): Promise<void> => {
|
|
||||||
await armRequest<void>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
|
|
||||||
method: "DELETE",
|
|
||||||
apiVersion: "2023-02-01-preview"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserSettings = async (): Promise<Settings> => {
|
|
||||||
const resp = await armRequest<any>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
|
|
||||||
method: "GET",
|
|
||||||
apiVersion: "2023-02-01-preview"
|
|
||||||
});
|
|
||||||
return resp;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putEphemeralUserSettings = async (userSubscriptionId: string, userRegion: string, vNetSettings?: object) => {
|
|
||||||
const ephemeralSettings = {
|
|
||||||
properties: {
|
|
||||||
preferredOsType: OsType.Linux,
|
|
||||||
preferredShellType: ShellType.Bash,
|
|
||||||
preferredLocation: userRegion,
|
|
||||||
networkType: (!vNetSettings || Object.keys(vNetSettings).length === 0) ? NetworkType.Default : (vNetSettings ? NetworkType.Isolated : NetworkType.Default),
|
|
||||||
sessionType: SessionType.Ephemeral,
|
|
||||||
userSubscription: userSubscriptionId,
|
|
||||||
vnetSettings: vNetSettings ?? {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return await armRequest({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
|
|
||||||
method: "PUT",
|
|
||||||
apiVersion: "2023-02-01-preview",
|
|
||||||
body: ephemeralSettings
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const verifyCloudShellProviderRegistration = async(subscriptionId: string) => {
|
|
||||||
return await armRequest({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell`,
|
|
||||||
method: "GET",
|
|
||||||
apiVersion: "2022-12-01"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const registerCloudShellProvider = async (subscriptionId: string) => {
|
|
||||||
return await armRequest({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell/register`,
|
|
||||||
method: "POST",
|
|
||||||
apiVersion: "2022-12-01"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const provisionConsole = async (subscriptionId: string, location: string): Promise<ProvisionConsoleResponse> => {
|
|
||||||
const data = {
|
|
||||||
properties: {
|
|
||||||
osType: OsType.Linux
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return await armRequest<any>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: `providers/Microsoft.Portal/consoles/default`,
|
|
||||||
method: "PUT",
|
|
||||||
apiVersion: "2023-02-01-preview",
|
|
||||||
customHeaders: {
|
|
||||||
'x-ms-console-preferred-location': location
|
|
||||||
},
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const connectTerminal = async (consoleUri: string, size: { rows: number, cols: number }): Promise<ConnectTerminalResponse> => {
|
|
||||||
const targetUri = consoleUri + `/terminals?cols=${size.cols}&rows=${size.rows}&version=2019-01-01&shell=bash`;
|
|
||||||
const resp = await fetch(targetUri, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': '2',
|
|
||||||
'Authorization': userContext.authorizationToken,
|
|
||||||
'x-ms-client-request-id': uuidv4(),
|
|
||||||
'Accept-Language': getLocale(),
|
|
||||||
},
|
|
||||||
body: "{}" // empty body is necessary
|
|
||||||
});
|
|
||||||
return resp.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const authorizeSession = async (consoleUri: string): Promise<Authorization> => {
|
|
||||||
const targetUri = consoleUri + "/authorize";
|
|
||||||
const resp = await fetch(targetUri, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Authorization': userContext.authorizationToken,
|
|
||||||
'Accept-Language': getLocale(),
|
|
||||||
"Content-Type": 'application/json'
|
|
||||||
},
|
|
||||||
body: "{}" // empty body is necessary
|
|
||||||
});
|
|
||||||
return resp.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getNetworkProfileInfo<T>(networkProfileResourceId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.NETWORK);
|
|
||||||
return await GetARMCall<T>(networkProfileResourceId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAccountDetails<T>(databaseAccountId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.DATABASE);
|
|
||||||
return await GetARMCall<T>(databaseAccountId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getVnetInformation<T>(vnetId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.VNET);
|
|
||||||
return await GetARMCall<T>(vnetId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSubnetInformation<T>(subnetId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.SUBNET);
|
|
||||||
return await GetARMCall<T>(subnetId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateSubnetInformation<T>(subnetId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.SUBNET);
|
|
||||||
return await PutARMCall<T>(subnetId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateDatabaseAccount<T>(accountId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.DATABASE);
|
|
||||||
return await PutARMCall<T>(accountId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDatabaseOperations<T>(accountId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.DATABASE);
|
|
||||||
return await GetARMCall<T>(`${accountId}/operations`, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateVnet<T>(vnetId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.VNET);
|
|
||||||
return await PutARMCall<T>(vnetId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getVnet<T>(vnetId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.VNET);
|
|
||||||
return await GetARMCall<T>(vnetId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createNetworkProfile<T>(networkProfileId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.NETWORK);
|
|
||||||
return await PutARMCall<T>(networkProfileId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRelay<T>(relayId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.RELAY);
|
|
||||||
return await PutARMCall<T>(relayId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRelay<T>(relayId: string, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.RELAY);
|
|
||||||
return await GetARMCall<T>(relayId, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRoleOnNetworkProfile<T>(roleId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.ROLE);
|
|
||||||
return await PutARMCall<T>(roleId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRoleOnRelay<T>(roleId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.ROLE);
|
|
||||||
return await PutARMCall<T>(roleId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createPrivateEndpoint<T>(privateEndpointId: string, request: object, apiVersionOverride?: string): Promise<T> {
|
|
||||||
const apiVersion = apiVersionOverride || getApiVersion(ResourceType.NETWORK);
|
|
||||||
return await PutARMCall<T>(privateEndpointId, request, apiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GetARMCall<T>(path: string, apiVersion: string = '2024-07-01'): Promise<T> {
|
|
||||||
return await armRequest<T>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: path,
|
|
||||||
method: "GET",
|
|
||||||
apiVersion: apiVersion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PutARMCall<T>(path: string, request: object, apiVersion: string = '2024-07-01'): Promise<T> {
|
|
||||||
return await armRequest<T>({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: path,
|
|
||||||
method: "PUT",
|
|
||||||
apiVersion: apiVersion,
|
|
||||||
body: request
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Localization utilities for CloudShell
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current locale for API requests
|
|
||||||
*/
|
|
||||||
export const getLocale = (): string => {
|
|
||||||
const langLocale = navigator.language;
|
|
||||||
return (langLocale && langLocale.length > 2 ? langLocale : 'en-us');
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Region utilities for CloudShell
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const getLocale = () => {
|
|
||||||
const langLocale = navigator.language;
|
|
||||||
return (langLocale && langLocale.length === 2 ? langLocale[1] : 'en-us');
|
|
||||||
};
|
|
||||||
|
|
||||||
const validCloudShellRegions = new Set([
|
|
||||||
"westus",
|
|
||||||
"southcentralus",
|
|
||||||
"eastus",
|
|
||||||
"northeurope",
|
|
||||||
"westeurope",
|
|
||||||
"centralindia",
|
|
||||||
"southeastasia",
|
|
||||||
"westcentralus"
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes a region name to a valid CloudShell region
|
|
||||||
* @param region The region to normalize
|
|
||||||
* @param defaultCloudshellRegion Default region to use if the provided region is not supported
|
|
||||||
*/
|
|
||||||
export const getNormalizedRegion = (region: string, defaultCloudshellRegion: string) => {
|
|
||||||
if (!region) return defaultCloudshellRegion;
|
|
||||||
|
|
||||||
const regionMap: Record<string, string> = {
|
|
||||||
"centralus": "westcentralus",
|
|
||||||
"eastus2": "eastus"
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizedRegion = regionMap[region.toLowerCase()] || region;
|
|
||||||
return validCloudShellRegions.has(normalizedRegion.toLowerCase()) ? normalizedRegion : defaultCloudshellRegion;
|
|
||||||
};
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
*/
|
|
||||||
import { TerminalKind } from "../../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
export const enum OsType {
|
|
||||||
Linux = "linux",
|
|
||||||
Windows = "windows"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum ShellType {
|
|
||||||
Bash = "bash",
|
|
||||||
PowerShellCore = "pwsh"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum NetworkType {
|
|
||||||
Default = "Default",
|
|
||||||
Isolated = "Isolated"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum SessionType {
|
|
||||||
Mounted = "Mounted",
|
|
||||||
Ephemeral = "Ephemeral"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum UserInputs {
|
|
||||||
NoReset = "1",
|
|
||||||
ConfigureVNet = "2",
|
|
||||||
ResetVNet = "3"
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Settings = {
|
|
||||||
properties: UserSettingProperties
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UserSettingProperties = {
|
|
||||||
networkType: string;
|
|
||||||
preferredLocation: string;
|
|
||||||
preferredOsType: OsType;
|
|
||||||
preferredShellType: ShellType;
|
|
||||||
userSubscription: string;
|
|
||||||
sessionType: SessionType;
|
|
||||||
vnetSettings: VnetSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VnetSettings = {
|
|
||||||
networkProfileResourceId?: string;
|
|
||||||
relayNamespaceResourceId?: string;
|
|
||||||
location?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProvisionConsoleResponse = {
|
|
||||||
properties: {
|
|
||||||
osType: OsType;
|
|
||||||
provisioningState: string;
|
|
||||||
uri: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Authorization = {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConnectTerminalResponse = {
|
|
||||||
id: string;
|
|
||||||
idleTimeout: string;
|
|
||||||
rootDirectory: string;
|
|
||||||
socketUri: string;
|
|
||||||
tokenUpdated: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type VnetModel = {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
etag: string;
|
|
||||||
type: string;
|
|
||||||
location: string;
|
|
||||||
tags: Record<string, string>;
|
|
||||||
properties: {
|
|
||||||
provisioningState: string;
|
|
||||||
resourceGuid: string;
|
|
||||||
addressSpace: {
|
|
||||||
addressPrefixes: string[];
|
|
||||||
};
|
|
||||||
encryption: {
|
|
||||||
enabled: boolean;
|
|
||||||
enforcement: string;
|
|
||||||
};
|
|
||||||
privateEndpointVNetPolicies: string;
|
|
||||||
subnets: Array<{
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
etag: string;
|
|
||||||
type: string;
|
|
||||||
properties: {
|
|
||||||
provisioningState: string;
|
|
||||||
addressPrefixes?: string[];
|
|
||||||
addressPrefix?: string;
|
|
||||||
networkSecurityGroup?: { id: string };
|
|
||||||
ipConfigurations?: { id: string }[];
|
|
||||||
ipConfigurationProfiles?: { id: string }[];
|
|
||||||
privateEndpoints?: { id: string }[];
|
|
||||||
serviceEndpoints?: Array<{
|
|
||||||
provisioningState: string;
|
|
||||||
service: string;
|
|
||||||
locations: string[];
|
|
||||||
}>;
|
|
||||||
delegations?: Array<{
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
etag: string;
|
|
||||||
type: string;
|
|
||||||
properties: {
|
|
||||||
provisioningState: string;
|
|
||||||
serviceName: string;
|
|
||||||
actions: string[];
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
purpose?: string;
|
|
||||||
privateEndpointNetworkPolicies?: string;
|
|
||||||
privateLinkServiceNetworkPolicies?: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
virtualNetworkPeerings: any[];
|
|
||||||
enableDdosProtection: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RelayNamespace = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
location: string;
|
|
||||||
tags: Record<string, string>;
|
|
||||||
properties: {
|
|
||||||
metricId: string;
|
|
||||||
serviceBusEndpoint: string;
|
|
||||||
provisioningState: string;
|
|
||||||
status: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
sku: {
|
|
||||||
name: string;
|
|
||||||
tier: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RelayNamespaceResponse = {
|
|
||||||
value: RelayNamespace[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resource types for API versioning
|
|
||||||
*/
|
|
||||||
export enum ResourceType {
|
|
||||||
NETWORK = "NETWORK",
|
|
||||||
DATABASE = "DATABASE",
|
|
||||||
VNET = "VNET",
|
|
||||||
SUBNET = "SUBNET",
|
|
||||||
RELAY = "RELAY",
|
|
||||||
ROLE = "ROLE"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type definition for API_VERSIONS configuration
|
|
||||||
export type ApiVersionsConfig = {
|
|
||||||
// Global default API version
|
|
||||||
DEFAULT: string;
|
|
||||||
|
|
||||||
// Resource-specific default API versions
|
|
||||||
RESOURCE_DEFAULTS: {
|
|
||||||
[key in ResourceType]: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Shell-type specific configurations
|
|
||||||
SHELL_TYPES: {
|
|
||||||
[key in TerminalKind]?: {
|
|
||||||
// Resource-specific overrides for this shell type
|
|
||||||
[key in ResourceType]?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Standardized terminal logging functions for consistent formatting
|
|
||||||
*/
|
|
||||||
export const terminalLog = {
|
|
||||||
// Section headers
|
|
||||||
header: (message: string) => `\n\x1B[1;34m┌─ ${message} ${"─".repeat(Math.max(45 - message.length, 0))}\x1B[0m`,
|
|
||||||
subheader: (message: string) => `\x1B[1;36m├ ${message}\x1B[0m`,
|
|
||||||
sectionEnd: () => `\x1B[1;34m└${"─".repeat(50)}\x1B[0m\n`,
|
|
||||||
|
|
||||||
// Status messages
|
|
||||||
success: (message: string) => `\x1B[32m✓ ${message}\x1B[0m`,
|
|
||||||
warning: (message: string) => `\x1B[33m⚠ ${message}\x1B[0m`,
|
|
||||||
error: (message: string) => `\x1B[31m✗ ${message}\x1B[0m`,
|
|
||||||
info: (message: string) => `\x1B[34m${message}\x1B[0m`,
|
|
||||||
|
|
||||||
// Resource information
|
|
||||||
database: (message: string) => `\x1B[35m🔶 Database: ${message}\x1B[0m`,
|
|
||||||
vnet: (message: string) => `\x1B[36m🔷 Network: ${message}\x1B[0m`,
|
|
||||||
cloudshell: (message: string) => `\x1B[32m🔷 CloudShell: ${message}\x1B[0m`,
|
|
||||||
|
|
||||||
// Data formatting
|
|
||||||
item: (label: string, value: string) => ` • ${label}: \x1B[32m${value}\x1B[0m`,
|
|
||||||
progress: (operation: string, status: string) => `\x1B[34m${operation}: \x1B[36m${status}\x1B[0m`,
|
|
||||||
|
|
||||||
// User interaction
|
|
||||||
prompt: (message: string) => `\x1B[1;37m${message}\x1B[0m`,
|
|
||||||
separator: () => `\x1B[30;1m${"─".repeat(50)}\x1B[0m`
|
|
||||||
};
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* API versions configuration for CloudShell
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { ResourceType } from "./DataModels";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for API versions used by the CloudShell
|
|
||||||
*/
|
|
||||||
export type ApiVersionsConfig = {
|
|
||||||
DEFAULT: string;
|
|
||||||
RESOURCE_DEFAULTS: Record<ResourceType, string>;
|
|
||||||
SHELL_TYPES: Record<TerminalKind, Record<ResourceType, string>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default API versions configuration
|
|
||||||
*/
|
|
||||||
export const DEFAULT_API_VERSIONS: ApiVersionsConfig = {
|
|
||||||
DEFAULT: '2024-07-01',
|
|
||||||
RESOURCE_DEFAULTS: {
|
|
||||||
[ResourceType.DATABASE]: '2024-11-15',
|
|
||||||
[ResourceType.NETWORK]: '2024-07-01',
|
|
||||||
[ResourceType.VNET]: '2024-07-01',
|
|
||||||
[ResourceType.SUBNET]: '2024-07-01',
|
|
||||||
[ResourceType.RELAY]: '2022-10-01',
|
|
||||||
[ResourceType.ROLE]: '2022-04-01',
|
|
||||||
},
|
|
||||||
SHELL_TYPES: {
|
|
||||||
[TerminalKind.Mongo]: {
|
|
||||||
[ResourceType.DATABASE]: '2024-11-15',
|
|
||||||
[ResourceType.NETWORK]: '2024-07-01',
|
|
||||||
[ResourceType.VNET]: '2024-07-01',
|
|
||||||
[ResourceType.SUBNET]: '2024-07-01',
|
|
||||||
[ResourceType.RELAY]: '2024-01-01',
|
|
||||||
[ResourceType.ROLE]: '2022-04-01',
|
|
||||||
},
|
|
||||||
[TerminalKind.VCoreMongo]: {
|
|
||||||
[ResourceType.DATABASE]: '2024-07-01',
|
|
||||||
[ResourceType.NETWORK]: '2024-07-01',
|
|
||||||
[ResourceType.VNET]: '2024-07-01',
|
|
||||||
[ResourceType.SUBNET]: '2024-07-01',
|
|
||||||
[ResourceType.RELAY]: '2024-01-01',
|
|
||||||
[ResourceType.ROLE]: '2022-04-01',
|
|
||||||
},
|
|
||||||
[TerminalKind.Postgres]: {
|
|
||||||
[ResourceType.DATABASE]: '2024-11-15',
|
|
||||||
[ResourceType.NETWORK]: '2024-07-01',
|
|
||||||
[ResourceType.VNET]: '2024-07-01',
|
|
||||||
[ResourceType.SUBNET]: '2024-07-01',
|
|
||||||
[ResourceType.RELAY]: '2024-01-01',
|
|
||||||
[ResourceType.ROLE]: '2022-04-01',
|
|
||||||
},
|
|
||||||
[TerminalKind.Cassandra]: {
|
|
||||||
[ResourceType.DATABASE]: '2024-11-15',
|
|
||||||
[ResourceType.NETWORK]: '2024-07-01',
|
|
||||||
[ResourceType.VNET]: '2024-07-01',
|
|
||||||
[ResourceType.SUBNET]: '2024-07-01',
|
|
||||||
[ResourceType.RELAY]: '2024-01-01',
|
|
||||||
[ResourceType.ROLE]: '2022-04-01',
|
|
||||||
},
|
|
||||||
[TerminalKind.Default]: {
|
|
||||||
[ResourceType.DATABASE]: undefined,
|
|
||||||
[ResourceType.NETWORK]: undefined,
|
|
||||||
[ResourceType.VNET]: undefined,
|
|
||||||
[ResourceType.SUBNET]: undefined,
|
|
||||||
[ResourceType.RELAY]: undefined,
|
|
||||||
[ResourceType.ROLE]: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Data models for CloudShell
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const enum OsType {
|
|
||||||
Linux = "linux",
|
|
||||||
Windows = "windows"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum ShellType {
|
|
||||||
Bash = "bash",
|
|
||||||
PowerShellCore = "pwsh"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum NetworkType {
|
|
||||||
Default = "Default",
|
|
||||||
Isolated = "Isolated"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum SessionType {
|
|
||||||
Mounted = "Mounted",
|
|
||||||
Ephemeral = "Ephemeral"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum UserInputs {
|
|
||||||
NoReset = "1",
|
|
||||||
ConfigureVNet = "2",
|
|
||||||
ResetVNet = "3"
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Settings = {
|
|
||||||
properties: UserSettingProperties
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UserSettingProperties = {
|
|
||||||
networkType: string;
|
|
||||||
preferredLocation: string;
|
|
||||||
preferredOsType: OsType;
|
|
||||||
preferredShellType: ShellType;
|
|
||||||
userSubscription: string;
|
|
||||||
sessionType: SessionType;
|
|
||||||
vnetSettings: VnetSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VnetSettings = {
|
|
||||||
networkProfileResourceId?: string;
|
|
||||||
relayNamespaceResourceId?: string;
|
|
||||||
location?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProvisionConsoleResponse = {
|
|
||||||
properties: {
|
|
||||||
osType: OsType;
|
|
||||||
provisioningState: string;
|
|
||||||
uri: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Authorization = {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConnectTerminalResponse = {
|
|
||||||
id: string;
|
|
||||||
idleTimeout: string;
|
|
||||||
rootDirectory: string;
|
|
||||||
socketUri: string;
|
|
||||||
tokenUpdated: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type VnetModel = {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
etag: string;
|
|
||||||
type: string;
|
|
||||||
location: string;
|
|
||||||
tags: Record<string, string>;
|
|
||||||
properties: {
|
|
||||||
provisioningState: string;
|
|
||||||
resourceGuid: string;
|
|
||||||
addressSpace: {
|
|
||||||
addressPrefixes: string[];
|
|
||||||
};
|
|
||||||
encryption: {
|
|
||||||
enabled: boolean;
|
|
||||||
enforcement: string;
|
|
||||||
};
|
|
||||||
privateEndpointVNetPolicies: string;
|
|
||||||
subnets: Array<{
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
etag: string;
|
|
||||||
type: string;
|
|
||||||
properties: {
|
|
||||||
provisioningState: string;
|
|
||||||
addressPrefixes?: string[];
|
|
||||||
addressPrefix?: string;
|
|
||||||
networkSecurityGroup?: { id: string };
|
|
||||||
ipConfigurations?: { id: string }[];
|
|
||||||
ipConfigurationProfiles?: { id: string }[];
|
|
||||||
privateEndpoints?: { id: string }[];
|
|
||||||
serviceEndpoints?: Array<{
|
|
||||||
provisioningState: string;
|
|
||||||
service: string;
|
|
||||||
locations: string[];
|
|
||||||
}>;
|
|
||||||
delegations?: Array<{
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
etag: string;
|
|
||||||
type: string;
|
|
||||||
properties: {
|
|
||||||
provisioningState: string;
|
|
||||||
serviceName: string;
|
|
||||||
actions: string[];
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
purpose?: string;
|
|
||||||
privateEndpointNetworkPolicies?: string;
|
|
||||||
privateLinkServiceNetworkPolicies?: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
virtualNetworkPeerings: any[];
|
|
||||||
enableDdosProtection: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RelayNamespace = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
location: string;
|
|
||||||
tags: Record<string, string>;
|
|
||||||
properties: {
|
|
||||||
metricId: string;
|
|
||||||
serviceBusEndpoint: string;
|
|
||||||
provisioningState: string;
|
|
||||||
status: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
sku: {
|
|
||||||
name: string;
|
|
||||||
tier: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RelayNamespaceResponse = {
|
|
||||||
value: RelayNamespace[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resource types for API versioning
|
|
||||||
*/
|
|
||||||
export enum ResourceType {
|
|
||||||
NETWORK = "NETWORK",
|
|
||||||
DATABASE = "DATABASE",
|
|
||||||
VNET = "VNET",
|
|
||||||
SUBNET = "SUBNET",
|
|
||||||
RELAY = "RELAY",
|
|
||||||
ROLE = "ROLE"
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Firewall handling functionality for CloudShell
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { userContext } from "../../../../UserContext";
|
|
||||||
import { hasFirewallRestrictions } from "../../Shared/CheckFirewallRules";
|
|
||||||
import { getAccountDetails, updateDatabaseAccount } from "../Data/CloudShellApiClient";
|
|
||||||
import { askConfirmation } from "../Utils/CommonUtils";
|
|
||||||
import { terminalLog } from "../Utils/LogFormatter";
|
|
||||||
|
|
||||||
export class FirewallHandler {
|
|
||||||
/**
|
|
||||||
* Checks if firewall configuration is needed for CloudShell
|
|
||||||
*/
|
|
||||||
public static async checkFirewallConfiguration(terminal: Terminal): Promise<boolean> {
|
|
||||||
if (!hasFirewallRestrictions()) {
|
|
||||||
return false; // No firewall rules to configure
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.header("Database Firewall Configuration"));
|
|
||||||
terminal.writeln(terminalLog.warning("Your database has firewall restrictions enabled"));
|
|
||||||
terminal.writeln(terminalLog.warning("CloudShell might need access through these restrictions"));
|
|
||||||
|
|
||||||
const shouldConfigureFirewall = await askConfirmation(
|
|
||||||
terminal,
|
|
||||||
"Would you like to check and configure firewall settings?"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!shouldConfigureFirewall) {
|
|
||||||
terminal.writeln(terminalLog.info("Skipping firewall configuration"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.configureFirewallForCloudShell(terminal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures firewall for CloudShell access
|
|
||||||
*/
|
|
||||||
private static async configureFirewallForCloudShell(terminal: Terminal): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
// Get current database account details
|
|
||||||
terminal.writeln(terminalLog.database("Retrieving current firewall configuration..."));
|
|
||||||
const dbAccount = userContext.databaseAccount;
|
|
||||||
const currentDbAccount = await getAccountDetails(dbAccount.id);
|
|
||||||
|
|
||||||
// Check if "Allow Azure Services" is already enabled
|
|
||||||
const ipRules = currentDbAccount.properties.ipRules || [];
|
|
||||||
const azureServicesEnabled = currentDbAccount.properties.publicNetworkAccess === "Enabled";
|
|
||||||
|
|
||||||
if (azureServicesEnabled) {
|
|
||||||
terminal.writeln(terminalLog.success("Azure services access is already enabled"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask user to enable Azure services access
|
|
||||||
terminal.writeln(terminalLog.warning("Azure services access is not enabled"));
|
|
||||||
terminal.writeln(terminalLog.info("CloudShell requires 'Allow Azure Services' to be enabled"));
|
|
||||||
|
|
||||||
const enableAzureServices = await askConfirmation(
|
|
||||||
terminal,
|
|
||||||
"Enable 'Allow Azure Services' for this database?"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!enableAzureServices) {
|
|
||||||
terminal.writeln(terminalLog.warning("CloudShell may not be able to connect without enabling Azure services access"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database account to enable Azure services access
|
|
||||||
terminal.writeln(terminalLog.info("Updating database firewall configuration..."));
|
|
||||||
|
|
||||||
// Create update payload - only modify firewall-related properties
|
|
||||||
const updatePayload = {
|
|
||||||
...currentDbAccount,
|
|
||||||
properties: {
|
|
||||||
...currentDbAccount.properties,
|
|
||||||
publicNetworkAccess: "Enabled"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateDatabaseAccount(dbAccount.id, updatePayload);
|
|
||||||
terminal.writeln(terminalLog.success("Database firewall updated successfully"));
|
|
||||||
terminal.writeln(terminalLog.success("Azure services access is now enabled"));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
terminal.writeln(terminalLog.error(`Error configuring firewall: ${error.message}`));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Network access configuration handler for CloudShell
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { IsPublicAccessAvailable } from "../../Shared/CheckFirewallRules";
|
|
||||||
import { getUserSettings } from "../Data/CloudShellApiClient";
|
|
||||||
import { VnetSettings } from "../Models/DataModels";
|
|
||||||
import { terminalLog } from "../Utils/LogFormatter";
|
|
||||||
import { VNetHandler } from "./VNetHandler";
|
|
||||||
|
|
||||||
export class NetworkAccessHandler {
|
|
||||||
/**
|
|
||||||
* Configures network access for the CloudShell based on shell type and network restrictions
|
|
||||||
*/
|
|
||||||
public static async configureNetworkAccess(
|
|
||||||
terminal: Terminal,
|
|
||||||
region: string,
|
|
||||||
shellType: TerminalKind
|
|
||||||
): Promise<{
|
|
||||||
vNetSettings: any;
|
|
||||||
isAllPublicAccessEnabled: boolean;
|
|
||||||
}> {
|
|
||||||
// Check if public access is available for this shell type
|
|
||||||
const isAllPublicAccessEnabled = await IsPublicAccessAvailable(shellType);
|
|
||||||
|
|
||||||
// If public access is enabled, no need for VNet configuration
|
|
||||||
if (isAllPublicAccessEnabled) {
|
|
||||||
terminal.writeln(terminalLog.database("Public access enabled. Skipping VNet configuration."));
|
|
||||||
return {
|
|
||||||
vNetSettings: {},
|
|
||||||
isAllPublicAccessEnabled: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public access is restricted, we need to configure a VNet or use existing one
|
|
||||||
terminal.writeln(terminalLog.database("Network restrictions detected"));
|
|
||||||
terminal.writeln(terminalLog.info("Loading CloudShell configuration..."));
|
|
||||||
|
|
||||||
// Get existing settings if available
|
|
||||||
const settings = await getUserSettings();
|
|
||||||
if (!settings) {
|
|
||||||
terminal.writeln(terminalLog.warning("No existing user settings found."));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve CloudShell VNet settings if available
|
|
||||||
let cloudShellVnetSettings: VnetSettings | undefined;
|
|
||||||
if (settings) {
|
|
||||||
cloudShellVnetSettings = await VNetHandler.retrieveCloudShellVnetSettings(settings, terminal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If CloudShell has VNet settings, check with database config
|
|
||||||
let finalVNetSettings = {};
|
|
||||||
if (cloudShellVnetSettings && cloudShellVnetSettings.networkProfileResourceId) {
|
|
||||||
// Check if we should use existing VNet settings
|
|
||||||
const isContinueWithSameVnet = await VNetHandler.askForVNetConfigConsent(terminal, shellType);
|
|
||||||
|
|
||||||
if (isContinueWithSameVnet) {
|
|
||||||
// Check if the VNet is already configured in the database
|
|
||||||
const isVNetInDatabaseConfig = await VNetHandler.isCloudShellVNetInDatabaseConfig(cloudShellVnetSettings, terminal);
|
|
||||||
|
|
||||||
if (!isVNetInDatabaseConfig) {
|
|
||||||
terminal.writeln(terminalLog.warning("CloudShell VNet is not configured in database access list"));
|
|
||||||
const addToDatabase = await VNetHandler.askToAddVNetToDatabase(terminal, cloudShellVnetSettings);
|
|
||||||
|
|
||||||
if (addToDatabase) {
|
|
||||||
await VNetHandler.addCloudShellVNetToDatabase(cloudShellVnetSettings, terminal);
|
|
||||||
finalVNetSettings = cloudShellVnetSettings;
|
|
||||||
} else {
|
|
||||||
// User declined to add VNet to database, need to recreate
|
|
||||||
terminal.writeln(terminalLog.warning("Will configure new VNet..."));
|
|
||||||
cloudShellVnetSettings = undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
terminal.writeln(terminalLog.success("CloudShell VNet is already in database configuration"));
|
|
||||||
finalVNetSettings = cloudShellVnetSettings;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cloudShellVnetSettings = undefined; // User declined to use existing VNet settings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't have valid VNet settings, create new ones
|
|
||||||
if (!cloudShellVnetSettings || !cloudShellVnetSettings.networkProfileResourceId) {
|
|
||||||
terminal.writeln(terminalLog.subheader("Configuring network infrastructure"));
|
|
||||||
finalVNetSettings = await VNetHandler.configureCloudShellVNet(terminal, region);
|
|
||||||
|
|
||||||
// Add the new VNet to the database
|
|
||||||
await VNetHandler.addCloudShellVNetToDatabase(finalVNetSettings as VnetSettings, terminal);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
vNetSettings: finalVNetSettings,
|
|
||||||
isAllPublicAccessEnabled: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,894 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* VNet handling functionality for CloudShell
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { userContext } from "../../../../UserContext";
|
|
||||||
import { hasPrivateEndpointsRestrictions } from "../../Shared/CheckFirewallRules";
|
|
||||||
import {
|
|
||||||
createNetworkProfile,
|
|
||||||
createPrivateEndpoint,
|
|
||||||
createRelay,
|
|
||||||
createRoleOnNetworkProfile,
|
|
||||||
createRoleOnRelay,
|
|
||||||
getAccountDetails,
|
|
||||||
getDatabaseOperations,
|
|
||||||
getNetworkProfileInfo,
|
|
||||||
getRelay,
|
|
||||||
getSubnetInformation,
|
|
||||||
getVnet,
|
|
||||||
getVnetInformation,
|
|
||||||
updateDatabaseAccount,
|
|
||||||
updateSubnetInformation,
|
|
||||||
updateVnet
|
|
||||||
} from "../Data/CloudShellApiClient";
|
|
||||||
import { Settings, VnetSettings } from "../Models/DataModels";
|
|
||||||
import { askConfirmation, askQuestion, wait } from "../Utils/CommonUtils";
|
|
||||||
import { terminalLog } from "../Utils/LogFormatter";
|
|
||||||
|
|
||||||
// Constants for VNet configuration
|
|
||||||
const POLLING_INTERVAL_MS = 5000;
|
|
||||||
const MAX_RETRY_COUNT = 10;
|
|
||||||
const STANDARD_SKU = "Standard";
|
|
||||||
const DEFAULT_VNET_ADDRESS_PREFIX = "10.0.0.0/16";
|
|
||||||
const DEFAULT_SUBNET_ADDRESS_PREFIX = "10.0.1.0/24";
|
|
||||||
const DEFAULT_CONTAINER_INSTANCE_OID = "88536fb9-d60a-4aee-8195-041425d6e927";
|
|
||||||
|
|
||||||
export class VNetHandler {
|
|
||||||
/**
|
|
||||||
* Retrieves CloudShell VNet settings from user settings
|
|
||||||
*/
|
|
||||||
public static async retrieveCloudShellVnetSettings(settings: Settings, terminal: Terminal): Promise<VnetSettings> {
|
|
||||||
if (settings?.properties?.vnetSettings && Object.keys(settings.properties.vnetSettings).length > 0) {
|
|
||||||
try {
|
|
||||||
const netProfileInfo = await getNetworkProfileInfo<any>(settings.properties.vnetSettings.networkProfileResourceId);
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.header("Existing Network Configuration"));
|
|
||||||
|
|
||||||
const subnetId = netProfileInfo.properties.containerNetworkInterfaceConfigurations[0]
|
|
||||||
.properties.ipConfigurations[0].properties.subnet.id;
|
|
||||||
const vnetResourceId = subnetId.replace(/\/subnets\/[^/]+$/, '');
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.item("VNet", vnetResourceId));
|
|
||||||
terminal.writeln(terminalLog.item("Subnet", subnetId));
|
|
||||||
terminal.writeln(terminalLog.item("Location", settings.properties.vnetSettings.location));
|
|
||||||
terminal.writeln(terminalLog.item("Network Profile", settings.properties.vnetSettings.networkProfileResourceId));
|
|
||||||
terminal.writeln(terminalLog.item("Relay Namespace", settings.properties.vnetSettings.relayNamespaceResourceId));
|
|
||||||
|
|
||||||
return {
|
|
||||||
networkProfileResourceId: settings.properties.vnetSettings.networkProfileResourceId,
|
|
||||||
relayNamespaceResourceId: settings.properties.vnetSettings.relayNamespaceResourceId,
|
|
||||||
location: settings.properties.vnetSettings.location
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
terminal.writeln(terminalLog.warning("Error retrieving network profile. Will configure new network."));
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asks the user if they want to use existing network configuration (VNet or private endpoint)
|
|
||||||
*/
|
|
||||||
public static async askForVNetConfigConsent(terminal: Terminal, shellType: TerminalKind = null): Promise<boolean> {
|
|
||||||
// Check if this shell type supports only private endpoints
|
|
||||||
const isPrivateEndpointOnlyShell = shellType === TerminalKind.VCoreMongo;
|
|
||||||
// Check if the database has private endpoints configured
|
|
||||||
const hasPrivateEndpoints = hasPrivateEndpointsRestrictions();
|
|
||||||
|
|
||||||
// Determine which network type to mention based on shell type and database configuration
|
|
||||||
const networkType = isPrivateEndpointOnlyShell || hasPrivateEndpoints ? "private endpoint" : "network";
|
|
||||||
|
|
||||||
// Ask for consent
|
|
||||||
terminal.writeln("");
|
|
||||||
terminal.writeln(terminalLog.prompt(`Use this existing ${networkType} configuration?`));
|
|
||||||
terminal.writeln(terminalLog.info(`Answering 'N' will configure a new ${networkType} for CloudShell`));
|
|
||||||
|
|
||||||
return await askConfirmation(terminal, `Press Y/N to continue...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the CloudShell VNet is already in the database configuration
|
|
||||||
*/
|
|
||||||
public static async isCloudShellVNetInDatabaseConfig(vNetSettings: VnetSettings, terminal: Terminal): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
terminal.writeln(terminalLog.subheader("Verifying if CloudShell VNet is configured in database"));
|
|
||||||
|
|
||||||
// Get the subnet ID from the CloudShell Network Profile
|
|
||||||
const netProfileInfo = await getNetworkProfileInfo<any>(vNetSettings.networkProfileResourceId);
|
|
||||||
|
|
||||||
if (!netProfileInfo?.properties?.containerNetworkInterfaceConfigurations?.[0]
|
|
||||||
?.properties?.ipConfigurations?.[0]?.properties?.subnet?.id) {
|
|
||||||
terminal.writeln(terminalLog.warning("Could not retrieve subnet ID from CloudShell VNet"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudShellSubnetId = netProfileInfo.properties.containerNetworkInterfaceConfigurations[0]
|
|
||||||
.properties.ipConfigurations[0].properties.subnet.id;
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.item("CloudShell Subnet", cloudShellSubnetId.split('/').pop() || ""));
|
|
||||||
|
|
||||||
// Check if this subnet ID is in the database VNet rules
|
|
||||||
const dbAccount = userContext.databaseAccount;
|
|
||||||
if (!dbAccount?.properties?.virtualNetworkRules) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vnetRules = dbAccount.properties.virtualNetworkRules;
|
|
||||||
|
|
||||||
// Check if the CloudShell subnet is already in the rules
|
|
||||||
return vnetRules.some(rule => rule.id === cloudShellSubnetId);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
terminal.writeln(terminalLog.error("Error checking database VNet configuration"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asks the user if they want to add the CloudShell VNet to the database configuration
|
|
||||||
*/
|
|
||||||
public static async askToAddVNetToDatabase(terminal: Terminal, vNetSettings: VnetSettings): Promise<boolean> {
|
|
||||||
terminal.writeln("");
|
|
||||||
terminal.writeln(terminalLog.header("Network Configuration Mismatch"));
|
|
||||||
terminal.writeln(terminalLog.warning("Your CloudShell VNet is not in your database's allowed networks"));
|
|
||||||
terminal.writeln(terminalLog.warning("To connect from CloudShell, this VNet must be added to your database"));
|
|
||||||
|
|
||||||
return await askConfirmation(terminal, "Add CloudShell VNet to database configuration?");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the CloudShell VNet to the database configuration
|
|
||||||
* Now supports both VNet rules and private endpoints
|
|
||||||
*/
|
|
||||||
public static async addCloudShellVNetToDatabase(vNetSettings: VnetSettings, terminal: Terminal): Promise<void> {
|
|
||||||
try {
|
|
||||||
terminal.writeln(terminalLog.header("Updating database network configuration"));
|
|
||||||
|
|
||||||
// Step 1: Get the subnet ID from CloudShell Network Profile
|
|
||||||
const { cloudShellSubnetId, cloudShellVnetId } = await this.getCloudShellNetworkIds(vNetSettings, terminal);
|
|
||||||
|
|
||||||
// Step 2: Get current database account details
|
|
||||||
const { currentDbAccount } = await this.getDatabaseAccountDetails(terminal);
|
|
||||||
|
|
||||||
// Step 3: Determine if database uses private endpoints
|
|
||||||
const usesPrivateEndpoints = hasPrivateEndpointsRestrictions() ||
|
|
||||||
(currentDbAccount.properties.privateEndpointConnections?.length > 0);
|
|
||||||
|
|
||||||
// Log which networking mode we're using
|
|
||||||
if (usesPrivateEndpoints) {
|
|
||||||
terminal.writeln(terminalLog.info("Database is configured with private endpoints"));
|
|
||||||
} else {
|
|
||||||
terminal.writeln(terminalLog.info("Database is configured with VNet rules"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Check if connection is already configured
|
|
||||||
if (usesPrivateEndpoints) {
|
|
||||||
if (await this.isPrivateEndpointAlreadyConfigured(cloudShellVnetId, currentDbAccount, terminal)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (await this.isVNetAlreadyConfigured(cloudShellSubnetId, currentDbAccount, terminal)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Check network resource statuses and ongoing operations
|
|
||||||
const { vnetInfo, subnetInfo, operationInProgress } =
|
|
||||||
await this.checkNetworkResourceStatuses(cloudShellSubnetId, cloudShellVnetId, currentDbAccount.id, terminal);
|
|
||||||
|
|
||||||
// Step 6: If no operation in progress, update the configuration
|
|
||||||
if (!operationInProgress) {
|
|
||||||
if (usesPrivateEndpoints) {
|
|
||||||
// Create or update private endpoint configuration
|
|
||||||
await this.configurePrivateEndpoint(
|
|
||||||
cloudShellSubnetId,
|
|
||||||
vnetInfo.location,
|
|
||||||
currentDbAccount.id,
|
|
||||||
terminal
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Enable CosmosDB service endpoint on subnet if needed (for VNet rules)
|
|
||||||
await this.enableCosmosDBServiceEndpoint(cloudShellSubnetId, subnetInfo, terminal);
|
|
||||||
|
|
||||||
// Update database account with VNet rule
|
|
||||||
await this.updateDatabaseWithVNetRule(currentDbAccount, cloudShellSubnetId, currentDbAccount.id, terminal);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
terminal.writeln(terminalLog.info("Monitoring existing network operation..."));
|
|
||||||
// Step 7: Monitor the update progress
|
|
||||||
await this.monitorVNetAdditionProgress(cloudShellSubnetId, currentDbAccount.id, terminal);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
terminal.writeln(terminalLog.error(`Error updating database network configuration: ${err.message}`));
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a private endpoint is already configured for the CloudShell VNet
|
|
||||||
*/
|
|
||||||
private static async isPrivateEndpointAlreadyConfigured(
|
|
||||||
cloudShellVnetId: string,
|
|
||||||
currentDbAccount: any,
|
|
||||||
terminal: Terminal
|
|
||||||
): Promise<boolean> {
|
|
||||||
// Check if private endpoints exist and are properly configured for this VNet
|
|
||||||
const hasConfiguredEndpoint = currentDbAccount.properties.privateEndpointConnections?.some(
|
|
||||||
(connection: any) => {
|
|
||||||
const isApproved = connection.properties.privateLinkServiceConnectionState.status === 'Approved';
|
|
||||||
// We would need to check if the endpoint is in the CloudShell VNet
|
|
||||||
// For simplicity, we're assuming connection.properties.networkInterface contains this info
|
|
||||||
const endpointVNetId = connection.properties.networkInterface?.id?.split('/subnets/')[0];
|
|
||||||
return isApproved && endpointVNetId === cloudShellVnetId;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasConfiguredEndpoint) {
|
|
||||||
terminal.writeln(terminalLog.success("CloudShell private endpoint is already configured"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures a private endpoint for the CloudShell VNet to connect to the database
|
|
||||||
*/
|
|
||||||
private static async configurePrivateEndpoint(
|
|
||||||
cloudShellSubnetId: string,
|
|
||||||
vnetLocation: any,
|
|
||||||
dbAccountId: string,
|
|
||||||
terminal: Terminal
|
|
||||||
): Promise<void> {
|
|
||||||
// Extract necessary information from IDs
|
|
||||||
const subnetIdParts = cloudShellSubnetId.split('/');
|
|
||||||
const subnetIndex = subnetIdParts.indexOf('subnets');
|
|
||||||
|
|
||||||
const subnetName = subnetIdParts[subnetIndex + 1];
|
|
||||||
const resourceGroup = subnetIdParts[4];
|
|
||||||
const subscriptionId = subnetIdParts[2];
|
|
||||||
|
|
||||||
// Generate a unique name for the private endpoint
|
|
||||||
const privateEndpointName = `pe-cloudshell-cosmos-${Math.floor(10000 + Math.random() * 90000)}`;
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.subheader("Creating private endpoint for CloudShell"));
|
|
||||||
terminal.writeln(terminalLog.item("Private Endpoint Name", privateEndpointName));
|
|
||||||
terminal.writeln(terminalLog.item("Target Subnet", subnetName));
|
|
||||||
|
|
||||||
// Construct the private endpoint creation payload
|
|
||||||
const privateEndpointPayload = {
|
|
||||||
location: vnetLocation,
|
|
||||||
properties: {
|
|
||||||
privateLinkServiceConnections: [
|
|
||||||
{
|
|
||||||
name: privateEndpointName,
|
|
||||||
properties: {
|
|
||||||
privateLinkServiceId: dbAccountId,
|
|
||||||
groupIds: [
|
|
||||||
"MongoDB"
|
|
||||||
],
|
|
||||||
requestMessage: "CloudShell connectivity request"
|
|
||||||
},
|
|
||||||
type: "Microsoft.Network/privateEndpoints/privateLinkServiceConnections"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
subnet: {
|
|
||||||
id: cloudShellSubnetId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send the request to create the private endpoint
|
|
||||||
// Note: This is a placeholder - we would need to implement this API call
|
|
||||||
terminal.writeln(terminalLog.info("Submitting private endpoint creation request"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const privateEndpointUrl = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/privateEndpoints/${privateEndpointName}`;
|
|
||||||
|
|
||||||
await createPrivateEndpoint(privateEndpointUrl, privateEndpointPayload, "2024-05-01");
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.success("Private endpoint creation request submitted"));
|
|
||||||
terminal.writeln(terminalLog.warning("Please approve the private endpoint connection in the Azure portal"));
|
|
||||||
terminal.writeln(terminalLog.info("Note: Private endpoint operations may take several minutes to complete"));
|
|
||||||
} catch (err) {
|
|
||||||
terminal.writeln(terminalLog.error(`Failed to create private endpoint: ${err.message}`));
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Gets the subnet and VNet IDs from CloudShell Network Profile
|
|
||||||
*/
|
|
||||||
private static async getCloudShellNetworkIds(vNetSettings: VnetSettings, terminal: Terminal): Promise<{ cloudShellSubnetId: string; cloudShellVnetId: string }> {
|
|
||||||
const netProfileInfo = await getNetworkProfileInfo<any>(vNetSettings.networkProfileResourceId);
|
|
||||||
|
|
||||||
if (!netProfileInfo?.properties?.containerNetworkInterfaceConfigurations?.[0]
|
|
||||||
?.properties?.ipConfigurations?.[0]?.properties?.subnet?.id) {
|
|
||||||
throw new Error("Could not retrieve subnet ID from CloudShell VNet");
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudShellSubnetId = netProfileInfo.properties.containerNetworkInterfaceConfigurations[0]
|
|
||||||
.properties.ipConfigurations[0].properties.subnet.id;
|
|
||||||
|
|
||||||
// Extract VNet ID from subnet ID
|
|
||||||
const cloudShellVnetId = cloudShellSubnetId.substring(0, cloudShellSubnetId.indexOf('/subnets/'));
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.subheader("Identified CloudShell network resources"));
|
|
||||||
terminal.writeln(terminalLog.item("Subnet", cloudShellSubnetId.split('/').pop() || ""));
|
|
||||||
terminal.writeln(terminalLog.item("VNet", cloudShellVnetId.split('/').pop() || ""));
|
|
||||||
|
|
||||||
return { cloudShellSubnetId, cloudShellVnetId };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the database account details
|
|
||||||
*/
|
|
||||||
private static async getDatabaseAccountDetails(terminal: Terminal): Promise<{ currentDbAccount: any }> {
|
|
||||||
const dbAccount = userContext.databaseAccount;
|
|
||||||
terminal.writeln(terminalLog.database("Verifying current configuration"));
|
|
||||||
const currentDbAccount = await getAccountDetails(dbAccount.id);
|
|
||||||
|
|
||||||
return { currentDbAccount };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the VNet is already configured in the database
|
|
||||||
*/
|
|
||||||
private static async isVNetAlreadyConfigured(cloudShellSubnetId: string, currentDbAccount: any, terminal: Terminal): Promise<boolean> {
|
|
||||||
const vnetAlreadyConfigured = currentDbAccount.properties.virtualNetworkRules &&
|
|
||||||
currentDbAccount.properties.virtualNetworkRules.some(
|
|
||||||
(rule: any) => rule.id === cloudShellSubnetId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (vnetAlreadyConfigured) {
|
|
||||||
terminal.writeln(terminalLog.success("CloudShell VNet is already in database configuration"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the status of network resources and ongoing operations
|
|
||||||
*/
|
|
||||||
private static async checkNetworkResourceStatuses(
|
|
||||||
cloudShellSubnetId: string,
|
|
||||||
cloudShellVnetId: string,
|
|
||||||
dbAccountId: string,
|
|
||||||
terminal: Terminal
|
|
||||||
): Promise<{ vnetInfo: any; subnetInfo: any; operationInProgress: boolean }> {
|
|
||||||
terminal.writeln(terminalLog.subheader("Checking network resource status"));
|
|
||||||
|
|
||||||
let operationInProgress = false;
|
|
||||||
let vnetInfo: any = null;
|
|
||||||
let subnetInfo: any = null;
|
|
||||||
|
|
||||||
if (cloudShellVnetId && cloudShellSubnetId) {
|
|
||||||
// Get VNet and subnet resource status
|
|
||||||
vnetInfo = await getVnetInformation<any>(cloudShellVnetId);
|
|
||||||
subnetInfo = await getSubnetInformation<any>(cloudShellSubnetId);
|
|
||||||
|
|
||||||
// Check if there's an ongoing operation on the VNet or subnet
|
|
||||||
const vnetProvisioningState = vnetInfo?.properties?.provisioningState;
|
|
||||||
const subnetProvisioningState = subnetInfo?.properties?.provisioningState;
|
|
||||||
|
|
||||||
if (vnetProvisioningState !== 'Succeeded' && vnetProvisioningState !== 'Failed') {
|
|
||||||
terminal.writeln(terminalLog.warning(`VNet operation in progress: ${vnetProvisioningState}`));
|
|
||||||
operationInProgress = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subnetProvisioningState !== 'Succeeded' && subnetProvisioningState !== 'Failed') {
|
|
||||||
terminal.writeln(terminalLog.warning(`Subnet operation in progress: ${subnetProvisioningState}`));
|
|
||||||
operationInProgress = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check database operations
|
|
||||||
const latestDbAccount = await getAccountDetails<any>(dbAccountId);
|
|
||||||
|
|
||||||
if (latestDbAccount.properties.virtualNetworkRules) {
|
|
||||||
const isPendingAdd = latestDbAccount.properties.virtualNetworkRules.some(
|
|
||||||
(rule: any) => rule.id === cloudShellSubnetId && rule.status === 'Updating'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isPendingAdd) {
|
|
||||||
terminal.writeln(terminalLog.warning("CloudShell VNet addition to database is already in progress"));
|
|
||||||
operationInProgress = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { vnetInfo, subnetInfo, operationInProgress };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables the CosmosDB service endpoint on a subnet if needed
|
|
||||||
*/
|
|
||||||
private static async enableCosmosDBServiceEndpoint(cloudShellSubnetId: string, subnetInfo: any, terminal: Terminal): Promise<void> {
|
|
||||||
if (!subnetInfo) {
|
|
||||||
terminal.writeln(terminalLog.warning("Unable to check subnet endpoint configuration"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.subheader("Checking and configuring CosmosDB service endpoint"));
|
|
||||||
|
|
||||||
// Parse the subnet ID to get resource information
|
|
||||||
const subnetIdParts = cloudShellSubnetId.split('/');
|
|
||||||
const subnetIndex = subnetIdParts.indexOf('subnets');
|
|
||||||
if (subnetIndex > 0) {
|
|
||||||
const subnetName = subnetIdParts[subnetIndex + 1];
|
|
||||||
const vnetName = subnetIdParts[subnetIndex - 1];
|
|
||||||
const resourceGroup = subnetIdParts[4];
|
|
||||||
const subscriptionId = subnetIdParts[2];
|
|
||||||
|
|
||||||
// Get the subnet URL
|
|
||||||
const subnetUrl = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/virtualNetworks/${vnetName}/subnets/${subnetName}`;
|
|
||||||
|
|
||||||
// Check if CosmosDB service endpoint is already enabled
|
|
||||||
const hasCosmosDBEndpoint = subnetInfo.properties.serviceEndpoints &&
|
|
||||||
subnetInfo.properties.serviceEndpoints.some(
|
|
||||||
(endpoint: any) => endpoint.service === 'Microsoft.AzureCosmosDB'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasCosmosDBEndpoint) {
|
|
||||||
terminal.writeln(terminalLog.warning("Enabling CosmosDB service endpoint on subnet..."));
|
|
||||||
|
|
||||||
// Create update payload with CosmosDB service endpoint
|
|
||||||
const serviceEndpoints = [
|
|
||||||
...(subnetInfo.properties.serviceEndpoints || []),
|
|
||||||
{ service: 'Microsoft.AzureCosmosDB' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Update the subnet configuration while preserving existing properties
|
|
||||||
const subnetUpdatePayload = {
|
|
||||||
...subnetInfo,
|
|
||||||
properties: {
|
|
||||||
...subnetInfo.properties,
|
|
||||||
serviceEndpoints: serviceEndpoints
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply the subnet update
|
|
||||||
await updateSubnetInformation(subnetUrl, subnetUpdatePayload);
|
|
||||||
|
|
||||||
// Wait for the subnet update to complete
|
|
||||||
let subnetUpdateComplete = false;
|
|
||||||
let subnetRetryCount = 0;
|
|
||||||
|
|
||||||
while (!subnetUpdateComplete && subnetRetryCount < MAX_RETRY_COUNT) {
|
|
||||||
const updatedSubnet = await getSubnetInformation<any>(subnetUrl);
|
|
||||||
|
|
||||||
const endpointEnabled = updatedSubnet.properties.serviceEndpoints &&
|
|
||||||
updatedSubnet.properties.serviceEndpoints.some(
|
|
||||||
(endpoint: any) => endpoint.service === 'Microsoft.AzureCosmosDB'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (endpointEnabled && updatedSubnet.properties.provisioningState === 'Succeeded') {
|
|
||||||
subnetUpdateComplete = true;
|
|
||||||
terminal.writeln(terminalLog.success("CosmosDB service endpoint enabled successfully"));
|
|
||||||
} else {
|
|
||||||
subnetRetryCount++;
|
|
||||||
terminal.writeln(terminalLog.progress("Subnet update", `Waiting (${subnetRetryCount}/${MAX_RETRY_COUNT})`));
|
|
||||||
await wait(POLLING_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subnetUpdateComplete) {
|
|
||||||
throw new Error("Failed to enable CosmosDB service endpoint on subnet");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
terminal.writeln(terminalLog.success("CosmosDB service endpoint is already enabled"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the database account with a new VNet rule
|
|
||||||
*/
|
|
||||||
private static async updateDatabaseWithVNetRule(currentDbAccount: any, cloudShellSubnetId: string, dbAccountId: string, terminal: Terminal): Promise<void> {
|
|
||||||
// Create a deep copy of the current database account
|
|
||||||
const updatePayload = JSON.parse(JSON.stringify(currentDbAccount));
|
|
||||||
|
|
||||||
// Update only the network-related properties
|
|
||||||
updatePayload.properties.virtualNetworkRules = [
|
|
||||||
...(currentDbAccount.properties.virtualNetworkRules || []),
|
|
||||||
{ id: cloudShellSubnetId, ignoreMissingVNetServiceEndpoint: false }
|
|
||||||
];
|
|
||||||
updatePayload.properties.isVirtualNetworkFilterEnabled = true;
|
|
||||||
|
|
||||||
// Update the database account
|
|
||||||
terminal.writeln(terminalLog.subheader("Submitting VNet update request to database"));
|
|
||||||
await updateDatabaseAccount(dbAccountId, updatePayload);
|
|
||||||
terminal.writeln(terminalLog.success("Updated Database account with Cloud Shell Vnet"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Monitors the progress of adding a VNet to the database account
|
|
||||||
*/
|
|
||||||
private static async monitorVNetAdditionProgress(cloudShellSubnetId: string, dbAccountId: string, terminal: Terminal): Promise<void> {
|
|
||||||
let updateComplete = false;
|
|
||||||
let retryCount = 0;
|
|
||||||
let lastStatus = "";
|
|
||||||
let lastProgress = 0;
|
|
||||||
let lastOpId = "";
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.subheader("Monitoring database update progress"));
|
|
||||||
|
|
||||||
while (!updateComplete && retryCount < MAX_RETRY_COUNT) {
|
|
||||||
// Check if the VNet is now in the database account
|
|
||||||
const updatedDbAccount = await getAccountDetails<any>(dbAccountId);
|
|
||||||
|
|
||||||
const isVNetAdded = updatedDbAccount.properties.virtualNetworkRules?.some(
|
|
||||||
(rule: any) => rule.id === cloudShellSubnetId && (!rule.status || rule.status === 'Succeeded')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isVNetAdded) {
|
|
||||||
updateComplete = true;
|
|
||||||
terminal.writeln(terminalLog.success("CloudShell VNet successfully added to database configuration"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not yet added, check for operation progress
|
|
||||||
const operations = await getDatabaseOperations<any>(dbAccountId);
|
|
||||||
|
|
||||||
// Find network-related operations
|
|
||||||
const networkOps = operations.value?.filter(
|
|
||||||
(op: any) =>
|
|
||||||
(op.properties.description?.toLowerCase().includes('network') ||
|
|
||||||
op.properties.description?.toLowerCase().includes('vnet'))
|
|
||||||
) || [];
|
|
||||||
|
|
||||||
// Find active operations
|
|
||||||
const activeOp = networkOps.find((op: any) => op.properties.status === 'InProgress');
|
|
||||||
|
|
||||||
if (activeOp) {
|
|
||||||
// Show progress details if available
|
|
||||||
const currentStatus = activeOp.properties.status;
|
|
||||||
const progress = activeOp.properties.percentComplete || 0;
|
|
||||||
const opId = activeOp.name;
|
|
||||||
|
|
||||||
// Only update the terminal if something has changed
|
|
||||||
if (currentStatus !== lastStatus || progress !== lastProgress || opId !== lastOpId) {
|
|
||||||
// Create a progress bar
|
|
||||||
const progressBarLength = 20;
|
|
||||||
const filledLength = Math.floor(progress / 100 * progressBarLength);
|
|
||||||
const progressBar = "█".repeat(filledLength) + "░".repeat(progressBarLength - filledLength);
|
|
||||||
|
|
||||||
terminal.writeln(`\x1B[34m [${progressBar}] ${progress}% - ${currentStatus}\x1B[0m`);
|
|
||||||
lastStatus = currentStatus;
|
|
||||||
lastProgress = progress;
|
|
||||||
lastOpId = opId;
|
|
||||||
}
|
|
||||||
} else if (networkOps.length > 0) {
|
|
||||||
// If there are completed operations, show their status
|
|
||||||
const lastCompletedOp = networkOps[0];
|
|
||||||
|
|
||||||
if (lastCompletedOp.properties.status !== lastStatus) {
|
|
||||||
terminal.writeln(terminalLog.progress("Operation status", lastCompletedOp.properties.status));
|
|
||||||
lastStatus = lastCompletedOp.properties.status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retryCount++;
|
|
||||||
await wait(POLLING_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!updateComplete) {
|
|
||||||
terminal.writeln(terminalLog.warning("Database update timed out. Please check the Azure portal."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures a new VNet for CloudShell
|
|
||||||
*/
|
|
||||||
public static async configureCloudShellVNet(terminal: Terminal, resolvedRegion: string): Promise<VnetSettings> {
|
|
||||||
// Use professional and shorter names for resources
|
|
||||||
const randomSuffix = Math.floor(10000 + Math.random() * 90000);
|
|
||||||
|
|
||||||
const subnetName = `cloudshell-subnet-${randomSuffix}`;
|
|
||||||
const vnetName = `cloudshell-vnet-${randomSuffix}`;
|
|
||||||
const networkProfileName = `cloudshell-network-profile-${randomSuffix}`;
|
|
||||||
const relayName = `cloudshell-relay-${randomSuffix}`;
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.header("Network Resource Configuration"));
|
|
||||||
|
|
||||||
const azureContainerInstanceOID = await askQuestion(
|
|
||||||
terminal,
|
|
||||||
"Enter Azure Container Instance OID (Refer. https://learn.microsoft.com/en-us/azure/cloud-shell/vnet/deployment#get-the-azure-container-instance-id)",
|
|
||||||
DEFAULT_CONTAINER_INSTANCE_OID
|
|
||||||
);
|
|
||||||
|
|
||||||
const vNetSubscriptionId = await askQuestion(
|
|
||||||
terminal,
|
|
||||||
"Enter Virtual Network Subscription ID",
|
|
||||||
userContext.subscriptionId
|
|
||||||
);
|
|
||||||
|
|
||||||
const vNetResourceGroup = await askQuestion(
|
|
||||||
terminal,
|
|
||||||
"Enter Virtual Network Resource Group",
|
|
||||||
userContext.resourceGroup
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 1: Create VNet with Subnet
|
|
||||||
terminal.writeln(terminalLog.header("Deploying Network Resources"));
|
|
||||||
const vNetConfigPayload = await this.createCloudShellVnet(
|
|
||||||
resolvedRegion,
|
|
||||||
subnetName,
|
|
||||||
terminal,
|
|
||||||
vnetName,
|
|
||||||
vNetSubscriptionId,
|
|
||||||
vNetResourceGroup
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 2: Create Network Profile
|
|
||||||
await this.createNetworkProfileWithVnet(
|
|
||||||
vNetSubscriptionId,
|
|
||||||
vNetResourceGroup,
|
|
||||||
vnetName,
|
|
||||||
subnetName,
|
|
||||||
resolvedRegion,
|
|
||||||
terminal,
|
|
||||||
networkProfileName
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 3: Create Network Relay
|
|
||||||
await this.createNetworkRelay(
|
|
||||||
resolvedRegion,
|
|
||||||
terminal,
|
|
||||||
relayName,
|
|
||||||
vNetSubscriptionId,
|
|
||||||
vNetResourceGroup
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 4: Assign Roles
|
|
||||||
terminal.writeln(terminalLog.header("Configuring Security Permissions"));
|
|
||||||
await this.assignRoleToNetworkProfile(
|
|
||||||
azureContainerInstanceOID,
|
|
||||||
vNetSubscriptionId,
|
|
||||||
terminal,
|
|
||||||
networkProfileName,
|
|
||||||
vNetResourceGroup
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.assignRoleToRelay(
|
|
||||||
azureContainerInstanceOID,
|
|
||||||
vNetSubscriptionId,
|
|
||||||
terminal,
|
|
||||||
relayName,
|
|
||||||
vNetResourceGroup
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 5: Create and return VNet settings
|
|
||||||
const networkProfileResourceId = `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfileName.replace(/[\n\r]/g, "")}`;
|
|
||||||
const relayResourceId = `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Relay/namespaces/${relayName.replace(/[\n\r]/g, "")}`;
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.success("Network configuration complete"));
|
|
||||||
|
|
||||||
return {
|
|
||||||
networkProfileResourceId,
|
|
||||||
relayNamespaceResourceId: relayResourceId,
|
|
||||||
location: vNetConfigPayload.location
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a VNet for CloudShell
|
|
||||||
*/
|
|
||||||
private static async createCloudShellVnet(
|
|
||||||
resolvedRegion: string,
|
|
||||||
subnetName: string,
|
|
||||||
terminal: Terminal,
|
|
||||||
vnetName: string,
|
|
||||||
vNetSubscriptionId: string,
|
|
||||||
vNetResourceGroup: string
|
|
||||||
): Promise<any> {
|
|
||||||
const vNetConfigPayload = {
|
|
||||||
location: resolvedRegion,
|
|
||||||
properties: {
|
|
||||||
addressSpace: {
|
|
||||||
addressPrefixes: [DEFAULT_VNET_ADDRESS_PREFIX],
|
|
||||||
},
|
|
||||||
subnets: [
|
|
||||||
{
|
|
||||||
name: subnetName,
|
|
||||||
properties: {
|
|
||||||
addressPrefix: DEFAULT_SUBNET_ADDRESS_PREFIX,
|
|
||||||
delegations: [
|
|
||||||
{
|
|
||||||
name: "CloudShellDelegation",
|
|
||||||
properties: {
|
|
||||||
serviceName: "Microsoft.ContainerInstance/containerGroups"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.vnet(`Creating VNet: ${vnetName}`));
|
|
||||||
let vNetResponse = await updateVnet<any>(
|
|
||||||
`/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/virtualNetworks/${vnetName}`,
|
|
||||||
vNetConfigPayload
|
|
||||||
);
|
|
||||||
|
|
||||||
while (vNetResponse?.properties?.provisioningState !== "Succeeded") {
|
|
||||||
vNetResponse = await getVnet<any>(
|
|
||||||
`/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/virtualNetworks/${vnetName}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const vNetState = vNetResponse?.properties?.provisioningState;
|
|
||||||
if (vNetState !== "Succeeded" && vNetState !== "Failed") {
|
|
||||||
await wait(POLLING_INTERVAL_MS);
|
|
||||||
terminal.writeln(terminalLog.progress("VNet deployment", vNetState));
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.success("VNet created successfully"));
|
|
||||||
return vNetConfigPayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Network Profile for CloudShell
|
|
||||||
*/
|
|
||||||
private static async createNetworkProfileWithVnet(
|
|
||||||
vNetSubscriptionId: string,
|
|
||||||
vNetResourceGroup: string,
|
|
||||||
vnetName: string,
|
|
||||||
subnetName: string,
|
|
||||||
resolvedRegion: string,
|
|
||||||
terminal: Terminal,
|
|
||||||
networkProfileName: string
|
|
||||||
): Promise<void> {
|
|
||||||
const subnetId = `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/virtualNetworks/${vnetName}/subnets/${subnetName}`;
|
|
||||||
|
|
||||||
const createNetworkProfilePayload = {
|
|
||||||
location: resolvedRegion,
|
|
||||||
properties: {
|
|
||||||
containerNetworkInterfaceConfigurations: [
|
|
||||||
{
|
|
||||||
name: 'defaultContainerNicConfig',
|
|
||||||
properties: {
|
|
||||||
ipConfigurations: [
|
|
||||||
{
|
|
||||||
name: 'defaultContainerIpConfig',
|
|
||||||
properties: {
|
|
||||||
subnet: {
|
|
||||||
id: subnetId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.vnet("Creating Network Profile"));
|
|
||||||
let networkProfileResponse = await createNetworkProfile<any>(
|
|
||||||
`/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfileName}`,
|
|
||||||
createNetworkProfilePayload
|
|
||||||
);
|
|
||||||
|
|
||||||
while (networkProfileResponse?.properties?.provisioningState !== "Succeeded") {
|
|
||||||
networkProfileResponse = await getNetworkProfileInfo<any>(
|
|
||||||
`/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfileName}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const networkProfileState = networkProfileResponse?.properties?.provisioningState;
|
|
||||||
if (networkProfileState !== "Succeeded" && networkProfileState !== "Failed") {
|
|
||||||
await wait(POLLING_INTERVAL_MS);
|
|
||||||
terminal.writeln(terminalLog.progress("Network Profile", networkProfileState));
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.success("Network Profile created successfully"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Network Relay for CloudShell
|
|
||||||
*/
|
|
||||||
private static async createNetworkRelay(
|
|
||||||
resolvedRegion: string,
|
|
||||||
terminal: Terminal,
|
|
||||||
relayName: string,
|
|
||||||
vNetSubscriptionId: string,
|
|
||||||
vNetResourceGroup: string
|
|
||||||
): Promise<void> {
|
|
||||||
const relayPayload = {
|
|
||||||
location: resolvedRegion,
|
|
||||||
sku: {
|
|
||||||
name: STANDARD_SKU,
|
|
||||||
tier: STANDARD_SKU,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.vnet("Creating Relay Namespace"));
|
|
||||||
let relayResponse = await createRelay<any>(
|
|
||||||
`/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Relay/namespaces/${relayName}`,
|
|
||||||
relayPayload
|
|
||||||
);
|
|
||||||
|
|
||||||
while (relayResponse?.properties?.provisioningState !== "Succeeded") {
|
|
||||||
relayResponse = await getRelay<any>(
|
|
||||||
`/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Relay/namespaces/${relayName}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const relayState = relayResponse?.properties?.provisioningState;
|
|
||||||
if (relayState !== "Succeeded" && relayState !== "Failed") {
|
|
||||||
await wait(POLLING_INTERVAL_MS);
|
|
||||||
terminal.writeln(terminalLog.progress("Relay Namespace", relayState));
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.success("Relay Namespace created successfully"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigns a role to a Network Profile
|
|
||||||
*/
|
|
||||||
private static async assignRoleToNetworkProfile(
|
|
||||||
azureContainerInstanceOID: string,
|
|
||||||
vNetSubscriptionId: string,
|
|
||||||
terminal: Terminal,
|
|
||||||
networkProfileName: string,
|
|
||||||
vNetResourceGroup: string
|
|
||||||
): Promise<void> {
|
|
||||||
const nfRoleName = uuidv4();
|
|
||||||
const networkProfileRoleAssignmentPayload = {
|
|
||||||
properties: {
|
|
||||||
principalId: azureContainerInstanceOID,
|
|
||||||
roleDefinitionId: `/subscriptions/${vNetSubscriptionId}/providers/Microsoft.Authorization/roleDefinitions/4d97b98b-1d4f-4787-a291-c67834d212e7`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.info("Assigning permissions to Network Profile"));
|
|
||||||
await createRoleOnNetworkProfile<any>(
|
|
||||||
`/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfileName}/providers/Microsoft.Authorization/roleAssignments/${nfRoleName}`,
|
|
||||||
networkProfileRoleAssignmentPayload
|
|
||||||
);
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.success("Network Profile permissions assigned"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigns a role to a Network Relay
|
|
||||||
*/
|
|
||||||
private static async assignRoleToRelay(
|
|
||||||
azureContainerInstanceOID: string,
|
|
||||||
vNetSubscriptionId: string,
|
|
||||||
terminal: Terminal,
|
|
||||||
relayName: string,
|
|
||||||
vNetResourceGroup: string
|
|
||||||
): Promise<void> {
|
|
||||||
const relayRoleName = uuidv4();
|
|
||||||
const relayRoleAssignmentPayload = {
|
|
||||||
properties: {
|
|
||||||
principalId: azureContainerInstanceOID,
|
|
||||||
roleDefinitionId: `/subscriptions/${vNetSubscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c`,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.info("Assigning permissions to Relay Namespace"));
|
|
||||||
await createRoleOnRelay<any>(
|
|
||||||
`/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Relay/namespaces/${relayName}/providers/Microsoft.Authorization/roleAssignments/${relayRoleName}`,
|
|
||||||
relayRoleAssignmentPayload
|
|
||||||
);
|
|
||||||
|
|
||||||
terminal.writeln(terminalLog.success("Relay Namespace permissions assigned"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Cassandra shell type handler
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { userContext } from "../../../../UserContext";
|
|
||||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
|
||||||
import { setShellType } from "../Data/CloudShellApiClient";
|
|
||||||
import { NetworkAccessHandler } from "../Network/NetworkAccessHandler";
|
|
||||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
|
||||||
import { ShellTypeConfig } from "./ShellTypeFactory";
|
|
||||||
|
|
||||||
export class CassandraShellHandler implements ShellTypeConfig {
|
|
||||||
private shellType: TerminalKind = TerminalKind.Cassandra;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
setShellType(this.shellType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getShellName(): string {
|
|
||||||
return "Cassandra";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getInitialCommands(): Promise<string> {
|
|
||||||
const dbAccount = userContext.databaseAccount;
|
|
||||||
const endpoint = dbAccount.properties.cassandraEndpoint;
|
|
||||||
|
|
||||||
// Get database key
|
|
||||||
const dbName = dbAccount.name;
|
|
||||||
let key = "";
|
|
||||||
if (dbName) {
|
|
||||||
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
|
||||||
key = keys?.primaryMasterKey || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
host: getHostFromUrl(endpoint),
|
|
||||||
name: dbAccount.name,
|
|
||||||
password: key,
|
|
||||||
endpoint: endpoint
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getCommands(config).join("\n").concat("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async configureNetworkAccess(terminal: Terminal, region: string): Promise<{
|
|
||||||
vNetSettings: any;
|
|
||||||
isAllPublicAccessEnabled: boolean;
|
|
||||||
}> {
|
|
||||||
return await NetworkAccessHandler.configureNetworkAccess(terminal, region, this.shellType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCommands(config: any): string[] {
|
|
||||||
return [
|
|
||||||
// 1. Fetch and display location details in a readable format
|
|
||||||
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
|
|
||||||
// 2. Check if cqlsh is installed; if not, proceed with installation
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then echo '⚠️ cqlsh not found. Installing...'; fi",
|
|
||||||
// 3. Download Cassandra if not installed
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then curl -LO https://archive.apache.org/dist/cassandra/5.0.3/apache-cassandra-5.0.3-bin.tar.gz; fi",
|
|
||||||
// 4. Extract Cassandra package if not installed
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then tar -xvzf apache-cassandra-5.0.3-bin.tar.gz; fi",
|
|
||||||
// 5. Move Cassandra binaries if not installed
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then mkdir -p ~/cassandra && mv apache-cassandra-5.0.3/* ~/cassandra/; fi",
|
|
||||||
// 6. Add Cassandra to PATH if not installed
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then echo 'export PATH=$HOME/cassandra/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
// 7. Set environment variables for SSL
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then echo 'export SSL_VERSION=TLSv1_2' >> ~/.bashrc; fi",
|
|
||||||
"if ! command -v cqlsh &> /dev/null; then echo 'export SSL_VALIDATE=false' >> ~/.bashrc; fi",
|
|
||||||
// 8. Source .bashrc to update PATH (even if cqlsh was already installed)
|
|
||||||
"source ~/.bashrc",
|
|
||||||
// 9. Verify cqlsh installation
|
|
||||||
"cqlsh --version",
|
|
||||||
// 10. Login to Cassandra
|
|
||||||
`cqlsh ${config.host} 10350 -u ${config.name} -p ${config.password} --ssl --protocol-version=4`
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Mongo shell type handler
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { userContext } from "../../../../UserContext";
|
|
||||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
|
||||||
import { setShellType } from "../Data/CloudShellApiClient";
|
|
||||||
import { NetworkAccessHandler } from "../Network/NetworkAccessHandler";
|
|
||||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
|
||||||
import { ShellTypeConfig } from "./ShellTypeFactory";
|
|
||||||
|
|
||||||
export class MongoShellHandler implements ShellTypeConfig {
|
|
||||||
private shellType: TerminalKind = TerminalKind.Mongo;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
setShellType(this.shellType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getShellName(): string {
|
|
||||||
return "MongoDB";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getInitialCommands(): Promise<string> {
|
|
||||||
const dbAccount = userContext.databaseAccount;
|
|
||||||
const endpoint = dbAccount.properties.mongoEndpoint;
|
|
||||||
|
|
||||||
// Get database key
|
|
||||||
const dbName = dbAccount.name;
|
|
||||||
let key = "";
|
|
||||||
if (dbName) {
|
|
||||||
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
|
||||||
key = keys?.primaryMasterKey || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
host: getHostFromUrl(endpoint),
|
|
||||||
name: dbAccount.name,
|
|
||||||
password: key,
|
|
||||||
endpoint: endpoint
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getCommands(config).join("\n").concat("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async configureNetworkAccess(terminal: Terminal, region: string): Promise<{
|
|
||||||
vNetSettings: any;
|
|
||||||
isAllPublicAccessEnabled: boolean;
|
|
||||||
}> {
|
|
||||||
return await NetworkAccessHandler.configureNetworkAccess(terminal, region, this.shellType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCommands(config: any): string[] {
|
|
||||||
return [
|
|
||||||
// 1. Fetch and display location details in a readable format
|
|
||||||
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
|
|
||||||
// 2. Check if mongosh is installed; if not, proceed with installation
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
|
||||||
// 3. Download mongosh if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz; fi",
|
|
||||||
// 4. Extract mongosh package if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-2.3.8-linux-x64.tgz; fi",
|
|
||||||
// 5. Move mongosh binaries if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-2.3.8-linux-x64/* ~/mongosh/; fi",
|
|
||||||
// 6. Add mongosh to PATH if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
// 7. Source .bashrc to update PATH (even if mongosh was already installed)
|
|
||||||
"source ~/.bashrc",
|
|
||||||
// 8. Verify mongosh installation
|
|
||||||
"mongosh --version",
|
|
||||||
// 9. Login to MongoDB
|
|
||||||
`mongosh --host ${config.host} --port 10255 --username ${config.name} --password ${config.password} --tls --tlsAllowInvalidCertificates`
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* PostgreSQL shell type handler
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { userContext } from "../../../../UserContext";
|
|
||||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
|
||||||
import { setShellType } from "../Data/CloudShellApiClient";
|
|
||||||
import { NetworkAccessHandler } from "../Network/NetworkAccessHandler";
|
|
||||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
|
||||||
import { ShellTypeConfig } from "./ShellTypeFactory";
|
|
||||||
|
|
||||||
export class PostgresShellHandler implements ShellTypeConfig {
|
|
||||||
private shellType: TerminalKind = TerminalKind.Postgres;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
setShellType(this.shellType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getShellName(): string {
|
|
||||||
return "PostgreSQL";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getInitialCommands(): Promise<string> {
|
|
||||||
const dbAccount = userContext.databaseAccount;
|
|
||||||
const endpoint = dbAccount.properties.postgresqlEndpoint;
|
|
||||||
|
|
||||||
// Get database key
|
|
||||||
const dbName = dbAccount.name;
|
|
||||||
let key = "";
|
|
||||||
if (dbName) {
|
|
||||||
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
|
||||||
key = keys?.primaryMasterKey || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
host: getHostFromUrl(endpoint),
|
|
||||||
name: dbAccount.name,
|
|
||||||
password: key,
|
|
||||||
endpoint: endpoint
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getCommands(config).join("\n").concat("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async configureNetworkAccess(terminal: Terminal, region: string): Promise<{
|
|
||||||
vNetSettings: any;
|
|
||||||
isAllPublicAccessEnabled: boolean;
|
|
||||||
}> {
|
|
||||||
return await NetworkAccessHandler.configureNetworkAccess(terminal, region, this.shellType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCommands(config: any): string[] {
|
|
||||||
return [
|
|
||||||
// 1. Fetch and display location details in a readable format
|
|
||||||
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
|
|
||||||
// 2. Check if psql is installed; if not, proceed with installation
|
|
||||||
"if ! command -v psql &> /dev/null; then echo '⚠️ psql not found. Installing...'; fi",
|
|
||||||
// 3. Download PostgreSQL if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then curl -LO https://ftp.postgresql.org/pub/source/v15.2/postgresql-15.2.tar.bz2; fi",
|
|
||||||
// 4. Extract PostgreSQL package if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then tar -xvjf postgresql-15.2.tar.bz2; fi",
|
|
||||||
// 5. Create a directory for PostgreSQL installation if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then mkdir -p ~/pgsql; fi",
|
|
||||||
// 6. Download readline (dependency for PostgreSQL) if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then curl -LO https://ftp.gnu.org/gnu/readline/readline-8.1.tar.gz; fi",
|
|
||||||
// 7. Extract readline package if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then tar -xvzf readline-8.1.tar.gz; fi",
|
|
||||||
// 8. Configure readline if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then cd readline-8.1 && ./configure --prefix=$HOME/pgsql; fi",
|
|
||||||
// 9. Add PostgreSQL to PATH if not installed
|
|
||||||
"if ! command -v psql &> /dev/null; then echo 'export PATH=$HOME/pgsql/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
// 10. Source .bashrc to update PATH (even if psql was already installed)
|
|
||||||
"source ~/.bashrc",
|
|
||||||
// 11. Verify PostgreSQL installation
|
|
||||||
"psql --version",
|
|
||||||
`psql 'read -p "Enter Database Name: " dbname && read -p "Enter Username: " username && host=${config.endpoint} port=5432 dbname=$dbname user=$username sslmode=require'`
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Factory for creating shell type handlers
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { CassandraShellHandler } from "./CassandraShellHandler";
|
|
||||||
import { MongoShellHandler } from "./MongoShellHandler";
|
|
||||||
import { PostgresShellHandler } from "./PostgresShellHandler";
|
|
||||||
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
|
||||||
|
|
||||||
export interface ShellTypeConfig {
|
|
||||||
getShellName(): string;
|
|
||||||
getInitialCommands(): Promise<string>;
|
|
||||||
configureNetworkAccess(terminal: Terminal, region: string): Promise<{
|
|
||||||
vNetSettings: any;
|
|
||||||
isAllPublicAccessEnabled: boolean;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ShellTypeHandler {
|
|
||||||
/**
|
|
||||||
* Gets the appropriate handler for the given shell type
|
|
||||||
*/
|
|
||||||
public static getHandler(shellType: TerminalKind): ShellTypeConfig {
|
|
||||||
switch (shellType) {
|
|
||||||
case TerminalKind.Postgres:
|
|
||||||
return new PostgresShellHandler();
|
|
||||||
case TerminalKind.Mongo:
|
|
||||||
return new MongoShellHandler();
|
|
||||||
case TerminalKind.VCoreMongo:
|
|
||||||
return new VCoreMongoShellHandler();
|
|
||||||
case TerminalKind.Cassandra:
|
|
||||||
return new CassandraShellHandler();
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported shell type: ${shellType}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the display name for a shell type
|
|
||||||
*/
|
|
||||||
public static getShellNameForDisplay(terminalKind: TerminalKind): string {
|
|
||||||
switch (terminalKind) {
|
|
||||||
case TerminalKind.Postgres:
|
|
||||||
return "PostgreSQL";
|
|
||||||
case TerminalKind.Mongo:
|
|
||||||
case TerminalKind.VCoreMongo:
|
|
||||||
return "MongoDB";
|
|
||||||
case TerminalKind.Cassandra:
|
|
||||||
return "Cassandra";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* VCore MongoDB shell type handler
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
|
||||||
import { userContext } from "../../../../UserContext";
|
|
||||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
|
||||||
import { setShellType } from "../Data/CloudShellApiClient";
|
|
||||||
import { NetworkAccessHandler } from "../Network/NetworkAccessHandler";
|
|
||||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
|
||||||
import { ShellTypeConfig } from "./ShellTypeFactory";
|
|
||||||
|
|
||||||
export class VCoreMongoShellHandler implements ShellTypeConfig {
|
|
||||||
private shellType: TerminalKind = TerminalKind.VCoreMongo;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
setShellType(this.shellType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getShellName(): string {
|
|
||||||
return "MongoDB VCore";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getInitialCommands(): Promise<string> {
|
|
||||||
const dbAccount = userContext.databaseAccount;
|
|
||||||
const endpoint = dbAccount.properties.vcoreMongoEndpoint;
|
|
||||||
|
|
||||||
// Get database key
|
|
||||||
const dbName = dbAccount.name;
|
|
||||||
let key = "";
|
|
||||||
if (dbName) {
|
|
||||||
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
|
||||||
key = keys?.primaryMasterKey || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
host: getHostFromUrl(endpoint),
|
|
||||||
name: dbAccount.name,
|
|
||||||
password: key,
|
|
||||||
endpoint: endpoint
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getCommands(config).join("\n").concat("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async configureNetworkAccess(terminal: Terminal, region: string): Promise<{
|
|
||||||
vNetSettings: any;
|
|
||||||
isAllPublicAccessEnabled: boolean;
|
|
||||||
}> {
|
|
||||||
// VCore MongoDB uses private endpoints
|
|
||||||
return await NetworkAccessHandler.configureNetworkAccess(terminal, region, this.shellType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCommands(config: any): string[] {
|
|
||||||
return [
|
|
||||||
// 1. Fetch and display location details in a readable format
|
|
||||||
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
|
|
||||||
// 2. Check if mongosh is installed; if not, proceed with installation
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
|
||||||
// 3. Download mongosh if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz; fi",
|
|
||||||
// 4. Extract mongosh package if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-2.3.8-linux-x64.tgz; fi",
|
|
||||||
// 5. Move mongosh binaries if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-2.3.8-linux-x64/* ~/mongosh/; fi",
|
|
||||||
// 6. Add mongosh to PATH if not installed
|
|
||||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
|
||||||
// 7. Source .bashrc to update PATH (even if mongosh was already installed)
|
|
||||||
"source ~/.bashrc",
|
|
||||||
// 8. Verify mongosh installation
|
|
||||||
"mongosh --version",
|
|
||||||
// 9. Login to MongoDB
|
|
||||||
`read -p "Enter username: " username && mongosh "mongodb+srv://$username:@${config.endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000" --tls --tlsAllowInvalidCertificates`
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
|
|
||||||
import { IDisposable, ITerminalAddon, Terminal } from 'xterm';
|
|
||||||
|
|
||||||
interface IAttachOptions {
|
|
||||||
bidirectional?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AttachAddon implements ITerminalAddon {
|
|
||||||
private _socket: WebSocket;
|
|
||||||
private _bidirectional: boolean;
|
|
||||||
private _disposables: IDisposable[] = [];
|
|
||||||
private _socketData: string;
|
|
||||||
|
|
||||||
constructor(socket: WebSocket, options?: IAttachOptions) {
|
|
||||||
this._socket = socket;
|
|
||||||
// always set binary type to arraybuffer, we do not handle blobs
|
|
||||||
this._socket.binaryType = 'arraybuffer';
|
|
||||||
this._bidirectional = !(options && options.bidirectional === false);
|
|
||||||
this._socketData = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public activate(terminal: Terminal): void {
|
|
||||||
this._disposables.push(
|
|
||||||
addSocketListener(this._socket, 'message', ev => {
|
|
||||||
let data: ArrayBuffer | string = ev.data;
|
|
||||||
const startStatusJson = 'ie_us';
|
|
||||||
const endStatusJson = 'ie_ue';
|
|
||||||
|
|
||||||
if (typeof data === 'object') {
|
|
||||||
const enc = new TextDecoder("utf-8");
|
|
||||||
data = enc.decode(ev.data as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
// for example of json object look in TerminalHelper in the socket.onMessage
|
|
||||||
if (data.includes(startStatusJson) && data.includes(endStatusJson)) {
|
|
||||||
// process as one line
|
|
||||||
const statusData = data.split(startStatusJson)[1].split(endStatusJson)[0];
|
|
||||||
data = data.replace(statusData, '');
|
|
||||||
data = data.replace(startStatusJson, '');
|
|
||||||
data = data.replace(endStatusJson, '');
|
|
||||||
} else if (data.includes(startStatusJson)) {
|
|
||||||
// check for start
|
|
||||||
const partialStatusData = data.split(startStatusJson)[1];
|
|
||||||
this._socketData += partialStatusData;
|
|
||||||
data = data.replace(partialStatusData, '');
|
|
||||||
data = data.replace(startStatusJson, '');
|
|
||||||
} else if (data.includes(endStatusJson)) {
|
|
||||||
// check for end and process the command
|
|
||||||
const partialStatusData = data.split(endStatusJson)[0];
|
|
||||||
this._socketData += partialStatusData;
|
|
||||||
data = data.replace(partialStatusData, '');
|
|
||||||
data = data.replace(endStatusJson, '');
|
|
||||||
this._socketData = '';
|
|
||||||
} else if (this._socketData.length > 0) {
|
|
||||||
// check if the line is all data then just concatenate
|
|
||||||
this._socketData += data;
|
|
||||||
data = '';
|
|
||||||
}
|
|
||||||
terminal.write(data);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this._bidirectional) {
|
|
||||||
this._disposables.push(terminal.onData(data => this._sendData(data)));
|
|
||||||
this._disposables.push(terminal.onBinary(data => this._sendBinary(data)));
|
|
||||||
}
|
|
||||||
|
|
||||||
this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose()));
|
|
||||||
this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void {
|
|
||||||
for (const d of this._disposables) {
|
|
||||||
d.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _sendData(data: string): void {
|
|
||||||
if (!this._checkOpenSocket()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._socket.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _sendBinary(data: string): void {
|
|
||||||
if (!this._checkOpenSocket()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const buffer = new Uint8Array(data.length);
|
|
||||||
for (let i = 0; i < data.length; ++i) {
|
|
||||||
buffer[i] = data.charCodeAt(i) & 255;
|
|
||||||
}
|
|
||||||
this._socket.send(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _checkOpenSocket(): boolean {
|
|
||||||
switch (this._socket.readyState) {
|
|
||||||
case WebSocket.OPEN:
|
|
||||||
return true;
|
|
||||||
case WebSocket.CONNECTING:
|
|
||||||
throw new Error('Attach addon was loaded before socket was open');
|
|
||||||
case WebSocket.CLOSING:
|
|
||||||
return false;
|
|
||||||
case WebSocket.CLOSED:
|
|
||||||
throw new Error('Attach addon socket is closed');
|
|
||||||
default:
|
|
||||||
throw new Error('Unexpected socket state');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
|
|
||||||
socket.addEventListener(type, handler);
|
|
||||||
return {
|
|
||||||
dispose: () => {
|
|
||||||
if (!handler) {
|
|
||||||
// Already disposed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socket.removeEventListener(type, handler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Common utility functions for CloudShell
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { terminalLog } from "./LogFormatter";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to wait for a specified duration
|
|
||||||
*/
|
|
||||||
export const wait = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to ask a question in the terminal
|
|
||||||
*/
|
|
||||||
export const askQuestion = (terminal: Terminal, question: string, defaultAnswer: string = ""): Promise<string> => {
|
|
||||||
return new Promise<string>((resolve) => {
|
|
||||||
const prompt = terminalLog.prompt(`${question} (${defaultAnswer}): `);
|
|
||||||
terminal.writeln(prompt);
|
|
||||||
terminal.focus();
|
|
||||||
let response = "";
|
|
||||||
|
|
||||||
const dataListener = terminal.onData((data: string) => {
|
|
||||||
if (data === "\r") { // Enter key pressed
|
|
||||||
terminal.writeln(""); // Move to a new line
|
|
||||||
dataListener.dispose();
|
|
||||||
if (response.trim() === "") {
|
|
||||||
response = defaultAnswer; // Use default answer if no input
|
|
||||||
}
|
|
||||||
return resolve(response.trim());
|
|
||||||
} else if (data === "\u007F" || data === "\b") { // Handle backspace
|
|
||||||
if (response.length > 0) {
|
|
||||||
response = response.slice(0, -1);
|
|
||||||
terminal.write("\x1B[D \x1B[D"); // Move cursor back, clear character
|
|
||||||
}
|
|
||||||
} else if (data.charCodeAt(0) >= 32) { // Ignore control characters
|
|
||||||
response += data;
|
|
||||||
terminal.write(data); // Display typed characters
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prevent cursor movement beyond the prompt
|
|
||||||
terminal.onKey(({ domEvent }: { domEvent: KeyboardEvent }) => {
|
|
||||||
if (domEvent.key === "ArrowLeft" && response.length === 0) {
|
|
||||||
domEvent.preventDefault(); // Stop moving left beyond the question
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to ask for yes/no confirmation
|
|
||||||
*/
|
|
||||||
export const askConfirmation = async (terminal: Terminal, question: string): Promise<boolean> => {
|
|
||||||
terminal.writeln("");
|
|
||||||
terminal.writeln(terminalLog.prompt(`${question} (Y/N)`));
|
|
||||||
terminal.focus();
|
|
||||||
return new Promise<boolean>((resolve) => {
|
|
||||||
const keyListener = terminal.onKey(({ key }: { key: string }) => {
|
|
||||||
keyListener.dispose();
|
|
||||||
terminal.writeln("");
|
|
||||||
|
|
||||||
if (key.toLowerCase() === 'y') {
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract host from a URL
|
|
||||||
*/
|
|
||||||
export const getHostFromUrl = (url: string): string => {
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(url);
|
|
||||||
return urlObj.hostname;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Invalid URL:", error);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* Standardized terminal logging functions for consistent formatting
|
|
||||||
*/
|
|
||||||
export const terminalLog = {
|
|
||||||
// Section headers
|
|
||||||
header: (message: string) => `\n\x1B[1;34m┌─ ${message} ${"─".repeat(Math.max(45 - message.length, 0))}\x1B[0m`,
|
|
||||||
subheader: (message: string) => `\x1B[1;36m├ ${message}\x1B[0m`,
|
|
||||||
sectionEnd: () => `\x1B[1;34m└${"─".repeat(50)}\x1B[0m\n`,
|
|
||||||
|
|
||||||
// Status messages
|
|
||||||
success: (message: string) => `\x1B[32m✓ ${message}\x1B[0m`,
|
|
||||||
warning: (message: string) => `\x1B[33m⚠ ${message}\x1B[0m`,
|
|
||||||
error: (message: string) => `\x1B[31m✗ ${message}\x1B[0m`,
|
|
||||||
info: (message: string) => `\x1B[34m${message}\x1B[0m`,
|
|
||||||
|
|
||||||
// Resource information
|
|
||||||
database: (message: string) => `\x1B[35m🔶 Database: ${message}\x1B[0m`,
|
|
||||||
vnet: (message: string) => `\x1B[36m🔷 Network: ${message}\x1B[0m`,
|
|
||||||
cloudshell: (message: string) => `\x1B[32m🔷 CloudShell: ${message}\x1B[0m`,
|
|
||||||
|
|
||||||
// Data formatting
|
|
||||||
item: (label: string, value: string) => ` • ${label}: \x1B[32m${value}\x1B[0m`,
|
|
||||||
progress: (operation: string, status: string) => `\x1B[34m${operation}: \x1B[36m${status}\x1B[0m`,
|
|
||||||
|
|
||||||
// User interaction
|
|
||||||
prompt: (message: string) => `\x1B[1;37m${message}\x1B[0m`,
|
|
||||||
separator: () => `\x1B[30;1m${"─".repeat(50)}\x1B[0m`
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import * as ViewModels from "Contracts/ViewModels";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { armRequest } from "Utils/arm/request";
|
import { armRequest } from "Utils/arm/request";
|
||||||
|
|
||||||
@@ -11,8 +10,16 @@ export async function checkFirewallRules(
|
|||||||
setMessageFunc?: (message: string) => void,
|
setMessageFunc?: (message: string) => void,
|
||||||
message?: string,
|
message?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
|
||||||
const isEnabled = await callFirewallAPis(apiVersion, firewallRulesPredicate);
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const response: any = await armRequest({
|
||||||
|
host: configContext.ARM_ENDPOINT,
|
||||||
|
path: firewallRulesUri,
|
||||||
|
method: "GET",
|
||||||
|
apiVersion: apiVersion,
|
||||||
|
});
|
||||||
|
const firewallRules: DataModels.FirewallRule[] = response?.data?.value || response?.value || [];
|
||||||
|
const isEnabled = firewallRules.some(firewallRulesPredicate);
|
||||||
|
|
||||||
if (isAllPublicIPAddressesEnabled) {
|
if (isAllPublicIPAddressesEnabled) {
|
||||||
isAllPublicIPAddressesEnabled(isEnabled);
|
isAllPublicIPAddressesEnabled(isEnabled);
|
||||||
@@ -35,89 +42,3 @@ export async function checkFirewallRules(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function callFirewallAPis(
|
|
||||||
apiVersion: string,
|
|
||||||
firewallRulesPredicate: (rule: DataModels.FirewallRule) => unknown):
|
|
||||||
Promise<boolean> {
|
|
||||||
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const response: any = await armRequest({
|
|
||||||
host: configContext.ARM_ENDPOINT,
|
|
||||||
path: firewallRulesUri,
|
|
||||||
method: "GET",
|
|
||||||
apiVersion: apiVersion,
|
|
||||||
});
|
|
||||||
const firewallRules: DataModels.FirewallRule[] = response?.data?.value || response?.value || [];
|
|
||||||
const isEnabled = firewallRules.some(firewallRulesPredicate);
|
|
||||||
|
|
||||||
return isEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkNetworkRules(kind: ViewModels.TerminalKind, isPublicAccessEnabledFlag: ko.Observable<boolean> | React.Dispatch<React.SetStateAction<boolean>>): Promise<void> {
|
|
||||||
if (kind === ViewModels.TerminalKind.Postgres) {
|
|
||||||
await checkFirewallRules(
|
|
||||||
"2022-11-08",
|
|
||||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
|
|
||||||
isPublicAccessEnabledFlag,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kind === ViewModels.TerminalKind.VCoreMongo) {
|
|
||||||
await checkFirewallRules(
|
|
||||||
"2023-03-01-preview",
|
|
||||||
(rule) =>
|
|
||||||
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
|
||||||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
|
||||||
isPublicAccessEnabledFlag,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function IsPublicAccessAvailable(kind: ViewModels.TerminalKind): Promise<boolean> {
|
|
||||||
if (kind === ViewModels.TerminalKind.Postgres) {
|
|
||||||
return await callFirewallAPis(
|
|
||||||
"2022-11-08",
|
|
||||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kind === ViewModels.TerminalKind.VCoreMongo) {
|
|
||||||
return await callFirewallAPis(
|
|
||||||
"2023-03-01-preview",
|
|
||||||
(rule) =>
|
|
||||||
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
|
||||||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !hasDatabaseNetworkRestrictions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the database account has network restrictions
|
|
||||||
*/
|
|
||||||
const hasDatabaseNetworkRestrictions = (): boolean => {
|
|
||||||
return hasVNetRestrictions() || hasFirewallRestrictions() || hasPrivateEndpointsRestrictions();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the database account has Private Endpoint restrictions
|
|
||||||
*/
|
|
||||||
export const hasPrivateEndpointsRestrictions = (): boolean => {
|
|
||||||
return userContext.databaseAccount.properties.privateEndpointConnections && userContext.databaseAccount.properties.privateEndpointConnections.length > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the database account has Firewall restrictions
|
|
||||||
*/
|
|
||||||
export const hasFirewallRestrictions = (): boolean => {
|
|
||||||
return userContext.databaseAccount.properties.isVirtualNetworkFilterEnabled;;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the database account has VNet restrictions
|
|
||||||
*/
|
|
||||||
export const hasVNetRestrictions = (): boolean => {
|
|
||||||
return userContext.databaseAccount.properties.virtualNetworkRules && userContext.databaseAccount.properties.virtualNetworkRules.length > 0
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
import { Spinner, SpinnerSize } from "@fluentui/react";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||||
import { checkNetworkRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||||
@@ -13,10 +13,8 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
|
|||||||
import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent";
|
import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
import { CloudShellTerminalComponent } from "./CloudShellTab/CloudShellTabComponent";
|
|
||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
|
|
||||||
export interface TerminalTabOptions extends ViewModels.TabOptions {
|
export interface TerminalTabOptions extends ViewModels.TabOptions {
|
||||||
account: DataModels.DatabaseAccount;
|
account: DataModels.DatabaseAccount;
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
@@ -45,98 +43,81 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||||
screenshot={FirewallRuleScreenshot}
|
screenshot={FirewallRuleScreenshot}
|
||||||
shellName={getShellNameForDisplay(this.kind)}
|
shellName={this.getShellNameForDisplay(this.kind)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.parameters() ? (
|
return this.parameters() ? (
|
||||||
<NotebookTerminalComponent
|
<NotebookTerminalComponent
|
||||||
notebookServerInfo={this.getNotebookServerInfo()}
|
notebookServerInfo={this.getNotebookServerInfo()}
|
||||||
databaseAccount={this.getDatabaseAccount()}
|
databaseAccount={this.getDatabaseAccount()}
|
||||||
tabId={this.getTabId()}
|
tabId={this.getTabId()}
|
||||||
username={this.getUsername()}
|
username={this.getUsername()}
|
||||||
/>): (
|
/>
|
||||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CloudShell terminal tab
|
|
||||||
*/
|
|
||||||
class CloudShellTerminalComponentAdapter implements ReactAdapter {
|
|
||||||
|
|
||||||
// parameters: true: show, false: hide
|
|
||||||
public parameters: ko.Computed<boolean>;
|
|
||||||
constructor(
|
|
||||||
private kind: ViewModels.TerminalKind,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
|
||||||
|
|
||||||
console.log("this.parameters() " + this.parameters() );
|
|
||||||
return this.parameters() ? (
|
|
||||||
<CloudShellTerminalComponent
|
|
||||||
shellType={this.kind}/>
|
|
||||||
) : (
|
) : (
|
||||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const getShellNameForDisplay = (terminalKind: ViewModels.TerminalKind): string => {
|
private getShellNameForDisplay(terminalKind: ViewModels.TerminalKind): string {
|
||||||
switch (terminalKind) {
|
switch (terminalKind) {
|
||||||
case ViewModels.TerminalKind.Postgres:
|
case ViewModels.TerminalKind.Postgres:
|
||||||
return "PostgreSQL";
|
return "PostgreSQL";
|
||||||
case ViewModels.TerminalKind.Mongo:
|
case ViewModels.TerminalKind.Mongo:
|
||||||
case ViewModels.TerminalKind.VCoreMongo:
|
case ViewModels.TerminalKind.VCoreMongo:
|
||||||
return "MongoDB";
|
return "MongoDB";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TerminalTab extends TabsBase {
|
export default class TerminalTab extends TabsBase {
|
||||||
public readonly html = '<div style="height: 100%" data-bind="react: terminalComponentAdapter"></div>';
|
public readonly html = '<div style="height: 100%" data-bind="react:notebookTerminalComponentAdapter"></div> ';
|
||||||
private container: Explorer;
|
private container: Explorer;
|
||||||
private terminalComponentAdapter: any;
|
private notebookTerminalComponentAdapter: NotebookTerminalComponentAdapter;
|
||||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
|
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
|
||||||
|
|
||||||
constructor (options: TerminalTabOptions) {
|
constructor(options: TerminalTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
||||||
|
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||||
checkNetworkRules(options.kind, this.isAllPublicIPAddressesEnabled);
|
() => this.getNotebookServerInfo(options),
|
||||||
|
() => userContext?.databaseAccount,
|
||||||
this.initializeNotebookTerminalAdapter(options);
|
() => this.tabId,
|
||||||
}
|
() => this.getUsername(),
|
||||||
|
this.isAllPublicIPAddressesEnabled,
|
||||||
private async initializeNotebookTerminalAdapter(options: TerminalTabOptions): Promise<void> {
|
options.kind,
|
||||||
if (userContext.features.enableCloudShell) {
|
);
|
||||||
this.terminalComponentAdapter = new CloudShellTerminalComponentAdapter(
|
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
options.kind
|
if (
|
||||||
);
|
|
||||||
|
|
||||||
this.terminalComponentAdapter.parameters = ko.computed<boolean>(() =>
|
|
||||||
this.isTemplateReady()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.terminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
|
||||||
() => this.getNotebookServerInfo(options),
|
|
||||||
() => userContext?.databaseAccount,
|
|
||||||
() => this.tabId,
|
|
||||||
() => this.getUsername(),
|
|
||||||
this.isAllPublicIPAddressesEnabled,
|
|
||||||
options.kind
|
|
||||||
);
|
|
||||||
|
|
||||||
this.terminalComponentAdapter.parameters = ko.computed<boolean>(() =>
|
|
||||||
this.isTemplateReady() &&
|
this.isTemplateReady() &&
|
||||||
useNotebook.getState().isNotebookEnabled &&
|
useNotebook.getState().isNotebookEnabled &&
|
||||||
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint &&
|
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint &&
|
||||||
this.isAllPublicIPAddressesEnabled()
|
this.isAllPublicIPAddressesEnabled()
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
||||||
|
checkFirewallRules(
|
||||||
|
"2022-11-08",
|
||||||
|
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
|
||||||
|
this.isAllPublicIPAddressesEnabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
|
||||||
|
checkFirewallRules(
|
||||||
|
"2023-03-01-preview",
|
||||||
|
(rule) =>
|
||||||
|
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
||||||
|
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
||||||
|
this.isAllPublicIPAddressesEnabled,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export type Features = {
|
|||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
readonly enableCloudShell: boolean;
|
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -111,7 +110,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
enableCloudShell: "true" === get("enablecloudshell"),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
case SelfServeType.example: {
|
case SelfServeType.example: {
|
||||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||||
const selfServeExample = new SelfServeExample.default();
|
const selfServeExample = new SelfServeExample.default();
|
||||||
await loadTranslations(selfServeExample.constructor.name);
|
await loadTranslations(selfServeType);
|
||||||
return selfServeExample.toSelfServeDescriptor();
|
return selfServeExample.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
case SelfServeType.sqlx: {
|
case SelfServeType.sqlx: {
|
||||||
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
||||||
const sqlX = new SqlX.default();
|
const sqlX = new SqlX.default();
|
||||||
await loadTranslations(sqlX.constructor.name);
|
await loadTranslations(selfServeType);
|
||||||
return sqlX.toSelfServeDescriptor();
|
return sqlX.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
case SelfServeType.graphapicompute: {
|
case SelfServeType.graphapicompute: {
|
||||||
@@ -55,7 +55,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
||||||
);
|
);
|
||||||
const graphAPICompute = new GraphAPICompute.default();
|
const graphAPICompute = new GraphAPICompute.default();
|
||||||
await loadTranslations(graphAPICompute.constructor.name);
|
await loadTranslations(selfServeType);
|
||||||
return graphAPICompute.toSelfServeDescriptor();
|
return graphAPICompute.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
case SelfServeType.materializedviewsbuilder: {
|
case SelfServeType.materializedviewsbuilder: {
|
||||||
@@ -63,7 +63,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
||||||
);
|
);
|
||||||
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
||||||
await loadTranslations(materializedViewsBuilder.constructor.name);
|
await loadTranslations(selfServeType);
|
||||||
return materializedViewsBuilder.toSelfServeDescriptor();
|
return materializedViewsBuilder.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -103,7 +103,7 @@ const handleMessage = async (event: MessageEvent): Promise<void> => {
|
|||||||
|
|
||||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||||
const selfServeTypeText = urlSearchParams.get("selfServeType") || inputs.selfServeType;
|
const selfServeTypeText = urlSearchParams.get("selfServeType") || inputs.selfServeType;
|
||||||
const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType];
|
const selfServeType = SelfServeType[selfServeTypeText.toLocaleLowerCase() as keyof typeof SelfServeType];
|
||||||
if (
|
if (
|
||||||
!inputs.subscriptionId ||
|
!inputs.subscriptionId ||
|
||||||
!inputs.resourceGroup ||
|
!inputs.resourceGroup ||
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ export enum SelfServeType {
|
|||||||
// Unsupported self serve type passed as feature flag
|
// Unsupported self serve type passed as feature flag
|
||||||
invalid = "invalid",
|
invalid = "invalid",
|
||||||
// Add your self serve types here
|
// Add your self serve types here
|
||||||
example = "example",
|
// NOTE: text and casing of the enum's value must match the corresponding file in Localization\en\
|
||||||
sqlx = "sqlx",
|
example = "SelfServeExample",
|
||||||
graphapicompute = "graphapicompute",
|
sqlx = "SqlX",
|
||||||
materializedviewsbuilder = "materializedviewsbuilder",
|
graphapicompute = "GraphAPICompute",
|
||||||
|
materializedviewsbuilder = "MaterializedViewsBuilder",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -89,3 +89,7 @@ export const getItemName = (): string => {
|
|||||||
return "Items";
|
return "Items";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
||||||
|
return apiType === "SQL" || apiType === "Tables";
|
||||||
|
};
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ export function decryptJWTToken(token: string) {
|
|||||||
return JSON.parse(tokenPayload);
|
return JSON.parse(tokenPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMsalInstance(clientId: string = "203f1145-856a-4232-83d4-a43568fba23d"){
|
export async function getMsalInstance() {
|
||||||
const msalConfig: msal.Configuration = {
|
const msalConfig: msal.Configuration = {
|
||||||
cache: {
|
cache: {
|
||||||
cacheLocation: "localStorage",
|
cacheLocation: "localStorage",
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
authority: `${configContext.AAD_ENDPOINT}organizations`,
|
authority: `${configContext.AAD_ENDPOINT}organizations`,
|
||||||
clientId: clientId,
|
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,8 +68,7 @@ export async function getMsalInstance(clientId: string = "203f1145-856a-4232-83d
|
|||||||
export async function acquireMsalTokenForAccount(
|
export async function acquireMsalTokenForAccount(
|
||||||
account: DatabaseAccount,
|
account: DatabaseAccount,
|
||||||
silent: boolean = false,
|
silent: boolean = false,
|
||||||
clientId: string = "203f1145-856a-4232-83d4-a43568fba23d",
|
user_hint?: string,
|
||||||
user_hint?: string
|
|
||||||
) {
|
) {
|
||||||
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
|
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
|
||||||
throw new Error("Database account has no document endpoint defined");
|
throw new Error("Database account has no document endpoint defined");
|
||||||
@@ -78,7 +77,7 @@ export async function acquireMsalTokenForAccount(
|
|||||||
/\/+$/,
|
/\/+$/,
|
||||||
"/.default",
|
"/.default",
|
||||||
);
|
);
|
||||||
const msalInstance = await getMsalInstance(clientId);
|
const msalInstance = await getMsalInstance();
|
||||||
const knownAccounts = msalInstance.getAllAccounts();
|
const knownAccounts = msalInstance.getAllAccounts();
|
||||||
// If user_hint is provided, we will try to use it to find the account.
|
// If user_hint is provided, we will try to use it to find the account.
|
||||||
// If no account is found, we will use the current active account or first account in the list.
|
// If no account is found, we will use the current active account or first account in the list.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const autoPilotThroughput1K = 1000;
|
export const autoPilotThroughput1K = 1000;
|
||||||
export const autoPilotIncrementStep = 1000;
|
export const autoPilotIncrementStep = 1000;
|
||||||
export const autoPilotThroughput4K = 4000;
|
export const autoPilotThroughput4K = 4000;
|
||||||
|
export const autoPilotThroughput10K = 10000;
|
||||||
|
|
||||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||||
if (!maxThroughput) {
|
if (!maxThroughput) {
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ interface Options {
|
|||||||
body?: unknown;
|
body?: unknown;
|
||||||
queryParams?: ARMQueryParams;
|
queryParams?: ARMQueryParams;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
customHeaders?: Record<string, string>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function armRequestWithoutPolling<T>({
|
export async function armRequestWithoutPolling<T>({
|
||||||
@@ -58,7 +57,6 @@ export async function armRequestWithoutPolling<T>({
|
|||||||
body: requestBody,
|
body: requestBody,
|
||||||
queryParams,
|
queryParams,
|
||||||
contentType,
|
contentType,
|
||||||
customHeaders
|
|
||||||
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
|
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
|
||||||
const url = new URL(path, host);
|
const url = new URL(path, host);
|
||||||
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
||||||
@@ -67,7 +65,7 @@ export async function armRequestWithoutPolling<T>({
|
|||||||
queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames);
|
queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userContext.authorizationToken && !customHeaders["Authorization"]) {
|
if (!userContext.authorizationToken) {
|
||||||
throw new Error("No authority token provided");
|
throw new Error("No authority token provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +74,6 @@ export async function armRequestWithoutPolling<T>({
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: userContext.authorizationToken,
|
Authorization: userContext.authorizationToken,
|
||||||
[HttpHeaders.contentType]: contentType || "application/json",
|
[HttpHeaders.contentType]: contentType || "application/json",
|
||||||
...customHeaders
|
|
||||||
},
|
},
|
||||||
body: requestBody ? JSON.stringify(requestBody) : undefined,
|
body: requestBody ? JSON.stringify(requestBody) : undefined,
|
||||||
});
|
});
|
||||||
@@ -99,15 +96,8 @@ export async function armRequestWithoutPolling<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
|
const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
|
||||||
if(!response || response.status === 204) {
|
const responseBody = (await response.json()) as T;
|
||||||
return { result: {} as T, operationStatusUrl: operationStatusUrl };
|
return { result: responseBody, operationStatusUrl: operationStatusUrl };
|
||||||
}
|
|
||||||
|
|
||||||
const responseBody = await response.json().catch((error) => {
|
|
||||||
console.error("armRequestWithoutPolling: Error parsing JSON response:", error);
|
|
||||||
return response.text; // Return an empty object if JSON parsing fails
|
|
||||||
});
|
|
||||||
return { result: responseBody as T, operationStatusUrl: operationStatusUrl };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
|
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
|
||||||
@@ -119,7 +109,6 @@ export async function armRequest<T>({
|
|||||||
body: requestBody,
|
body: requestBody,
|
||||||
queryParams,
|
queryParams,
|
||||||
contentType,
|
contentType,
|
||||||
customHeaders
|
|
||||||
}: Options): Promise<T> {
|
}: Options): Promise<T> {
|
||||||
const armRequestResult = await armRequestWithoutPolling<T>({
|
const armRequestResult = await armRequestWithoutPolling<T>({
|
||||||
host,
|
host,
|
||||||
@@ -129,7 +118,6 @@ export async function armRequest<T>({
|
|||||||
body: requestBody,
|
body: requestBody,
|
||||||
queryParams,
|
queryParams,
|
||||||
contentType,
|
contentType,
|
||||||
customHeaders
|
|
||||||
});
|
});
|
||||||
const operationStatusUrl = armRequestResult.operationStatusUrl;
|
const operationStatusUrl = armRequestResult.operationStatusUrl;
|
||||||
if (operationStatusUrl) {
|
if (operationStatusUrl) {
|
||||||
@@ -218,14 +206,6 @@ export async function getOfferingIdsRequest<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
|
const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
|
||||||
if(!response || response.status === 204) {
|
const responseBody = (await response.json()) as T;
|
||||||
return { result: {} as T, operationStatusUrl: operationStatusUrl };
|
return { result: responseBody, operationStatusUrl: operationStatusUrl };
|
||||||
}
|
|
||||||
|
|
||||||
const responseBody = await response.json().catch((error) => {
|
|
||||||
console.error("getOfferingIdsRequest: Error parsing JSON response:", error);
|
|
||||||
return response.text; // Return an empty object if JSON parsing fails
|
|
||||||
});
|
|
||||||
|
|
||||||
return { result: responseBody as T, operationStatusUrl: operationStatusUrl };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
readSubComponentState,
|
readSubComponentState,
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
@@ -299,7 +300,7 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
|||||||
);
|
);
|
||||||
if (!userContext.features.enableAadDataPlane) {
|
if (!userContext.features.enableAadDataPlane) {
|
||||||
Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD");
|
Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD");
|
||||||
if (userContext.apiType === "SQL") {
|
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||||
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
||||||
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
@@ -552,7 +553,7 @@ async function configurePortal(): Promise<Explorer> {
|
|||||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||||
|
|
||||||
let dataPlaneRbacEnabled;
|
let dataPlaneRbacEnabled;
|
||||||
if (userContext.apiType === "SQL") {
|
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||||
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
||||||
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
|
|||||||
Reference in New Issue
Block a user