Skip to content

Commit bc28d89

Browse files
authored
Merge pull request #93 from coder/upgrade-coder-client-auth-api
Upgrade coder client authentication API - resolves Unable to authenticate #86 - authentication to REST API is no longer using cookies. - upgraded REST models - upgraded Gradle plugin and dependencies
2 parents 43ce79c + a8ccb01 commit bc28d89

22 files changed

+312
-86
lines changed

CHANGELOG.md

+14-4
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@
33
# coder-gateway Changelog
44

55
## [Unreleased]
6+
### Added
7+
8+
- upgraded support for the latest Coder REST API
9+
10+
### Fixed
11+
12+
- authentication flow is now done using HTTP headers
613

714
## [2.1.1]
8-
### Added
9-
- support for remembering last opened Coder session
1015

11-
### Changed
12-
- minimum supported Gateway build is now 222.3739.54
16+
### Added
17+
18+
- support for remembering last opened Coder session
19+
20+
### Changed
21+
22+
- minimum supported Gateway build is now 222.3739.54
1323
- some dialog titles
1424

1525
## [2.1.0]

build.gradle.kts

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins {
99
// Kotlin support
1010
id("org.jetbrains.kotlin.jvm") version "1.7.21"
1111
// Gradle IntelliJ Plugin
12-
id("org.jetbrains.intellij") version "1.9.0"
12+
id("org.jetbrains.intellij") version "1.10.0"
1313
// Gradle Changelog Plugin
1414
id("org.jetbrains.changelog") version "2.0.0"
1515
// Gradle Qodana Plugin
@@ -23,10 +23,9 @@ val ktorVersion = properties("ktorVersion")
2323
dependencies {
2424
implementation("com.squareup.retrofit2:retrofit:2.9.0")
2525
// define a BOM and its version
26-
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
26+
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0"))
2727
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
2828
implementation("com.squareup.okhttp3:okhttp")
29-
implementation("com.squareup.okhttp3:okhttp-urlconnection")
3029
implementation("com.squareup.okhttp3:logging-interceptor")
3130

3231
implementation("org.zeroturnaround:zt-exec:1.12") {

src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.coder.gateway.models
22

33
import com.coder.gateway.sdk.Arch
44
import com.coder.gateway.sdk.OS
5+
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
56
import java.util.UUID
67

78
data class WorkspaceAgentModel(
@@ -12,7 +13,7 @@ data class WorkspaceAgentModel(
1213
val templateName: String,
1314
val status: WorkspaceVersionStatus,
1415
val agentStatus: WorkspaceAgentStatus,
15-
val lastBuildTransition: String,
16+
val lastBuildTransition: WorkspaceTransition,
1617
val agentOS: OS?,
1718
val agentArch: Arch?,
1819
val homeDirectory: String?

src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt

+9-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.coder.gateway.models
22

33
import com.coder.gateway.sdk.v2.models.ProvisionerJobStatus
44
import com.coder.gateway.sdk.v2.models.Workspace
5-
import com.coder.gateway.sdk.v2.models.WorkspaceBuildTransition
5+
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
66

77
enum class WorkspaceAgentStatus(val label: String) {
88
QUEUED("◍ Queued"), STARTING("⦿ Starting"), STOPPING("◍ Stopping"), DELETING("⦸ Deleting"),
@@ -12,16 +12,16 @@ enum class WorkspaceAgentStatus(val label: String) {
1212
companion object {
1313
fun from(workspace: Workspace) = when (workspace.latestBuild.job.status) {
1414
ProvisionerJobStatus.PENDING -> QUEUED
15-
ProvisionerJobStatus.RUNNING -> when (workspace.latestBuild.workspaceTransition) {
16-
WorkspaceBuildTransition.START -> STARTING
17-
WorkspaceBuildTransition.STOP -> STOPPING
18-
WorkspaceBuildTransition.DELETE -> DELETING
15+
ProvisionerJobStatus.RUNNING -> when (workspace.latestBuild.transition) {
16+
WorkspaceTransition.START -> STARTING
17+
WorkspaceTransition.STOP -> STOPPING
18+
WorkspaceTransition.DELETE -> DELETING
1919
}
2020

21-
ProvisionerJobStatus.SUCCEEDED -> when (workspace.latestBuild.workspaceTransition) {
22-
WorkspaceBuildTransition.START -> RUNNING
23-
WorkspaceBuildTransition.STOP -> STOPPED
24-
WorkspaceBuildTransition.DELETE -> DELETED
21+
ProvisionerJobStatus.SUCCEEDED -> when (workspace.latestBuild.transition) {
22+
WorkspaceTransition.START -> RUNNING
23+
WorkspaceTransition.STOP -> STOPPED
24+
WorkspaceTransition.DELETE -> DELETED
2525
}
2626

2727
ProvisionerJobStatus.CANCELING -> CANCELING

src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt

+8-20
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@ import com.coder.gateway.sdk.v2.models.User
1313
import com.coder.gateway.sdk.v2.models.Workspace
1414
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
1515
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
16+
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
1617
import com.google.gson.Gson
1718
import com.google.gson.GsonBuilder
1819
import com.intellij.openapi.components.Service
19-
import okhttp3.Cookie
20-
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
21-
import okhttp3.JavaNetCookieJar
2220
import okhttp3.OkHttpClient
2321
import okhttp3.logging.HttpLoggingInterceptor
2422
import retrofit2.Retrofit
2523
import retrofit2.converter.gson.GsonConverterFactory
26-
import java.net.CookieManager
2724
import java.net.HttpURLConnection.HTTP_CREATED
2825
import java.net.URL
2926
import java.time.Instant
@@ -42,26 +39,17 @@ class CoderRestClientService {
4239
* @throws [AuthenticationResponseException] if authentication failed.
4340
*/
4441
fun initClientSession(url: URL, token: String): User {
45-
val cookieUrl = url.toHttpUrlOrNull()!!
46-
val cookieJar = JavaNetCookieJar(CookieManager()).apply {
47-
saveFromResponse(
48-
cookieUrl,
49-
listOf(Cookie.parse(cookieUrl, "session_token=$token")!!)
50-
)
51-
}
5242
val gson: Gson = GsonBuilder()
5343
.registerTypeAdapter(Instant::class.java, InstantConverter())
5444
.setPrettyPrinting()
5545
.create()
5646

57-
val interceptor = HttpLoggingInterceptor()
58-
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC)
5947
retroRestClient = Retrofit.Builder()
6048
.baseUrl(url.toString())
6149
.client(
6250
OkHttpClient.Builder()
63-
.addInterceptor(interceptor)
64-
.cookieJar(cookieJar)
51+
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("Coder-Session-Token", token).build()) }
52+
.addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BASIC) })
6553
.build()
6654
)
6755
.addConverterFactory(GsonConverterFactory.create(gson))
@@ -91,7 +79,7 @@ class CoderRestClientService {
9179
throw WorkspaceResponseException("Could not retrieve Coder Workspaces:${workspacesResponse.code()}, reason: ${workspacesResponse.message()}")
9280
}
9381

94-
return workspacesResponse.body()!!
82+
return workspacesResponse.body()!!.workspaces
9583
}
9684

9785
private fun buildInfo(): BuildInfo {
@@ -126,7 +114,7 @@ class CoderRestClientService {
126114
}
127115

128116
fun startWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
129-
val buildRequest = CreateWorkspaceBuildRequest(null, "start", null, null, null)
117+
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.START, null, null, null, null)
130118
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
131119
if (buildResponse.code() != HTTP_CREATED) {
132120
throw WorkspaceResponseException("Failed to build workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
@@ -136,7 +124,7 @@ class CoderRestClientService {
136124
}
137125

138126
fun stopWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
139-
val buildRequest = CreateWorkspaceBuildRequest(null, "stop", null, null, null)
127+
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.STOP, null, null, null, null)
140128
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
141129
if (buildResponse.code() != HTTP_CREATED) {
142130
throw WorkspaceResponseException("Failed to stop workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
@@ -145,10 +133,10 @@ class CoderRestClientService {
145133
return buildResponse.body()!!
146134
}
147135

148-
fun updateWorkspace(workspaceID: UUID, workspaceName: String, lastWorkspaceTransition: String, templateID: UUID): WorkspaceBuild {
136+
fun updateWorkspace(workspaceID: UUID, workspaceName: String, lastWorkspaceTransition: WorkspaceTransition, templateID: UUID): WorkspaceBuild {
149137
val template = template(templateID)
150138

151-
val buildRequest = CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null)
139+
val buildRequest = CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null, null)
152140
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
153141
if (buildResponse.code() != HTTP_CREATED) {
154142
throw WorkspaceResponseException("Failed to update workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")

src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import com.coder.gateway.sdk.v2.models.BuildInfo
44
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
55
import com.coder.gateway.sdk.v2.models.Template
66
import com.coder.gateway.sdk.v2.models.User
7-
import com.coder.gateway.sdk.v2.models.Workspace
87
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
98
import com.coder.gateway.sdk.v2.models.WorkspaceResource
9+
import com.coder.gateway.sdk.v2.models.WorkspacesResponse
1010
import retrofit2.Call
1111
import retrofit2.http.Body
1212
import retrofit2.http.GET
@@ -27,7 +27,7 @@ interface CoderV2RestFacade {
2727
* Retrieves all workspaces the authenticated user has access to.
2828
*/
2929
@GET("api/v2/workspaces")
30-
fun workspaces(@Query("q") searchParams: String): Call<List<Workspace>>
30+
fun workspaces(@Query("q") searchParams: String): Call<WorkspacesResponse>
3131

3232
@GET("api/v2/buildinfo")
3333
fun buildInfo(): Call<BuildInfo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.coder.gateway.sdk.v2.models
2+
3+
import com.google.gson.annotations.SerializedName
4+
5+
enum class BuildReason {
6+
// "initiator" is used when a workspace build is triggered by a user.
7+
// Combined with the initiator id/username, it indicates which user initiated the build.
8+
@SerializedName("initiator")
9+
INITIATOR,
10+
11+
// "autostart" is used when a build to start a workspace is triggered by Autostart.
12+
// The initiator id/username in this case is the workspace owner and can be ignored.
13+
@SerializedName("autostart")
14+
AUTOSTART,
15+
16+
// "autostop" is used when a build to stop a workspace is triggered by Autostop.
17+
// The initiator id/username in this case is the workspace owner and can be ignored.
18+
@SerializedName("autostop")
19+
AUTOSTOP
20+
}

src/main/kotlin/com/coder/gateway/sdk/v2/models/CreateParameterRequest.kt

+22-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ data class CreateParameterRequest(
77
@SerializedName("copy_from_parameter") val cloneID: UUID?,
88
@SerializedName("name") val name: String,
99
@SerializedName("source_value") val sourceValue: String,
10-
@SerializedName("source_scheme") val sourceScheme: String,
11-
@SerializedName("destination_scheme") val destinationScheme: String
12-
)
10+
@SerializedName("source_scheme") val sourceScheme: ParameterSourceScheme,
11+
@SerializedName("destination_scheme") val destinationScheme: ParameterDestinationScheme
12+
)
13+
14+
enum class ParameterSourceScheme {
15+
@SerializedName("none")
16+
NONE,
17+
18+
@SerializedName("data")
19+
DATA
20+
}
21+
22+
enum class ParameterDestinationScheme {
23+
@SerializedName("none")
24+
NONE,
25+
26+
@SerializedName("environment_variable")
27+
ENVIRONMENT_VARIABLE,
28+
29+
@SerializedName("provisioner_variable")
30+
PROVISIONER_VARIABLE
31+
}

src/main/kotlin/com/coder/gateway/sdk/v2/models/CreateWorkspaceBuildRequest.kt

+11-9
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import java.util.UUID
55

66
data class CreateWorkspaceBuildRequest(
77
@SerializedName("template_version_id") val templateVersionID: UUID?,
8-
@SerializedName("transition") val transition: String,
8+
@SerializedName("transition") val transition: WorkspaceTransition,
99
@SerializedName("dry_run") val dryRun: Boolean?,
10-
@SerializedName("state") val state: Array<Byte>?,
10+
@SerializedName("state") val provisionerState: Array<Byte>?,
11+
// Orphan may be set for the Destroy transition.
12+
@SerializedName("orphan") val orphan: Boolean?,
1113
@SerializedName("parameter_values") val parameterValues: Array<CreateParameterRequest>?
1214
) {
13-
1415
override fun equals(other: Any?): Boolean {
1516
if (this === other) return true
1617
if (javaClass != other?.javaClass) return false
@@ -20,10 +21,11 @@ data class CreateWorkspaceBuildRequest(
2021
if (templateVersionID != other.templateVersionID) return false
2122
if (transition != other.transition) return false
2223
if (dryRun != other.dryRun) return false
23-
if (state != null) {
24-
if (other.state == null) return false
25-
if (!state.contentEquals(other.state)) return false
26-
} else if (other.state != null) return false
24+
if (provisionerState != null) {
25+
if (other.provisionerState == null) return false
26+
if (!provisionerState.contentEquals(other.provisionerState)) return false
27+
} else if (other.provisionerState != null) return false
28+
if (orphan != other.orphan) return false
2729
if (parameterValues != null) {
2830
if (other.parameterValues == null) return false
2931
if (!parameterValues.contentEquals(other.parameterValues)) return false
@@ -36,9 +38,9 @@ data class CreateWorkspaceBuildRequest(
3638
var result = templateVersionID?.hashCode() ?: 0
3739
result = 31 * result + transition.hashCode()
3840
result = 31 * result + (dryRun?.hashCode() ?: 0)
39-
result = 31 * result + (state?.contentHashCode() ?: 0)
41+
result = 31 * result + (provisionerState?.contentHashCode() ?: 0)
42+
result = 31 * result + (orphan?.hashCode() ?: 0)
4043
result = 31 * result + (parameterValues?.contentHashCode() ?: 0)
4144
return result
4245
}
4346
}
44-

src/main/kotlin/com/coder/gateway/sdk/v2/models/ProvisionerJob.kt

+7-4
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import java.util.UUID
77
data class ProvisionerJob(
88
@SerializedName("id") val id: UUID,
99
@SerializedName("created_at") val createdAt: Instant,
10-
@SerializedName("started_at") val startedAt: Instant,
11-
@SerializedName("completed_at") val completedAt: Instant,
12-
@SerializedName("error") val error: String,
10+
@SerializedName("started_at") val startedAt: Instant?,
11+
@SerializedName("completed_at") val completedAt: Instant?,
12+
@SerializedName("canceled_at") val canceledAt: Instant?,
13+
@SerializedName("error") val error: String?,
1314
@SerializedName("status") val status: ProvisionerJobStatus,
14-
@SerializedName("worker_id") val workerID: UUID,
15+
@SerializedName("worker_id") val workerID: UUID?,
16+
@SerializedName("file_id") val fileID: UUID,
17+
@SerializedName("tags") val tags: Map<String, String>,
1518
)
1619

1720
enum class ProvisionerJobStatus {

src/main/kotlin/com/coder/gateway/sdk/v2/models/Role.kt

+31-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,34 @@ package com.coder.gateway.sdk.v2.models
22

33
import com.google.gson.annotations.SerializedName
44

5-
data class Role(@SerializedName("name") val name: String, @SerializedName("display_name") val displayName: String)
5+
data class Role(
6+
@SerializedName("name") val name: String,
7+
@SerializedName("display_name") val displayName: String,
8+
@SerializedName("site") val site: Permission,
9+
// Org is a map of orgid to permissions. We represent orgid as a string.
10+
// We scope the organizations in the role so we can easily combine all the
11+
// roles.
12+
@SerializedName("org") val org: Map<String, List<Permission>>,
13+
@SerializedName("user") val user: List<Permission>,
14+
15+
)
16+
17+
data class Permission(
18+
@SerializedName("negate") val negate: Boolean,
19+
@SerializedName("resource_type") val resourceType: String,
20+
@SerializedName("action") val action: Action,
21+
)
22+
23+
enum class Action {
24+
@SerializedName("create")
25+
CREATE,
26+
27+
@SerializedName("read")
28+
READ,
29+
30+
@SerializedName("update")
31+
UPDATE,
32+
33+
@SerializedName("delete")
34+
DELETE
35+
}

src/main/kotlin/com/coder/gateway/sdk/v2/models/Template.kt

+17-3
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,26 @@ data class Template(
1010
@SerializedName("updated_at") val updatedAt: Instant,
1111
@SerializedName("organization_id") val organizationIterator: UUID,
1212
@SerializedName("name") val name: String,
13-
@SerializedName("provisioner") val provisioner: String,
13+
@SerializedName("display_name") val displayName: String,
14+
@SerializedName("provisioner") val provisioner: ProvisionerType,
1415
@SerializedName("active_version_id") val activeVersionID: UUID,
1516
@SerializedName("workspace_owner_count") val workspaceOwnerCount: Int,
17+
@SerializedName("active_user_count") val activeUserCount: Int,
18+
@SerializedName("build_time_stats") val buildTimeStats: Map<WorkspaceTransition, TransitionStats>,
1619
@SerializedName("description") val description: String,
17-
@SerializedName("max_ttl_ms") val maxTTLMillis: Long,
18-
@SerializedName("min_autostart_interval_ms") val minAutostartIntervalMillis: Long,
20+
@SerializedName("icon") val icon: String,
21+
@SerializedName("default_ttl_ms") val defaultTTLMillis: Long,
1922
@SerializedName("created_by_id") val createdByID: UUID,
2023
@SerializedName("created_by_name") val createdByName: String,
24+
@SerializedName("allow_user_cancel_workspace_jobs") val allowUserCancelWorkspaceJobs: Boolean,
2125
)
26+
27+
enum class ProvisionerType {
28+
@SerializedName("echo")
29+
ECHO,
30+
31+
@SerializedName("terraform")
32+
TERRAFORM
33+
}
34+
35+
data class TransitionStats(val p50: Long, val p95: Long)

0 commit comments

Comments
 (0)