diff --git a/src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx b/src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx index 23979a21f..5bdbb449c 100644 --- a/src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx +++ b/src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx @@ -5,12 +5,13 @@ import "xterm/css/xterm.css"; import { TerminalKind } from "../../../Contracts/ViewModels"; import { startCloudShellTerminal } from "./UseTerminal"; + export interface CloudShellTerminalProps { shellType: TerminalKind; } export const CloudShellTerminalComponent: React.FC = ({ - shellType + shellType }: CloudShellTerminalProps) => { const terminalRef = useRef(null); // Reference for terminal container const xtermRef = useRef(null); // Reference for XTerm instance diff --git a/src/Explorer/Tabs/CloudShellTab/Data.tsx b/src/Explorer/Tabs/CloudShellTab/Data.tsx index 7c56b413d..689d27328 100644 --- a/src/Explorer/Tabs/CloudShellTab/Data.tsx +++ b/src/Explorer/Tabs/CloudShellTab/Data.tsx @@ -2,12 +2,45 @@ * 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; @@ -16,6 +49,47 @@ export const validateUserSettings = (userSettings: Settings) => { } } +// 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, @@ -26,25 +100,6 @@ export const getUserRegion = async (subscriptionId: string, resourceGroup: strin }; -export async function GetARMCall(path: string, apiVersion: string = "2024-07-01"): Promise { - return await armRequest({ - host: configContext.ARM_ENDPOINT, - path: path, - method: "GET", - apiVersion: apiVersion - }); -}; - -export async function PutARMCall(path: string, request: object, apiVersion: string = "2024-07-01"): Promise { - return await armRequest({ - host: configContext.ARM_ENDPOINT, - path: path, - method: "PUT", - apiVersion: apiVersion, - body: request - }); -}; - export const deleteUserSettings = async (): Promise => { await armRequest({ host: configContext.ARM_ENDPOINT, @@ -173,5 +228,93 @@ export const getNormalizedRegion = (region: string, defaultCloudshellRegion: str const normalizedRegion = regionMap[region.toLowerCase()] || region; return validCloudShellRegions.has(normalizedRegion.toLowerCase()) ? normalizedRegion : defaultCloudshellRegion; - }; +}; +export async function getNetworkProfileInfo(networkProfileResourceId: string, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.NETWORK); + return await GetARMCall(networkProfileResourceId, apiVersion); +} + +export async function getAccountDetails(databaseAccountId: string, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.DATABASE); + return await GetARMCall(databaseAccountId, apiVersion); +} + +export async function getVnetInformation(vnetId: string, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.VNET); + return await GetARMCall(vnetId, apiVersion); +} + +export async function getSubnetInformation(subnetId: string, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.SUBNET); + return await GetARMCall(subnetId, apiVersion); +} + +export async function updateSubnetInformation(subnetId: string, request: object, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.SUBNET); + return await PutARMCall(subnetId, request, apiVersion); +} + +export async function updateDatabaseAccount(accountId: string, request: object, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.DATABASE); + return await PutARMCall(accountId, request, apiVersion); +} + +export async function getDatabaseOperations(accountId: string, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.DATABASE); + return await GetARMCall(`${accountId}/operations`, apiVersion); +} + +export async function updateVnet(vnetId: string, request: object, apiVersionOverride?: string) { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.VNET); + return await PutARMCall(vnetId, request, apiVersion); +} + +export async function getVnet(vnetId: string, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.VNET); + return await GetARMCall(vnetId, apiVersion); +} + +export async function createNetworkProfile(networkProfileId: string, request: object, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.NETWORK); + return await PutARMCall(networkProfileId, request, apiVersion); +} + +export async function createRelay(relayId: string, request: object, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.RELAY); + return await PutARMCall(relayId, request, apiVersion); +} + +export async function getRelay(relayId: string, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.RELAY); + return await GetARMCall(relayId, apiVersion); +} + +export async function createRoleOnNetworkProfile(roleid: string, request: object, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.ROLE); + return await PutARMCall(roleid, request, apiVersion); +} + +export async function createRoleOnRelay(roleid: string, request: object, apiVersionOverride?: string): Promise { + const apiVersion = apiVersionOverride || getApiVersion(ResourceType.ROLE); + return await PutARMCall(roleid, request, apiVersion); +} + +export async function GetARMCall(path: string, apiVersion: string = API_VERSIONS.DEFAULT): Promise { + return await armRequest({ + host: configContext.ARM_ENDPOINT, + path: path, + method: "GET", + apiVersion: apiVersion + }); +} + +export async function PutARMCall(path: string, request: object, apiVersion: string = API_VERSIONS.DEFAULT): Promise { + return await armRequest({ + host: configContext.ARM_ENDPOINT, + path: path, + method: "PUT", + apiVersion: apiVersion, + body: request + }); +} \ No newline at end of file diff --git a/src/Explorer/Tabs/CloudShellTab/DataModels.tsx b/src/Explorer/Tabs/CloudShellTab/DataModels.tsx index dcf07e91f..626325590 100644 --- a/src/Explorer/Tabs/CloudShellTab/DataModels.tsx +++ b/src/Explorer/Tabs/CloudShellTab/DataModels.tsx @@ -1,7 +1,7 @@ /** * Copyright (c) Microsoft Corporation. All rights reserved. */ - +import { TerminalKind } from "../../../Contracts/ViewModels"; export const enum OsType { Linux = "linux", @@ -150,5 +150,36 @@ 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; + }; + }; + }; + diff --git a/src/Explorer/Tabs/CloudShellTab/LogFormatter.tsx b/src/Explorer/Tabs/CloudShellTab/LogFormatter.tsx new file mode 100644 index 000000000..ad30e0955 --- /dev/null +++ b/src/Explorer/Tabs/CloudShellTab/LogFormatter.tsx @@ -0,0 +1,29 @@ + +/** + * 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` +}; diff --git a/src/Explorer/Tabs/CloudShellTab/UseTerminal.tsx b/src/Explorer/Tabs/CloudShellTab/UseTerminal.tsx index b7aec420b..1ee89487b 100644 --- a/src/Explorer/Tabs/CloudShellTab/UseTerminal.tsx +++ b/src/Explorer/Tabs/CloudShellTab/UseTerminal.tsx @@ -7,21 +7,40 @@ import { listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import { v4 as uuidv4 } from 'uuid'; import { Terminal } from "xterm"; import { TerminalKind } from "../../../Contracts/ViewModels"; +import { + IsPublicAccessAvailable, + hasPrivateEndpointsRestrictions, + hasVNetRestrictions +} from "Explorer/Tabs/Shared/CheckFirewallRules"; import { userContext } from "../../../UserContext"; import { AttachAddon } from "./AttachAddOn"; import { getCommands } from "./Commands"; import { authorizeSession, connectTerminal, - GetARMCall, + createNetworkProfile, + createRelay, + createRoleOnNetworkProfile, + createRoleOnRelay, + getAccountDetails, + getDatabaseOperations, + getNetworkProfileInfo, getNormalizedRegion, + getRelay, + getSubnetInformation, getUserSettings, + getVnet, + getVnetInformation, provisionConsole, - PutARMCall, putEphemeralUserSettings, registerCloudShellProvider, + setShellType, + updateDatabaseAccount, + updateSubnetInformation, + updateVnet, verifyCloudShellProviderRegistration } from "./Data"; +import { terminalLog } from "./LogFormatter"; // Constants const DEFAULT_CLOUDSHELL_REGION = "westus"; @@ -33,39 +52,13 @@ 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"; -/** - * Standardized terminal logging functions for consistent formatting - */ -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` -}; - /** * Main function to start a CloudShell terminal */ export const startCloudShellTerminal = async (terminal: Terminal, shellType: TerminalKind) => { + // Set the shell type to use the appropriate API version for all calls + setShellType(shellType); + terminal.writeln(terminalLog.header("Initializing Azure CloudShell")); await ensureCloudShellProviderRegistered(terminal); @@ -77,57 +70,72 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter return {}; // Exit if user declined } - // Check database network restrictions - const hasNetworkRestrictions = hasDatabaseNetworkRestrictions(); + const isAllPublicAccessEnabled = await IsPublicAccessAvailable(shellType); let settings: Settings | undefined; - let vNetSettings: VnetSettings | undefined; + let cloudShellVnetSettings: VnetSettings | undefined; let finalVNetSettings: VnetSettings | {}; - if (hasNetworkRestrictions) { + if (!isAllPublicAccessEnabled) { // Fetch and process user settings for restricted networks terminal.writeln(terminalLog.database("Network restrictions detected")); terminal.writeln(terminalLog.info("Loading CloudShell configuration...")); - settings = await fetchUserSettings(terminal); - vNetSettings = await retrieveCloudShellVnetSettings(settings, terminal); - + settings = await getUserSettings(); + if (!settings) { + terminal.writeln(terminalLog.warning("No existing user settings found.")); + } + else { + cloudShellVnetSettings = await retrieveCloudShellVnetSettings(settings, terminal); + } + // If CloudShell has VNet settings, check with database config - if (vNetSettings && vNetSettings.networkProfileResourceId) { + if (cloudShellVnetSettings && cloudShellVnetSettings.networkProfileResourceId) { + + //TODO: askForVNetConfigConsent consent should change to support private end point + // if shell type is vcore or for any other shell as private endpoint restrictions const isContinueWithSameVnet = await askForVNetConfigConsent(terminal); if(isContinueWithSameVnet) { - const isVNetInDatabaseConfig = await isCloudShellVNetInDatabaseConfig(vNetSettings, terminal); + // TODO: isCloudShellVNetInDatabaseConfig call should handle private endpoint check also. + // a private endpoint is associated with the vnet and subnet. + // vcore supports only private endpoint, it doesnot support vnet, dbAccount?.properties?.privateEndpointConnections doesn't have any info. ARM call might needs to make to get private endpoint info + const isVNetInDatabaseConfig = await isCloudShellVNetInDatabaseConfig(cloudShellVnetSettings, terminal); if (!isVNetInDatabaseConfig) { terminal.writeln(terminalLog.warning("CloudShell VNet is not configured in database access list")); - const addToDatabase = await askToAddVNetToDatabase(terminal, vNetSettings); + // TODO: Add logic in askToAddVNetToDatabase to ask accordingly if user has private endpoint or vnet settings + const addToDatabase = await askToAddVNetToDatabase(terminal, cloudShellVnetSettings); if (addToDatabase) { - await addCloudShellVNetToDatabase(vNetSettings, terminal); + // TODO: Add a logic to add private endpoint to database if user has private endpoint settings in the database + await addCloudShellVNetToDatabase(cloudShellVnetSettings, terminal); } else { // User declined to add VNet to database, need to recreate terminal.writeln(terminalLog.warning("Will configure new VNet...")); - vNetSettings = undefined; + cloudShellVnetSettings = undefined; } } else { terminal.writeln(terminalLog.success("CloudShell VNet is already in database configuration")); } } else { - vNetSettings = undefined; // User declined to use existing VNet settings + cloudShellVnetSettings = undefined; // User declined to use existing VNet settings } } - // Configure VNet if needed - if (!vNetSettings || !vNetSettings.networkProfileResourceId) { - terminal.writeln(terminalLog.subheader("Configuring network infrastructure")); - finalVNetSettings = await configureCloudShellVNet(terminal, resolvedRegion, vNetSettings); + if (!cloudShellVnetSettings || !cloudShellVnetSettings.networkProfileResourceId) { - // Add the new VNet to database configuration + //TODO: Add logic to check if the user has existing VNet or private endpoint (which would be associated with the Vnet) settings in the database with cloudshell as CloudShellDelegation, + // if yes, use that vnet to get network profile and get the existing relay and + // use that for cloudshell, else create only those resources which are required and ternminal write the existing relay and vnet settings.which it is going to use. + terminal.writeln(terminalLog.subheader("Configuring network infrastructure")); + finalVNetSettings = await configureCloudShellVNet(terminal, resolvedRegion); + + // TODO: Add a logic to add private endpoint to database if user has private endpoint settings in the database await addCloudShellVNetToDatabase(finalVNetSettings as VnetSettings, terminal); } else { terminal.writeln(terminalLog.success("Using existing network configuration")); - finalVNetSettings = vNetSettings; + finalVNetSettings = cloudShellVnetSettings; } } else { terminal.writeln(terminalLog.database("Public access enabled. Skipping VNet configuration.")); @@ -144,13 +152,13 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter }; try { - sessionDetails = await provisionCloudShellSession(resolvedRegion, terminal, finalVNetSettings); + sessionDetails = await provisionCloudShellSession(resolvedRegion, terminal, finalVNetSettings, 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, finalVNetSettings); + sessionDetails = await provisionCloudShellSession(defaultCloudShellRegion, terminal, finalVNetSettings, isAllPublicAccessEnabled); } if (!sessionDetails.socketUri) { @@ -171,13 +179,21 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter }; /** - * Asks the user if they want to use existing VNet settings or create new ones + * Asks the user if they want to use existing network configuration (VNet or private endpoint) */ -const askForVNetConfigConsent = async (terminal: Terminal): Promise => { +const askForVNetConfigConsent = async (terminal: Terminal, shellType: TerminalKind = null): Promise => { + // 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 network configuration? (Y/N)")); - terminal.writeln(terminalLog.info("Answering 'N' will configure a new network for CloudShell")); + terminal.writeln(terminalLog.prompt(`Use this existing ${networkType} configuration? (Y/N)`)); + terminal.writeln(terminalLog.info(`Answering 'N' will configure a new ${networkType} for CloudShell`)); return new Promise((resolve) => { const keyListener = terminal.onKey(({ key }: { key: string }) => { @@ -185,10 +201,10 @@ const askForVNetConfigConsent = async (terminal: Terminal): Promise => terminal.writeln(""); if (key.toLowerCase() === 'y') { - terminal.writeln(terminalLog.success("Proceeding with existing network configuration")); + terminal.writeln(terminalLog.success(`Proceeding with existing ${networkType} configuration`)); resolve(true); } else { - terminal.writeln(terminalLog.info("Will configure new network settings")); + terminal.writeln(terminalLog.info(`Will configure new ${networkType} settings`)); resolve(false); } }); @@ -203,7 +219,7 @@ const isCloudShellVNetInDatabaseConfig = async (vNetSettings: VnetSettings, term terminal.writeln(terminalLog.subheader("Verifying if CloudShell VNet is configured in database")); // Get the subnet ID from the CloudShell Network Profile - const netProfileInfo = await GetARMCall(vNetSettings.networkProfileResourceId); + const netProfileInfo = await getNetworkProfileInfo(vNetSettings.networkProfileResourceId); if (!netProfileInfo?.properties?.containerNetworkInterfaceConfigurations?.[0] ?.properties?.ipConfigurations?.[0]?.properties?.subnet?.id) { @@ -225,9 +241,8 @@ const isCloudShellVNetInDatabaseConfig = async (vNetSettings: VnetSettings, term const vnetRules = dbAccount.properties.virtualNetworkRules; // Check if the CloudShell subnet is already in the rules - const isAlreadyConfigured = vnetRules.some(rule => rule.id === cloudShellSubnetId); + return vnetRules.some(rule => rule.id === cloudShellSubnetId); - return isAlreadyConfigured; } catch (err) { terminal.writeln(terminalLog.error("Error checking database VNet configuration")); return false; @@ -271,7 +286,7 @@ const addCloudShellVNetToDatabase = async (vNetSettings: VnetSettings, terminal: const { cloudShellSubnetId, cloudShellVnetId } = await getCloudShellNetworkIds(vNetSettings, terminal); // Step 2: Get current database account details - const { dbAccountId, currentDbAccount } = await getDatabaseAccountDetails(terminal); + const { currentDbAccount } = await getDatabaseAccountDetails(terminal); // Step 3: Check if VNet is already configured in database if (await isVNetAlreadyConfigured(cloudShellSubnetId, currentDbAccount, terminal)) { @@ -280,7 +295,7 @@ const addCloudShellVNetToDatabase = async (vNetSettings: VnetSettings, terminal: // Step 4: Check network resource statuses const { vnetInfo, subnetInfo, operationInProgress } = - await checkNetworkResourceStatuses(cloudShellSubnetId, cloudShellVnetId, dbAccountId, terminal); + await checkNetworkResourceStatuses(cloudShellSubnetId, cloudShellVnetId, currentDbAccount.id, terminal); // Step 5: If no operation in progress, update the subnet and database if (!operationInProgress) { @@ -288,11 +303,11 @@ const addCloudShellVNetToDatabase = async (vNetSettings: VnetSettings, terminal: await enableCosmosDBServiceEndpoint(cloudShellSubnetId, subnetInfo, terminal); // Step 5b: Update database account with VNet rule - await updateDatabaseWithVNetRule(currentDbAccount, cloudShellSubnetId, dbAccountId, terminal); + await updateDatabaseWithVNetRule(currentDbAccount, cloudShellSubnetId, currentDbAccount.id, terminal); } else { terminal.writeln(terminalLog.info("Monitoring existing VNet operation...")); // Step 6: Monitor the update progress - await monitorVNetAdditionProgress(cloudShellSubnetId, dbAccountId, terminal); + await monitorVNetAdditionProgress(cloudShellSubnetId, currentDbAccount.id, terminal); } } catch (err) { @@ -305,7 +320,7 @@ const addCloudShellVNetToDatabase = async (vNetSettings: VnetSettings, terminal: * Gets the subnet and VNet IDs from CloudShell Network Profile */ const getCloudShellNetworkIds = async (vNetSettings: VnetSettings, terminal: Terminal): Promise<{ cloudShellSubnetId: string; cloudShellVnetId: string }> => { - const netProfileInfo = await GetARMCall(vNetSettings.networkProfileResourceId, "2023-05-01"); + const netProfileInfo = await getNetworkProfileInfo(vNetSettings.networkProfileResourceId); if (!netProfileInfo?.properties?.containerNetworkInterfaceConfigurations?.[0] ?.properties?.ipConfigurations?.[0]?.properties?.subnet?.id) { @@ -328,14 +343,12 @@ const getCloudShellNetworkIds = async (vNetSettings: VnetSettings, terminal: Ter /** * Gets the database account details */ -const getDatabaseAccountDetails = async (terminal: Terminal): Promise<{ dbAccountId: string; currentDbAccount: any }> => { +const getDatabaseAccountDetails = async (terminal: Terminal): Promise<{ currentDbAccount: any }> => { const dbAccount = userContext.databaseAccount; - const dbAccountId = `/subscriptions/${userContext.subscriptionId}/resourceGroups/${userContext.resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${dbAccount.name}`; - terminal.writeln(terminalLog.database("Verifying current configuration")); - const currentDbAccount = await GetARMCall(dbAccountId, "2023-04-15"); + const currentDbAccount = await getAccountDetails(dbAccount.id); - return { dbAccountId, currentDbAccount }; + return { currentDbAccount }; }; /** @@ -372,8 +385,8 @@ const checkNetworkResourceStatuses = async ( if (cloudShellVnetId && cloudShellSubnetId) { // Get VNet and subnet resource status - vnetInfo = await GetARMCall(cloudShellVnetId, "2023-05-01"); - subnetInfo = await GetARMCall(cloudShellSubnetId, "2023-05-01"); + vnetInfo = await getVnetInformation(cloudShellVnetId); + subnetInfo = await getSubnetInformation(cloudShellSubnetId); // Check if there's an ongoing operation on the VNet or subnet const vnetProvisioningState = vnetInfo?.properties?.provisioningState; @@ -390,7 +403,7 @@ const checkNetworkResourceStatuses = async ( } // Also check database operations - const latestDbAccount = await GetARMCall(dbAccountId, "2023-04-15"); + const latestDbAccount = await getAccountDetails(dbAccountId); if (latestDbAccount.properties.virtualNetworkRules) { const isPendingAdd = latestDbAccount.properties.virtualNetworkRules.some( @@ -455,14 +468,14 @@ const enableCosmosDBServiceEndpoint = async (cloudShellSubnetId: string, subnetI }; // Apply the subnet update - await PutARMCall(subnetUrl, subnetUpdatePayload, "2023-05-01"); + 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 GetARMCall(subnetUrl, "2023-05-01"); + const updatedSubnet = await getSubnetInformation(subnetUrl); const endpointEnabled = updatedSubnet.properties.serviceEndpoints && updatedSubnet.properties.serviceEndpoints.some( @@ -504,7 +517,7 @@ const updateDatabaseWithVNetRule = async (currentDbAccount: any, cloudShellSubne // Update the database account terminal.writeln(terminalLog.subheader("Submitting VNet update request to database")); - await PutARMCall(dbAccountId, updatePayload, "2023-04-15"); + await updateDatabaseAccount(dbAccountId, updatePayload); terminal.writeln(terminalLog.success("Updated Database account with Cloud Shell Vnet")); }; @@ -522,7 +535,7 @@ const monitorVNetAdditionProgress = async (cloudShellSubnetId: string, dbAccount while (!updateComplete && retryCount < MAX_RETRY_COUNT) { // Check if the VNet is now in the database account - const updatedDbAccount = await GetARMCall(dbAccountId, "2023-04-15"); + const updatedDbAccount = await getAccountDetails(dbAccountId); const isVNetAdded = updatedDbAccount.properties.virtualNetworkRules?.some( (rule: any) => rule.id === cloudShellSubnetId && (!rule.status || rule.status === 'Succeeded') @@ -535,7 +548,7 @@ const monitorVNetAdditionProgress = async (cloudShellSubnetId: string, dbAccount } // If not yet added, check for operation progress - const operations = await GetARMCall(`${dbAccountId}/operations`, "2023-04-15"); + const operations = await getDatabaseOperations(dbAccountId); // Find network-related operations const networkOps = operations.value?.filter( @@ -584,55 +597,6 @@ const monitorVNetAdditionProgress = async (cloudShellSubnetId: string, dbAccount } }; -/** - * Checks if the database account has network restrictions - */ -const hasDatabaseNetworkRestrictions = (): boolean => { - const dbAccount = userContext.databaseAccount; - - if (!dbAccount) { - return false; - } - - // Check for virtual network filters - const hasVNetFilters = dbAccount.properties.virtualNetworkRules && dbAccount.properties.virtualNetworkRules.length > 0; - - // Check for IP-based firewall - const hasIpRules = dbAccount.properties.isVirtualNetworkFilterEnabled; - - // Check for private endpoints - const hasPrivateEndpoints = dbAccount.properties.privateEndpointConnections && - dbAccount.properties.privateEndpointConnections.length > 0; - - return hasVNetFilters || hasIpRules || hasPrivateEndpoints; -}; - -/** - * Checks if there's an ongoing VNet operation for the database account - */ -const isVNetOperationInProgress = async (dbAccountId: string): Promise => { - try { - // Get the ongoing operations for the database account - const operationsUrl = `${dbAccountId}/operations`; - const operations = await GetARMCall(operationsUrl); - - if (!operations || !operations.value || !operations.value.length) { - return false; - } - - // Check if there's any network-related operation in progress - return operations.value.some( - (op: any) => - op.properties.status === 'InProgress' && - (op.properties.description?.toLowerCase().includes('network') || - op.properties.description?.toLowerCase().includes('vnet')) - ); - } catch (err) { - // If we can't check operations, assume no operations in progress - return false; - } -}; - /** * Ensures that the CloudShell provider is registered for the current subscription */ @@ -652,32 +616,22 @@ const ensureCloudShellProviderRegistered = async (terminal: Terminal): Promise => { - try { - return await getUserSettings(); - } catch (err) { - terminal.writeln(terminalLog.warning("No user settings found. Using defaults.")); - return undefined; - } -}; - /** * Retrieves existing VNet settings from user settings if available */ const retrieveCloudShellVnetSettings = async (settings: Settings, terminal: Terminal): Promise => { if (settings?.properties?.vnetSettings && Object.keys(settings.properties.vnetSettings).length > 0) { try { - const netProfileInfo = await GetARMCall(settings.properties.vnetSettings.networkProfileResourceId); + const netProfileInfo = await getNetworkProfileInfo(settings.properties.vnetSettings.networkProfileResourceId); terminal.writeln(terminalLog.header("Existing Network Configuration")); - const vnetResourceId = netProfileInfo.properties.containerNetworkInterfaceConfigurations[0] - .properties.ipConfigurations[0].properties.subnet.id.replace(/\/subnets\/[^/]+$/, ''); + 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)); @@ -709,31 +663,33 @@ const determineCloudShellRegion = (terminal: Terminal): { resolvedRegion: string /** * Configures a new VNet for CloudShell */ -const configureCloudShellVNet = async (terminal: Terminal, resolvedRegion: string, vNetSettings: VnetSettings): Promise => { +const configureCloudShellVNet = async (terminal: Terminal, resolvedRegion: string): Promise => { + + // TODO: 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-netprofile-${randomSuffix}`; + const networkProfileName = `cloudshell-network-profile-${randomSuffix}`; const relayName = `cloudshell-relay-${randomSuffix}`; terminal.writeln(terminalLog.header("Network Resource Configuration")); const azureContainerInstanceOID = await askQuestion( terminal, - "Azure Container Instance OID", + "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, - "VNet subscription ID", + "Enter Virtual Network Subscription ID", userContext.subscriptionId ); const vNetResourceGroup = await askQuestion( terminal, - "VNet resource group", + "Enter Virtual Network Resource Group", userContext.resourceGroup ); @@ -749,7 +705,7 @@ const configureCloudShellVNet = async (terminal: Terminal, resolvedRegion: strin ); // Step 2: Create Network Profile - await createNetworkProfile( + await createNetworkProfileWithVnet( vNetSubscriptionId, vNetResourceGroup, vnetName, @@ -836,13 +792,13 @@ const createCloudShellVnet = async ( }; terminal.writeln(terminalLog.vnet(`Creating VNet: ${vnetName}`)); - let vNetResponse = await PutARMCall( + let vNetResponse = await updateVnet( `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/virtualNetworks/${vnetName}`, vNetConfigPayload ); while (vNetResponse?.properties?.provisioningState !== "Succeeded") { - vNetResponse = await GetARMCall( + vNetResponse = await getVnet( `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/virtualNetworks/${vnetName}` ); @@ -862,7 +818,7 @@ const createCloudShellVnet = async ( /** * Creates a Network Profile for CloudShell */ -const createNetworkProfile = async ( +const createNetworkProfileWithVnet = async ( vNetSubscriptionId: string, vNetResourceGroup: string, vnetName: string, @@ -897,14 +853,13 @@ const createNetworkProfile = async ( }; terminal.writeln(terminalLog.vnet("Creating Network Profile")); - let networkProfileResponse = await PutARMCall( + let networkProfileResponse = await createNetworkProfile( `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfileName}`, - createNetworkProfilePayload, - "2024-01-01" + createNetworkProfilePayload ); while (networkProfileResponse?.properties?.provisioningState !== "Succeeded") { - networkProfileResponse = await GetARMCall( + networkProfileResponse = await getNetworkProfileInfo( `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfileName}` ); @@ -939,14 +894,13 @@ const createNetworkRelay = async ( }; terminal.writeln(terminalLog.vnet("Creating Relay Namespace")); - let relayResponse = await PutARMCall( + let relayResponse = await createRelay( `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Relay/namespaces/${relayName}`, - relayPayload, - "2024-01-01" + relayPayload ); while (relayResponse?.properties?.provisioningState !== "Succeeded") { - relayResponse = await GetARMCall( + relayResponse = await getRelay( `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Relay/namespaces/${relayName}` ); @@ -981,10 +935,9 @@ const assignRoleToNetworkProfile = async ( }; terminal.writeln(terminalLog.info("Assigning permissions to Network Profile")); - await PutARMCall( + await createRoleOnNetworkProfile( `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfileName}/providers/Microsoft.Authorization/roleAssignments/${nfRoleName}`, - networkProfileRoleAssignmentPayload, - "2022-04-01" + networkProfileRoleAssignmentPayload ); terminal.writeln(terminalLog.success("Network Profile permissions assigned")); @@ -1009,10 +962,9 @@ const assignRoleToRelay = async ( }; terminal.writeln(terminalLog.info("Assigning permissions to Relay Namespace")); - await PutARMCall( + await createRoleOnRelay( `/subscriptions/${vNetSubscriptionId}/resourceGroups/${vNetResourceGroup}/providers/Microsoft.Relay/namespaces/${relayName}/providers/Microsoft.Authorization/roleAssignments/${relayRoleName}`, - relayRoleAssignmentPayload, - "2022-04-01" + relayRoleAssignmentPayload ); terminal.writeln(terminalLog.success("Relay Namespace permissions assigned")); @@ -1024,7 +976,8 @@ const assignRoleToRelay = async ( const provisionCloudShellSession = async ( resolvedRegion: string, terminal: Terminal, - vNetSettings: object + vNetSettings: object, + isAllPublicAccessEnabled: boolean ): Promise<{ socketUri?: string; provisionConsoleResponse?: any; targetUri?: string }> => { return new Promise( async (resolve, reject) => { try { @@ -1052,7 +1005,7 @@ const provisionCloudShellSession = async ( terminal.writeln(terminalLog.warning("No VNet configuration provided")); terminal.writeln(terminalLog.warning("CloudShell will be provisioned with public network access")); - if (hasDatabaseNetworkRestrictions()) { + 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")); } diff --git a/src/Explorer/Tabs/Shared/CheckFirewallRules.ts b/src/Explorer/Tabs/Shared/CheckFirewallRules.ts index d2774ac40..33c7f8059 100644 --- a/src/Explorer/Tabs/Shared/CheckFirewallRules.ts +++ b/src/Explorer/Tabs/Shared/CheckFirewallRules.ts @@ -1,5 +1,6 @@ import { configContext } from "ConfigContext"; import * as DataModels from "Contracts/DataModels"; +import * as ViewModels from "Contracts/ViewModels"; import { userContext } from "UserContext"; import { armRequest } from "Utils/arm/request"; @@ -10,16 +11,8 @@ export async function checkFirewallRules( setMessageFunc?: (message: string) => void, message?: string, ): Promise { - 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); + + const isEnabled = await callFirewallAPis(apiVersion, firewallRulesPredicate); if (isAllPublicIPAddressesEnabled) { isAllPublicIPAddressesEnabled(isEnabled); @@ -41,4 +34,90 @@ export async function checkFirewallRules( 30000, ); } -} \ No newline at end of file +} + +export async function callFirewallAPis( + apiVersion: string, + firewallRulesPredicate: (rule: DataModels.FirewallRule) => unknown): + Promise { + 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 | React.Dispatch>): Promise { + 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 { + 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 +}; \ No newline at end of file diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index 918f89726..0f7a47e5b 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -1,7 +1,7 @@ import { Spinner, SpinnerSize } from "@fluentui/react"; import { MessageTypes } from "Contracts/ExplorerContracts"; import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification"; -import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; +import { checkNetworkRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; import * as ko from "knockout"; import * as React from "react"; import FirewallRuleScreenshot from "../../../images/firewallRule.png"; @@ -65,23 +65,16 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { * CloudShell terminal tab */ class CloudShellTerminalComponentAdapter implements ReactAdapter { + // parameters: true: show, false: hide public parameters: ko.Computed; constructor( - private isAllPublicIPAddressesEnabled: ko.Observable, private kind: ViewModels.TerminalKind, ) {} public renderComponent(): JSX.Element { - if (!this.isAllPublicIPAddressesEnabled()) { - return ( - - ); - } + + console.log("this.parameters() " + this.parameters() ); return this.parameters() ? ( @@ -109,39 +102,25 @@ export default class TerminalTab extends TabsBase { private terminalComponentAdapter: any; private isAllPublicIPAddressesEnabled: ko.Observable; - constructor(options: TerminalTabOptions) { + constructor (options: TerminalTabOptions) { super(options); this.container = options.container; this.isAllPublicIPAddressesEnabled = ko.observable(true); - 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, - ); - } + checkNetworkRules(options.kind, this.isAllPublicIPAddressesEnabled); this.initializeNotebookTerminalAdapter(options); - } - private initializeNotebookTerminalAdapter(options: TerminalTabOptions): void { + private async initializeNotebookTerminalAdapter(options: TerminalTabOptions): Promise { if (userContext.features.enableCloudShell) { this.terminalComponentAdapter = new CloudShellTerminalComponentAdapter( - this.isAllPublicIPAddressesEnabled, options.kind ); + + this.terminalComponentAdapter.parameters = ko.computed(() => + this.isTemplateReady() + ); } else { this.terminalComponentAdapter = new NotebookTerminalComponentAdapter( @@ -152,15 +131,14 @@ export default class TerminalTab extends TabsBase { this.isAllPublicIPAddressesEnabled, options.kind ); + + this.terminalComponentAdapter.parameters = ko.computed(() => + this.isTemplateReady() && + useNotebook.getState().isNotebookEnabled && + useNotebook.getState().notebookServerInfo?.notebookServerEndpoint && + this.isAllPublicIPAddressesEnabled() + ); } - - this.terminalComponentAdapter.parameters = ko.computed(() => - this.isTemplateReady() && - (userContext.features.enableCloudShell || - (useNotebook.getState().isNotebookEnabled && - useNotebook.getState().notebookServerInfo?.notebookServerEndpoint)) && - this.isAllPublicIPAddressesEnabled() - ); } public getContainer(): Explorer {