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