Skip to content

Commit c212bd8

Browse files
committed
Replace rest client service
It is only used in that one step so I think the service complicated things.
1 parent 8bfbe1d commit c212bd8

9 files changed

+347
-347
lines changed

src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ package com.coder.gateway
55
import com.coder.gateway.models.TokenSource
66
import com.coder.gateway.models.WorkspaceAgentModel
77
import com.coder.gateway.cli.CoderCLIManager
8+
import com.coder.gateway.sdk.BaseCoderRestClient
89
import com.coder.gateway.sdk.CoderRestClient
9-
import com.coder.gateway.sdk.DefaultCoderRestClient
1010
import com.coder.gateway.cli.ensureCLI
1111
import com.coder.gateway.sdk.ex.AuthenticationResponseException
1212
import com.coder.gateway.util.toURL
@@ -128,7 +128,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
128128
* Return an authenticated Coder CLI and the user's name, asking for the
129129
* token as long as it continues to result in an authentication failure.
130130
*/
131-
private fun authenticate(deploymentURL: URL, queryToken: String?, lastToken: Pair<String, TokenSource>? = null): Pair<CoderRestClient, String> {
131+
private fun authenticate(deploymentURL: URL, queryToken: String?, lastToken: Pair<String, TokenSource>? = null): Pair<BaseCoderRestClient, String> {
132132
// Use the token from the query, unless we already tried that.
133133
val isRetry = lastToken != null
134134
val token = if (!queryToken.isNullOrBlank() && !isRetry)
@@ -143,7 +143,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
143143
if (token == null) { // User aborted.
144144
throw IllegalArgumentException("Unable to connect to $deploymentURL, $TOKEN is missing")
145145
}
146-
val client = DefaultCoderRestClient(deploymentURL, token.first)
146+
val client = CoderRestClient(deploymentURL, token.first)
147147
return try {
148148
Pair(client, client.me().username)
149149
} catch (ex: AuthenticationResponseException) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package com.coder.gateway.sdk
2+
3+
import com.coder.gateway.icons.CoderIcons
4+
import com.coder.gateway.icons.toRetinaAwareIcon
5+
import com.coder.gateway.models.WorkspaceAgentModel
6+
import com.coder.gateway.sdk.convertors.InstantConverter
7+
import com.coder.gateway.sdk.ex.AuthenticationResponseException
8+
import com.coder.gateway.sdk.ex.TemplateResponseException
9+
import com.coder.gateway.sdk.ex.WorkspaceResponseException
10+
import com.coder.gateway.sdk.v2.CoderV2RestFacade
11+
import com.coder.gateway.sdk.v2.models.BuildInfo
12+
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
13+
import com.coder.gateway.sdk.v2.models.Template
14+
import com.coder.gateway.sdk.v2.models.User
15+
import com.coder.gateway.sdk.v2.models.Workspace
16+
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
17+
import com.coder.gateway.sdk.v2.models.WorkspaceResource
18+
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
19+
import com.coder.gateway.sdk.v2.models.toAgentModels
20+
import com.coder.gateway.services.CoderSettingsState
21+
import com.coder.gateway.settings.CoderSettings
22+
import com.coder.gateway.util.CoderHostnameVerifier
23+
import com.coder.gateway.util.coderSocketFactory
24+
import com.coder.gateway.util.coderTrustManagers
25+
import com.coder.gateway.util.getHeaders
26+
import com.coder.gateway.util.toURL
27+
import com.coder.gateway.util.withPath
28+
import com.google.gson.Gson
29+
import com.google.gson.GsonBuilder
30+
import com.intellij.openapi.util.SystemInfo
31+
import com.intellij.util.ImageLoader
32+
import com.intellij.util.ui.ImageUtil
33+
import okhttp3.Credentials
34+
import okhttp3.OkHttpClient
35+
import okhttp3.logging.HttpLoggingInterceptor
36+
import org.imgscalr.Scalr
37+
import retrofit2.Retrofit
38+
import retrofit2.converter.gson.GsonConverterFactory
39+
import java.io.IOException
40+
import java.net.HttpURLConnection
41+
import java.net.URL
42+
import java.time.Instant
43+
import java.util.*
44+
import javax.net.ssl.X509TrustManager
45+
import javax.swing.Icon
46+
47+
/**
48+
* In non-test code use CoderRestClient instead.
49+
*/
50+
open class BaseCoderRestClient(
51+
var url: URL, var token: String,
52+
private val settings: CoderSettings = CoderSettings(CoderSettingsState()),
53+
private val proxyValues: ProxyValues? = null,
54+
private val pluginVersion: String = "development",
55+
) {
56+
private val httpClient: OkHttpClient
57+
private val retroRestClient: CoderV2RestFacade
58+
59+
lateinit var me: User
60+
lateinit var buildVersion: String
61+
62+
init {
63+
val gson: Gson = GsonBuilder().registerTypeAdapter(Instant::class.java, InstantConverter()).setPrettyPrinting().create()
64+
65+
val socketFactory = coderSocketFactory(settings.tls)
66+
val trustManagers = coderTrustManagers(settings.tls.caPath)
67+
var builder = OkHttpClient.Builder()
68+
69+
if (proxyValues != null) {
70+
builder = builder
71+
.proxySelector(proxyValues.selector)
72+
.proxyAuthenticator { _, response ->
73+
if (proxyValues.useAuth && proxyValues.username != null && proxyValues.password != null) {
74+
val credentials = Credentials.basic(proxyValues.username, proxyValues.password)
75+
response.request.newBuilder()
76+
.header("Proxy-Authorization", credentials)
77+
.build()
78+
} else null
79+
}
80+
}
81+
82+
httpClient = builder
83+
.sslSocketFactory(socketFactory, trustManagers[0] as X509TrustManager)
84+
.hostnameVerifier(CoderHostnameVerifier(settings.tls.altHostname))
85+
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("Coder-Session-Token", token).build()) }
86+
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("User-Agent", "Coder Gateway/${pluginVersion} (${SystemInfo.getOsNameAndVersion()}; ${SystemInfo.OS_ARCH})").build()) }
87+
.addInterceptor {
88+
var request = it.request()
89+
val headers = getHeaders(url, settings.headerCommand)
90+
if (headers.isNotEmpty()) {
91+
val reqBuilder = request.newBuilder()
92+
headers.forEach { h -> reqBuilder.addHeader(h.key, h.value) }
93+
request = reqBuilder.build()
94+
}
95+
it.proceed(request)
96+
}
97+
// This should always be last if we want to see previous interceptors logged.
98+
.addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BASIC) })
99+
.build()
100+
101+
retroRestClient = Retrofit.Builder().baseUrl(url.toString()).client(httpClient)
102+
.addConverterFactory(GsonConverterFactory.create(gson))
103+
.build().create(CoderV2RestFacade::class.java)
104+
}
105+
106+
/**
107+
* Authenticate and load information about the current user and the build
108+
* version.
109+
*
110+
* @throws [AuthenticationResponseException] if authentication failed.
111+
*/
112+
fun authenticate(): User {
113+
me = me()
114+
buildVersion = buildInfo().version
115+
return me
116+
}
117+
118+
/**
119+
* Retrieve the current user.
120+
* @throws [AuthenticationResponseException] if authentication failed.
121+
*/
122+
fun me(): User {
123+
val userResponse = retroRestClient.me().execute()
124+
if (!userResponse.isSuccessful) {
125+
throw AuthenticationResponseException(
126+
"Unable to authenticate to $url: code ${userResponse.code()}, ${
127+
userResponse.message().ifBlank { "has your token expired?" }
128+
}"
129+
)
130+
}
131+
132+
return userResponse.body()!!
133+
}
134+
135+
/**
136+
* Retrieves the available workspaces created by the user.
137+
* @throws WorkspaceResponseException if workspaces could not be retrieved.
138+
*/
139+
fun workspaces(): List<Workspace> {
140+
val workspacesResponse = retroRestClient.workspaces("owner:me").execute()
141+
if (!workspacesResponse.isSuccessful) {
142+
throw WorkspaceResponseException(
143+
"Unable to retrieve workspaces from $url: code ${workspacesResponse.code()}, reason: ${
144+
workspacesResponse.message().ifBlank { "no reason provided" }
145+
}"
146+
)
147+
}
148+
149+
return workspacesResponse.body()!!.workspaces
150+
}
151+
152+
/**
153+
* Retrieves agents for the specified workspaces, including those that are
154+
* off.
155+
*/
156+
fun agents(workspaces: List<Workspace>): List<WorkspaceAgentModel> {
157+
return workspaces.flatMap {
158+
val resources = resources(it)
159+
it.toAgentModels(resources)
160+
}
161+
}
162+
163+
/**
164+
* Retrieves resources for the specified workspace. The workspaces response
165+
* does not include agents when the workspace is off so this can be used to
166+
* get them instead, just like `coder config-ssh` does (otherwise we risk
167+
* removing hosts from the SSH config when they are off).
168+
*/
169+
fun resources(workspace: Workspace): List<WorkspaceResource> {
170+
val resourcesResponse = retroRestClient.templateVersionResources(workspace.latestBuild.templateVersionID).execute()
171+
if (!resourcesResponse.isSuccessful) {
172+
throw WorkspaceResponseException(
173+
"Unable to retrieve template resources for ${workspace.name} from $url: code ${resourcesResponse.code()}, reason: ${
174+
resourcesResponse.message().ifBlank { "no reason provided" }
175+
}"
176+
)
177+
}
178+
return resourcesResponse.body()!!
179+
}
180+
181+
fun buildInfo(): BuildInfo {
182+
val buildInfoResponse = retroRestClient.buildInfo().execute()
183+
if (!buildInfoResponse.isSuccessful) {
184+
throw java.lang.IllegalStateException("Unable to retrieve build information for $url, code: ${buildInfoResponse.code()}, reason: ${buildInfoResponse.message().ifBlank { "no reason provided" }}")
185+
}
186+
return buildInfoResponse.body()!!
187+
}
188+
189+
private fun template(templateID: UUID): Template {
190+
val templateResponse = retroRestClient.template(templateID).execute()
191+
if (!templateResponse.isSuccessful) {
192+
throw TemplateResponseException(
193+
"Unable to retrieve template with ID $templateID from $url, code: ${templateResponse.code()}, reason: ${
194+
templateResponse.message().ifBlank { "no reason provided" }
195+
}"
196+
)
197+
}
198+
return templateResponse.body()!!
199+
}
200+
201+
fun startWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
202+
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.START, null, null, null, null)
203+
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
204+
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
205+
throw WorkspaceResponseException(
206+
"Unable to build workspace $workspaceName on $url, code: ${buildResponse.code()}, reason: ${
207+
buildResponse.message().ifBlank { "no reason provided" }
208+
}"
209+
)
210+
}
211+
212+
return buildResponse.body()!!
213+
}
214+
215+
fun stopWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
216+
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.STOP, null, null, null, null)
217+
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
218+
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
219+
throw WorkspaceResponseException(
220+
"Unable to stop workspace $workspaceName on $url, code: ${buildResponse.code()}, reason: ${
221+
buildResponse.message().ifBlank { "no reason provided" }
222+
}"
223+
)
224+
}
225+
226+
return buildResponse.body()!!
227+
}
228+
229+
fun updateWorkspace(workspaceID: UUID, workspaceName: String, lastWorkspaceTransition: WorkspaceTransition, templateID: UUID): WorkspaceBuild {
230+
val template = template(templateID)
231+
232+
val buildRequest =
233+
CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null, null)
234+
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
235+
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
236+
throw WorkspaceResponseException(
237+
"Unable to update workspace $workspaceName on $url, code: ${buildResponse.code()}, reason: ${
238+
buildResponse.message().ifBlank { "no reason provided" }
239+
}"
240+
)
241+
}
242+
243+
return buildResponse.body()!!
244+
}
245+
246+
247+
private val iconCache = mutableMapOf<Pair<String, String>, Icon>()
248+
249+
fun loadIcon(path: String, workspaceName: String): Icon {
250+
var iconURL: URL? = null
251+
if (path.startsWith("http")) {
252+
iconURL = path.toURL()
253+
} else if (!path.contains(":") && !path.contains("//")) {
254+
iconURL = url.withPath(path)
255+
}
256+
257+
if (iconURL != null) {
258+
val cachedIcon = iconCache[Pair(workspaceName, path)]
259+
if (cachedIcon != null) {
260+
return cachedIcon
261+
}
262+
val img = ImageLoader.loadFromUrl(iconURL)
263+
if (img != null) {
264+
val icon = toRetinaAwareIcon(Scalr.resize(ImageUtil.toBufferedImage(img), Scalr.Method.ULTRA_QUALITY, 32))
265+
iconCache[Pair(workspaceName, path)] = icon
266+
return icon
267+
}
268+
}
269+
270+
return CoderIcons.fromChar(workspaceName.lowercase().first())
271+
}
272+
}

0 commit comments

Comments
 (0)