1+ /*
2+ * Copyright 2018-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the MIT license.
3+ */
4+
5+ package com.microsoft.azure.toolkit.intellij.cloudshell
6+
7+ import com.azure.core.credential.TokenRequestContext
8+ import com.azure.identity.implementation.util.ScopeUtil
9+ import com.intellij.notification.Notification
10+ import com.intellij.notification.NotificationType
11+ import com.intellij.openapi.application.EDT
12+ import com.intellij.openapi.components.Service
13+ import com.intellij.openapi.components.service
14+ import com.intellij.openapi.diagnostic.logger
15+ import com.intellij.openapi.project.Project
16+ import com.intellij.openapi.wm.ToolWindowManager
17+ import com.intellij.platform.ide.progress.withBackgroundProgress
18+ import com.intellij.platform.util.progress.withProgressText
19+ import com.microsoft.azure.toolkit.intellij.cloudshell.rest.*
20+ import com.microsoft.azure.toolkit.intellij.cloudshell.terminal.AzureCloudTerminalFactory
21+ import com.microsoft.azure.toolkit.lib.Azure
22+ import com.microsoft.azure.toolkit.lib.auth.Account
23+ import com.microsoft.azure.toolkit.lib.auth.AzureAccount
24+ import com.microsoft.azure.toolkit.lib.common.model.Subscription
25+ import kotlinx.coroutines.Dispatchers
26+ import kotlinx.coroutines.delay
27+ import kotlinx.coroutines.reactor.awaitSingleOrNull
28+ import kotlinx.coroutines.withContext
29+ import org.jetbrains.plugins.terminal.TerminalToolWindowFactory
30+ import org.jetbrains.plugins.terminal.TerminalToolWindowManager
31+ import java.net.URI
32+ import kotlin.time.Duration.Companion.milliseconds
33+
34+ @Service(Service .Level .PROJECT )
35+ class CloudShellProvisioningService (private val project : Project ) {
36+ companion object {
37+ fun getInstance (project : Project ) = project.service<CloudShellProvisioningService >()
38+ private val LOG = logger<CloudShellProvisioningService >()
39+ }
40+
41+ private val defaultTerminalColumns = 100
42+ private val defaultTerminalRows = 30
43+
44+ suspend fun provision (subscription : Subscription ) = withBackgroundProgress(project, " Provisioning cloud shell..." ) {
45+ try {
46+ val account = Azure .az(AzureAccount ::class .java).account()
47+ if (! account.isLoggedIn) {
48+ showUnsuccessfulNotification(" Account is not logged in." )
49+ return @withBackgroundProgress
50+ }
51+
52+ val accessTokenResult = setAccessToken(account, subscription.tenantId)
53+ if (! accessTokenResult) return @withBackgroundProgress
54+
55+ val resourceManagerEndpoint = account.environment.resourceManagerEndpoint
56+
57+ val settings = getSettings(resourceManagerEndpoint)
58+ if (settings == null ) {
59+ showUnsuccessfulNotification(" Unable to retrieve cloud shell settings." )
60+ return @withBackgroundProgress
61+ }
62+
63+ val provisionResult = provision(resourceManagerEndpoint, settings)
64+ if (provisionResult == null ) {
65+ showUnsuccessfulNotification(" Unable to provision cloud shell." )
66+ return @withBackgroundProgress
67+ }
68+
69+ val provisionUrl = provisionResult.properties.uri
70+ if (provisionUrl.isNullOrEmpty()) {
71+ LOG .error(" Cloud shell URL was empty" )
72+ return @withBackgroundProgress
73+ }
74+
75+ val provisionTerminalResult = provisionTerminal(provisionUrl, account, subscription.tenantId)
76+ if (provisionTerminalResult == null ) {
77+ showUnsuccessfulNotification(" Unable to provision cloud shell terminal." )
78+ return @withBackgroundProgress
79+ }
80+
81+ val socketUri = provisionTerminalResult.socketUri?.trimEnd(' /' )
82+ if (socketUri.isNullOrEmpty()) {
83+ LOG .error(" Socket URI was empty" )
84+ return @withBackgroundProgress
85+ }
86+
87+ connectShellToTheTerminal(provisionUrl, socketUri)
88+
89+ } catch (t: Throwable ) {
90+ LOG .error(" Unable to provision cloud shell" , t)
91+ showUnsuccessfulNotification(" " )
92+ }
93+ }
94+
95+ private suspend fun setAccessToken (account : Account , tenantId : String ): Boolean {
96+ val accessToken = CloudShellAccessTokenService .getInstance().getAccessToken(tenantId, account)
97+ if (accessToken == null ) {
98+ LOG .warn(" Unable to get access token" )
99+ return false
100+ }
101+
102+ val cloudConsoleService = CloudConsoleService .getInstance(project)
103+ cloudConsoleService.setAccessToken(accessToken, tenantId)
104+
105+ return true
106+ }
107+
108+ private suspend fun getSettings (resourceManagerEndpoint : String ): CloudConsoleUserSettings ? {
109+ val cloudConsoleService = CloudConsoleService .getInstance(project)
110+ val settings = withProgressText(" Retrieving cloud shell preferences..." ) {
111+ cloudConsoleService.getUserSettings(resourceManagerEndpoint)
112+ }
113+ if (settings == null ) {
114+ LOG .warn(" Azure Cloud Shell is not configured in any Azure subscription" )
115+ return null
116+ }
117+
118+ return settings
119+ }
120+
121+ private suspend fun provision (
122+ resourceManagerEndpoint : String ,
123+ settings : CloudConsoleUserSettings
124+ ): CloudConsoleProvisionResult ? {
125+ val cloudConsoleService = CloudConsoleService .getInstance(project)
126+ val preferredOsType = settings.properties?.preferredOsType ? : " Linux"
127+ val provisionParameters = CloudConsoleProvisionParameters (
128+ CloudConsoleProvisionParameterProperties (preferredOsType)
129+ )
130+ val provisionResult = withProgressText(" Provisioning cloud shell..." ) {
131+ cloudConsoleService.provision(
132+ resourceManagerEndpoint,
133+ settings.properties?.preferredLocation,
134+ provisionParameters
135+ )
136+ }
137+
138+ if (provisionResult == null || provisionResult.properties.provisioningState != " Succeeded" ) {
139+ LOG .warn(" Unable to provision cloud shell" )
140+ return null
141+ }
142+
143+ return provisionResult
144+ }
145+
146+ private suspend fun provisionTerminal (
147+ provisionUrl : String ,
148+ account : Account ,
149+ tenantId : String
150+ ): CloudConsoleProvisionTerminalResult ? {
151+ val shellUrl = " $provisionUrl /terminals"
152+
153+ val keyVaultDnsSuffix = " https://" + account.environment.keyVaultDnsSuffix.trimStart(' .' ) + " /"
154+ val vaultScopes = ScopeUtil .resourceToScopes(keyVaultDnsSuffix)
155+ val vaultToken = account
156+ .getTenantTokenCredential(tenantId)
157+ .getToken(TokenRequestContext ().apply { addScopes(* vaultScopes) })
158+ .awaitSingleOrNull()
159+ ?.token
160+
161+ if (vaultToken == null ) {
162+ LOG .warn(" Unable to obtain a vault token" )
163+ return null
164+ }
165+
166+ val shellUri = URI (shellUrl)
167+ val referrer = " ${shellUri.scheme} ://${shellUri.host} /\$ hc${shellUri.path} "
168+ val provisionTerminalParameters = CloudConsoleProvisionTerminalParameters (
169+ listOf (vaultToken)
170+ )
171+ val cloudConsoleService = CloudConsoleService .getInstance(project)
172+ val provisionTerminalResult = withProgressText(" Requesting cloud shell terminal..." ) {
173+ cloudConsoleService.provisionTerminal(
174+ shellUrl,
175+ defaultTerminalColumns,
176+ defaultTerminalRows,
177+ referrer,
178+ provisionTerminalParameters,
179+ )
180+ }
181+ if (provisionTerminalResult == null ) {
182+ LOG .warn(" Unable to provision cloud shell terminal" )
183+ return null
184+ }
185+
186+ return provisionTerminalResult
187+ }
188+
189+ private suspend fun connectShellToTheTerminal (provisionUrl : String , socketUri : String ) =
190+ withProgressText(" Connecting to cloud shell terminal..." ) {
191+ val runner = AzureCloudTerminalFactory
192+ .getInstance(project)
193+ .createTerminalRunner(provisionUrl, socketUri)
194+
195+ val terminalWindow = ToolWindowManager .getInstance(project)
196+ .getToolWindow(TerminalToolWindowFactory .TOOL_WINDOW_ID )
197+ ? : return @withProgressText
198+
199+ withContext(Dispatchers .EDT ) {
200+ terminalWindow.show()
201+
202+ delay(500 .milliseconds)
203+
204+ TerminalToolWindowManager
205+ .getInstance(project).createNewSession(runner)
206+ }
207+ }
208+
209+ private suspend fun showUnsuccessfulNotification (message : String ) = withContext(Dispatchers .EDT ) {
210+ Notification (
211+ " Azure CloudShell" ,
212+ " Unable to provision cloud shell" ,
213+ message,
214+ NotificationType .WARNING
215+ )
216+ .notify(project)
217+ }
218+ }
0 commit comments