Skip to content

Commit c45b1e0

Browse files
committed
Separate services
1 parent 6497be2 commit c45b1e0

18 files changed

+515
-444
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ package com.coder.gateway
44

55
import com.coder.gateway.models.TokenSource
66
import com.coder.gateway.models.WorkspaceAgentModel
7-
import com.coder.gateway.sdk.CoderCLIManager
7+
import com.coder.gateway.cli.CoderCLIManager
88
import com.coder.gateway.sdk.CoderRestClient
99
import com.coder.gateway.sdk.DefaultCoderRestClient
10-
import com.coder.gateway.sdk.ensureCLI
10+
import com.coder.gateway.cli.ensureCLI
1111
import com.coder.gateway.sdk.ex.AuthenticationResponseException
1212
import com.coder.gateway.util.toURL
1313
import com.coder.gateway.sdk.v2.models.Workspace
@@ -38,7 +38,7 @@ private const val IDE_PATH_ON_HOST = "ide_path_on_host"
3838
// CoderGatewayConnectionProvider handles connecting via a Gateway link such as
3939
// jetbrains-gateway://connect#type=coder.
4040
class CoderGatewayConnectionProvider : GatewayConnectionProvider {
41-
private val settings: CoderSettingsService = service()
41+
private val settings: CoderSettingsService = service<CoderSettingsService>()
4242

4343
override suspend fun connect(parameters: Map<String, String>, requestor: ConnectionRequestor): GatewayConnectionHandle? {
4444
CoderRemoteConnectionHandle().connect{ indicator ->

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.coder.gateway.util.suspendingRetryWithExponentialBackOff
1010
import com.coder.gateway.util.toURL
1111
import com.coder.gateway.util.withPath
1212
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
13-
import com.coder.gateway.services.CoderSettings
13+
import com.coder.gateway.settings.CoderSettings
1414
import com.intellij.ide.BrowserUtil
1515
import com.intellij.openapi.application.ApplicationManager
1616
import com.intellij.openapi.application.ModalityState

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import java.nio.file.Path
2020
class CoderSettingsConfigurable : BoundConfigurable("Coder") {
2121
override fun createPanel(): DialogPanel {
2222
val state: CoderSettingsState = service()
23-
val settings: CoderSettingsService = service()
23+
val settings: CoderSettingsService = service<CoderSettingsService>()
2424
return panel {
2525
row(CoderGatewayBundle.message("gateway.connector.settings.data-directory.title")) {
2626
textField().resizableColumn().align(AlignX.FILL)

src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt renamed to src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package com.coder.gateway.sdk
1+
package com.coder.gateway.cli
22

3-
import com.coder.gateway.services.CoderSettings
3+
import com.coder.gateway.settings.CoderSettings
44
import com.coder.gateway.services.CoderSettingsState
55
import com.coder.gateway.util.CoderHostnameVerifier
66
import com.coder.gateway.util.InvalidVersionException
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package com.coder.gateway.sdk
2+
3+
import com.coder.gateway.models.WorkspaceAgentModel
4+
import com.coder.gateway.sdk.convertors.InstantConverter
5+
import com.coder.gateway.sdk.ex.AuthenticationResponseException
6+
import com.coder.gateway.sdk.ex.TemplateResponseException
7+
import com.coder.gateway.sdk.ex.WorkspaceResponseException
8+
import com.coder.gateway.sdk.v2.CoderV2RestFacade
9+
import com.coder.gateway.sdk.v2.models.BuildInfo
10+
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
11+
import com.coder.gateway.sdk.v2.models.Template
12+
import com.coder.gateway.sdk.v2.models.User
13+
import com.coder.gateway.sdk.v2.models.Workspace
14+
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
15+
import com.coder.gateway.sdk.v2.models.WorkspaceResource
16+
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
17+
import com.coder.gateway.sdk.v2.models.toAgentModels
18+
import com.coder.gateway.services.CoderSettingsState
19+
import com.coder.gateway.settings.CoderSettings
20+
import com.coder.gateway.util.CoderHostnameVerifier
21+
import com.coder.gateway.util.coderSocketFactory
22+
import com.coder.gateway.util.coderTrustManagers
23+
import com.coder.gateway.util.getHeaders
24+
import com.google.gson.Gson
25+
import com.google.gson.GsonBuilder
26+
import com.intellij.openapi.util.SystemInfo
27+
import okhttp3.Credentials
28+
import okhttp3.OkHttpClient
29+
import okhttp3.logging.HttpLoggingInterceptor
30+
import retrofit2.Retrofit
31+
import retrofit2.converter.gson.GsonConverterFactory
32+
import java.net.HttpURLConnection
33+
import java.net.URL
34+
import java.time.Instant
35+
import java.util.*
36+
import javax.net.ssl.X509TrustManager
37+
38+
/**
39+
* In non-test code use DefaultCoderRestClient instead.
40+
*/
41+
open class CoderRestClient(
42+
var url: URL, var token: String,
43+
private val settings: CoderSettings = CoderSettings(CoderSettingsState()),
44+
private val proxyValues: ProxyValues? = null,
45+
private val pluginVersion: String = "development",
46+
) {
47+
private val httpClient: OkHttpClient
48+
private val retroRestClient: CoderV2RestFacade
49+
50+
init {
51+
val gson: Gson = GsonBuilder().registerTypeAdapter(Instant::class.java, InstantConverter()).setPrettyPrinting().create()
52+
53+
val socketFactory = coderSocketFactory(settings.tls)
54+
val trustManagers = coderTrustManagers(settings.tls.caPath)
55+
var builder = OkHttpClient.Builder()
56+
57+
if (proxyValues != null) {
58+
builder = builder
59+
.proxySelector(proxyValues.selector)
60+
.proxyAuthenticator { _, response ->
61+
if (proxyValues.useAuth && proxyValues.username != null && proxyValues.password != null) {
62+
val credentials = Credentials.basic(proxyValues.username, proxyValues.password)
63+
response.request.newBuilder()
64+
.header("Proxy-Authorization", credentials)
65+
.build()
66+
} else null
67+
}
68+
}
69+
70+
httpClient = builder
71+
.sslSocketFactory(socketFactory, trustManagers[0] as X509TrustManager)
72+
.hostnameVerifier(CoderHostnameVerifier(settings.tls.altHostname))
73+
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("Coder-Session-Token", token).build()) }
74+
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("User-Agent", "Coder Gateway/${pluginVersion} (${SystemInfo.getOsNameAndVersion()}; ${SystemInfo.OS_ARCH})").build()) }
75+
.addInterceptor {
76+
var request = it.request()
77+
val headers = getHeaders(url, settings.headerCommand)
78+
if (headers.isNotEmpty()) {
79+
val reqBuilder = request.newBuilder()
80+
headers.forEach { h -> reqBuilder.addHeader(h.key, h.value) }
81+
request = reqBuilder.build()
82+
}
83+
it.proceed(request)
84+
}
85+
// This should always be last if we want to see previous interceptors logged.
86+
.addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BASIC) })
87+
.build()
88+
89+
retroRestClient = Retrofit.Builder().baseUrl(url.toString()).client(httpClient)
90+
.addConverterFactory(GsonConverterFactory.create(gson))
91+
.build().create(CoderV2RestFacade::class.java)
92+
}
93+
94+
/**
95+
* Retrieve the current user.
96+
* @throws [AuthenticationResponseException] if authentication failed.
97+
*/
98+
fun me(): User {
99+
val userResponse = retroRestClient.me().execute()
100+
if (!userResponse.isSuccessful) {
101+
throw AuthenticationResponseException(
102+
"Unable to authenticate to $url: code ${userResponse.code()}, ${
103+
userResponse.message().ifBlank { "has your token expired?" }
104+
}"
105+
)
106+
}
107+
108+
return userResponse.body()!!
109+
}
110+
111+
/**
112+
* Retrieves the available workspaces created by the user.
113+
* @throws WorkspaceResponseException if workspaces could not be retrieved.
114+
*/
115+
fun workspaces(): List<Workspace> {
116+
val workspacesResponse = retroRestClient.workspaces("owner:me").execute()
117+
if (!workspacesResponse.isSuccessful) {
118+
throw WorkspaceResponseException(
119+
"Unable to retrieve workspaces from $url: code ${workspacesResponse.code()}, reason: ${
120+
workspacesResponse.message().ifBlank { "no reason provided" }
121+
}"
122+
)
123+
}
124+
125+
return workspacesResponse.body()!!.workspaces
126+
}
127+
128+
/**
129+
* Retrieves agents for the specified workspaces, including those that are
130+
* off.
131+
*/
132+
fun agents(workspaces: List<Workspace>): List<WorkspaceAgentModel> {
133+
return workspaces.flatMap {
134+
val resources = resources(it)
135+
it.toAgentModels(resources)
136+
}
137+
}
138+
139+
/**
140+
* Retrieves resources for the specified workspace. The workspaces response
141+
* does not include agents when the workspace is off so this can be used to
142+
* get them instead, just like `coder config-ssh` does (otherwise we risk
143+
* removing hosts from the SSH config when they are off).
144+
*/
145+
fun resources(workspace: Workspace): List<WorkspaceResource> {
146+
val resourcesResponse = retroRestClient.templateVersionResources(workspace.latestBuild.templateVersionID).execute()
147+
if (!resourcesResponse.isSuccessful) {
148+
throw WorkspaceResponseException(
149+
"Unable to retrieve template resources for ${workspace.name} from $url: code ${resourcesResponse.code()}, reason: ${
150+
resourcesResponse.message().ifBlank { "no reason provided" }
151+
}"
152+
)
153+
}
154+
return resourcesResponse.body()!!
155+
}
156+
157+
fun buildInfo(): BuildInfo {
158+
val buildInfoResponse = retroRestClient.buildInfo().execute()
159+
if (!buildInfoResponse.isSuccessful) {
160+
throw java.lang.IllegalStateException("Unable to retrieve build information for $url, code: ${buildInfoResponse.code()}, reason: ${buildInfoResponse.message().ifBlank { "no reason provided" }}")
161+
}
162+
return buildInfoResponse.body()!!
163+
}
164+
165+
private fun template(templateID: UUID): Template {
166+
val templateResponse = retroRestClient.template(templateID).execute()
167+
if (!templateResponse.isSuccessful) {
168+
throw TemplateResponseException(
169+
"Unable to retrieve template with ID $templateID from $url, code: ${templateResponse.code()}, reason: ${
170+
templateResponse.message().ifBlank { "no reason provided" }
171+
}"
172+
)
173+
}
174+
return templateResponse.body()!!
175+
}
176+
177+
fun startWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
178+
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.START, null, null, null, null)
179+
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
180+
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
181+
throw WorkspaceResponseException(
182+
"Unable to build workspace $workspaceName on $url, code: ${buildResponse.code()}, reason: ${
183+
buildResponse.message().ifBlank { "no reason provided" }
184+
}"
185+
)
186+
}
187+
188+
return buildResponse.body()!!
189+
}
190+
191+
fun stopWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
192+
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.STOP, null, null, null, null)
193+
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
194+
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
195+
throw WorkspaceResponseException(
196+
"Unable to stop workspace $workspaceName on $url, code: ${buildResponse.code()}, reason: ${
197+
buildResponse.message().ifBlank { "no reason provided" }
198+
}"
199+
)
200+
}
201+
202+
return buildResponse.body()!!
203+
}
204+
205+
fun updateWorkspace(workspaceID: UUID, workspaceName: String, lastWorkspaceTransition: WorkspaceTransition, templateID: UUID): WorkspaceBuild {
206+
val template = template(templateID)
207+
208+
val buildRequest =
209+
CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null, null)
210+
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
211+
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
212+
throw WorkspaceResponseException(
213+
"Unable to update workspace $workspaceName on $url, code: ${buildResponse.code()}, reason: ${
214+
buildResponse.message().ifBlank { "no reason provided" }
215+
}"
216+
)
217+
}
218+
219+
return buildResponse.body()!!
220+
}
221+
}

0 commit comments

Comments
 (0)