Skip to content

Upgrade coder client authentication API #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@
# coder-gateway Changelog

## [Unreleased]
### Added

- upgraded support for the latest Coder REST API

### Fixed

- authentication flow is now done using HTTP headers

## [2.1.1]
### Added
- support for remembering last opened Coder session

### Changed
- minimum supported Gateway build is now 222.3739.54
### Added

- support for remembering last opened Coder session

### Changed

- minimum supported Gateway build is now 222.3739.54
- some dialog titles

## [2.1.0]
Expand Down
5 changes: 2 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {
// Kotlin support
id("org.jetbrains.kotlin.jvm") version "1.7.21"
// Gradle IntelliJ Plugin
id("org.jetbrains.intellij") version "1.9.0"
id("org.jetbrains.intellij") version "1.10.0"
// Gradle Changelog Plugin
id("org.jetbrains.changelog") version "2.0.0"
// Gradle Qodana Plugin
Expand All @@ -23,10 +23,9 @@ val ktorVersion = properties("ktorVersion")
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// define a BOM and its version
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0"))
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp")
implementation("com.squareup.okhttp3:okhttp-urlconnection")
implementation("com.squareup.okhttp3:logging-interceptor")

implementation("org.zeroturnaround:zt-exec:1.12") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.coder.gateway.models

import com.coder.gateway.sdk.Arch
import com.coder.gateway.sdk.OS
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
import java.util.UUID

data class WorkspaceAgentModel(
Expand All @@ -12,7 +13,7 @@ data class WorkspaceAgentModel(
val templateName: String,
val status: WorkspaceVersionStatus,
val agentStatus: WorkspaceAgentStatus,
val lastBuildTransition: String,
val lastBuildTransition: WorkspaceTransition,
val agentOS: OS?,
val agentArch: Arch?,
val homeDirectory: String?
Expand Down
18 changes: 9 additions & 9 deletions src/main/kotlin/com/coder/gateway/models/WorkspaceAgentStatus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.coder.gateway.models

import com.coder.gateway.sdk.v2.models.ProvisionerJobStatus
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceBuildTransition
import com.coder.gateway.sdk.v2.models.WorkspaceTransition

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

ProvisionerJobStatus.SUCCEEDED -> when (workspace.latestBuild.workspaceTransition) {
WorkspaceBuildTransition.START -> RUNNING
WorkspaceBuildTransition.STOP -> STOPPED
WorkspaceBuildTransition.DELETE -> DELETED
ProvisionerJobStatus.SUCCEEDED -> when (workspace.latestBuild.transition) {
WorkspaceTransition.START -> RUNNING
WorkspaceTransition.STOP -> STOPPED
WorkspaceTransition.DELETE -> DELETED
}

ProvisionerJobStatus.CANCELING -> CANCELING
Expand Down
28 changes: 8 additions & 20 deletions src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,14 @@ import com.coder.gateway.sdk.v2.models.User
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.intellij.openapi.components.Service
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.CookieManager
import java.net.HttpURLConnection.HTTP_CREATED
import java.net.URL
import java.time.Instant
Expand All @@ -42,26 +39,17 @@ class CoderRestClientService {
* @throws [AuthenticationResponseException] if authentication failed.
*/
fun initClientSession(url: URL, token: String): User {
val cookieUrl = url.toHttpUrlOrNull()!!
val cookieJar = JavaNetCookieJar(CookieManager()).apply {
saveFromResponse(
cookieUrl,
listOf(Cookie.parse(cookieUrl, "session_token=$token")!!)
)
}
val gson: Gson = GsonBuilder()
.registerTypeAdapter(Instant::class.java, InstantConverter())
.setPrettyPrinting()
.create()

val interceptor = HttpLoggingInterceptor()
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC)
retroRestClient = Retrofit.Builder()
.baseUrl(url.toString())
.client(
OkHttpClient.Builder()
.addInterceptor(interceptor)
.cookieJar(cookieJar)
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("Coder-Session-Token", token).build()) }
.addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BASIC) })
.build()
)
.addConverterFactory(GsonConverterFactory.create(gson))
Expand Down Expand Up @@ -91,7 +79,7 @@ class CoderRestClientService {
throw WorkspaceResponseException("Could not retrieve Coder Workspaces:${workspacesResponse.code()}, reason: ${workspacesResponse.message()}")
}

return workspacesResponse.body()!!
return workspacesResponse.body()!!.workspaces
}

private fun buildInfo(): BuildInfo {
Expand Down Expand Up @@ -126,7 +114,7 @@ class CoderRestClientService {
}

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

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

fun updateWorkspace(workspaceID: UUID, workspaceName: String, lastWorkspaceTransition: String, templateID: UUID): WorkspaceBuild {
fun updateWorkspace(workspaceID: UUID, workspaceName: String, lastWorkspaceTransition: WorkspaceTransition, templateID: UUID): WorkspaceBuild {
val template = template(templateID)

val buildRequest = CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null)
val buildRequest = CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null, null)
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
if (buildResponse.code() != HTTP_CREATED) {
throw WorkspaceResponseException("Failed to update workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import com.coder.gateway.sdk.v2.models.BuildInfo
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
import com.coder.gateway.sdk.v2.models.Template
import com.coder.gateway.sdk.v2.models.User
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
import com.coder.gateway.sdk.v2.models.WorkspaceResource
import com.coder.gateway.sdk.v2.models.WorkspacesResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
Expand All @@ -27,7 +27,7 @@ interface CoderV2RestFacade {
* Retrieves all workspaces the authenticated user has access to.
*/
@GET("api/v2/workspaces")
fun workspaces(@Query("q") searchParams: String): Call<List<Workspace>>
fun workspaces(@Query("q") searchParams: String): Call<WorkspacesResponse>

@GET("api/v2/buildinfo")
fun buildInfo(): Call<BuildInfo>
Expand Down
20 changes: 20 additions & 0 deletions src/main/kotlin/com/coder/gateway/sdk/v2/models/BuildReason.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.coder.gateway.sdk.v2.models

import com.google.gson.annotations.SerializedName

enum class BuildReason {
// "initiator" is used when a workspace build is triggered by a user.
// Combined with the initiator id/username, it indicates which user initiated the build.
@SerializedName("initiator")
INITIATOR,

// "autostart" is used when a build to start a workspace is triggered by Autostart.
// The initiator id/username in this case is the workspace owner and can be ignored.
@SerializedName("autostart")
AUTOSTART,

// "autostop" is used when a build to stop a workspace is triggered by Autostop.
// The initiator id/username in this case is the workspace owner and can be ignored.
@SerializedName("autostop")
AUTOSTOP
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ data class CreateParameterRequest(
@SerializedName("copy_from_parameter") val cloneID: UUID?,
@SerializedName("name") val name: String,
@SerializedName("source_value") val sourceValue: String,
@SerializedName("source_scheme") val sourceScheme: String,
@SerializedName("destination_scheme") val destinationScheme: String
)
@SerializedName("source_scheme") val sourceScheme: ParameterSourceScheme,
@SerializedName("destination_scheme") val destinationScheme: ParameterDestinationScheme
)

enum class ParameterSourceScheme {
@SerializedName("none")
NONE,

@SerializedName("data")
DATA
}

enum class ParameterDestinationScheme {
@SerializedName("none")
NONE,

@SerializedName("environment_variable")
ENVIRONMENT_VARIABLE,

@SerializedName("provisioner_variable")
PROVISIONER_VARIABLE
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import java.util.UUID

data class CreateWorkspaceBuildRequest(
@SerializedName("template_version_id") val templateVersionID: UUID?,
@SerializedName("transition") val transition: String,
@SerializedName("transition") val transition: WorkspaceTransition,
@SerializedName("dry_run") val dryRun: Boolean?,
@SerializedName("state") val state: Array<Byte>?,
@SerializedName("state") val provisionerState: Array<Byte>?,
// Orphan may be set for the Destroy transition.
@SerializedName("orphan") val orphan: Boolean?,
@SerializedName("parameter_values") val parameterValues: Array<CreateParameterRequest>?
) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
Expand All @@ -20,10 +21,11 @@ data class CreateWorkspaceBuildRequest(
if (templateVersionID != other.templateVersionID) return false
if (transition != other.transition) return false
if (dryRun != other.dryRun) return false
if (state != null) {
if (other.state == null) return false
if (!state.contentEquals(other.state)) return false
} else if (other.state != null) return false
if (provisionerState != null) {
if (other.provisionerState == null) return false
if (!provisionerState.contentEquals(other.provisionerState)) return false
} else if (other.provisionerState != null) return false
if (orphan != other.orphan) return false
if (parameterValues != null) {
if (other.parameterValues == null) return false
if (!parameterValues.contentEquals(other.parameterValues)) return false
Expand All @@ -36,9 +38,9 @@ data class CreateWorkspaceBuildRequest(
var result = templateVersionID?.hashCode() ?: 0
result = 31 * result + transition.hashCode()
result = 31 * result + (dryRun?.hashCode() ?: 0)
result = 31 * result + (state?.contentHashCode() ?: 0)
result = 31 * result + (provisionerState?.contentHashCode() ?: 0)
result = 31 * result + (orphan?.hashCode() ?: 0)
result = 31 * result + (parameterValues?.contentHashCode() ?: 0)
return result
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import java.util.UUID
data class ProvisionerJob(
@SerializedName("id") val id: UUID,
@SerializedName("created_at") val createdAt: Instant,
@SerializedName("started_at") val startedAt: Instant,
@SerializedName("completed_at") val completedAt: Instant,
@SerializedName("error") val error: String,
@SerializedName("started_at") val startedAt: Instant?,
@SerializedName("completed_at") val completedAt: Instant?,
@SerializedName("canceled_at") val canceledAt: Instant?,
@SerializedName("error") val error: String?,
@SerializedName("status") val status: ProvisionerJobStatus,
@SerializedName("worker_id") val workerID: UUID,
@SerializedName("worker_id") val workerID: UUID?,
@SerializedName("file_id") val fileID: UUID,
@SerializedName("tags") val tags: Map<String, String>,
)

enum class ProvisionerJobStatus {
Expand Down
32 changes: 31 additions & 1 deletion src/main/kotlin/com/coder/gateway/sdk/v2/models/Role.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,34 @@ package com.coder.gateway.sdk.v2.models

import com.google.gson.annotations.SerializedName

data class Role(@SerializedName("name") val name: String, @SerializedName("display_name") val displayName: String)
data class Role(
@SerializedName("name") val name: String,
@SerializedName("display_name") val displayName: String,
@SerializedName("site") val site: Permission,
// Org is a map of orgid to permissions. We represent orgid as a string.
// We scope the organizations in the role so we can easily combine all the
// roles.
@SerializedName("org") val org: Map<String, List<Permission>>,
@SerializedName("user") val user: List<Permission>,

)

data class Permission(
@SerializedName("negate") val negate: Boolean,
@SerializedName("resource_type") val resourceType: String,
@SerializedName("action") val action: Action,
)

enum class Action {
@SerializedName("create")
CREATE,

@SerializedName("read")
READ,

@SerializedName("update")
UPDATE,

@SerializedName("delete")
DELETE
}
20 changes: 17 additions & 3 deletions src/main/kotlin/com/coder/gateway/sdk/v2/models/Template.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,26 @@ data class Template(
@SerializedName("updated_at") val updatedAt: Instant,
@SerializedName("organization_id") val organizationIterator: UUID,
@SerializedName("name") val name: String,
@SerializedName("provisioner") val provisioner: String,
@SerializedName("display_name") val displayName: String,
@SerializedName("provisioner") val provisioner: ProvisionerType,
@SerializedName("active_version_id") val activeVersionID: UUID,
@SerializedName("workspace_owner_count") val workspaceOwnerCount: Int,
@SerializedName("active_user_count") val activeUserCount: Int,
@SerializedName("build_time_stats") val buildTimeStats: Map<WorkspaceTransition, TransitionStats>,
@SerializedName("description") val description: String,
@SerializedName("max_ttl_ms") val maxTTLMillis: Long,
@SerializedName("min_autostart_interval_ms") val minAutostartIntervalMillis: Long,
@SerializedName("icon") val icon: String,
@SerializedName("default_ttl_ms") val defaultTTLMillis: Long,
@SerializedName("created_by_id") val createdByID: UUID,
@SerializedName("created_by_name") val createdByName: String,
@SerializedName("allow_user_cancel_workspace_jobs") val allowUserCancelWorkspaceJobs: Boolean,
)

enum class ProvisionerType {
@SerializedName("echo")
ECHO,

@SerializedName("terraform")
TERRAFORM
}

data class TransitionStats(val p50: Long, val p95: Long)
Loading