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