From a13c2ef60bc1eae3a6c54398f14c3a12af25aa6a Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 13 Oct 2023 14:43:44 -0800 Subject: [PATCH 1/4] Support agent ID parameter --- .../gateway/CoderGatewayConnectionProvider.kt | 20 ++++++++++++++----- .../gateway/models/WorkspaceAgentModel.kt | 1 + .../coder/gateway/sdk/v2/models/Workspace.kt | 2 ++ src/test/groovy/DataGen.groovy | 1 + 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt index f685ebc5..58b83a98 100644 --- a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt @@ -23,7 +23,8 @@ import java.net.URL private const val URL = "url" private const val TOKEN = "token" private const val WORKSPACE = "workspace" -private const val AGENT = "agent" +private const val AGENT_NAME = "agent" +private const val AGENT_ID = "agent_id" private const val FOLDER = "folder" private const val IDE_DOWNLOAD_LINK = "ide_download_link" private const val IDE_PRODUCT_CODE = "ide_product_code" @@ -72,14 +73,23 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { } // If the agent is missing and the workspace has only one, use that. - val agent = if (!parameters[AGENT].isNullOrBlank()) - agents.firstOrNull { it.name == "$workspaceName.${parameters[AGENT]}"} + // Prefer the ID over the name if both are set. + val agent = if (!parameters[AGENT_ID].isNullOrBlank()) + agents.firstOrNull {it.agentID.toString() == parameters[AGENT_ID]} + else if (!parameters[AGENT_NAME].isNullOrBlank()) + agents.firstOrNull { it.name == "$workspaceName.${parameters[AGENT_NAME]}"} else if (agents.size == 1) agents.first() else null if (agent == null) { - // TODO: Show a dropdown and ask for an agent. - throw IllegalArgumentException("Query parameter \"$AGENT\" is missing") + if (parameters[AGENT_ID].isNullOrBlank() && parameters[AGENT_NAME].isNullOrBlank()) { + // TODO: Show a dropdown and ask for an agent. + throw IllegalArgumentException("Unable to determine which agent to connect to; one of \"$AGENT_NAME\" or \"$AGENT_ID\" must be set because \"$workspaceName\" has more than one agent") + } else if (parameters[AGENT_ID].isNullOrBlank()) { + throw IllegalArgumentException("The workspace \"$workspaceName\" does not have an agent with ID \"${parameters[AGENT_ID]}\"") + } else { + throw IllegalArgumentException("The workspace \"$workspaceName\" does not have an agent named \"${parameters[AGENT_NAME]}\"") + } } if (agent.agentStatus.pending()) { diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt index c4893b9c..61276100 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt @@ -14,6 +14,7 @@ import javax.swing.Icon // iterate over the list we can add the workspace row if it has no agents // otherwise iterate over the agents and then flatten the result. data class WorkspaceAgentModel( + val agentID: UUID?, val workspaceID: UUID, val workspaceName: String, val name: String, // Name of the workspace OR the agent if this is for an agent. diff --git a/src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt b/src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt index 129489c6..e97794ea 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt @@ -35,6 +35,7 @@ fun Workspace.toAgentModels(): Set { val wam = this.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }.map { agent -> val workspaceWithAgentName = "${this.name}.${agent.name}" val wm = WorkspaceAgentModel( + agent.id, this.id, this.name, workspaceWithAgentName, @@ -55,6 +56,7 @@ fun Workspace.toAgentModels(): Set { }.toSet() if (wam.isNullOrEmpty()) { val wm = WorkspaceAgentModel( + null, this.id, this.name, this.name, diff --git a/src/test/groovy/DataGen.groovy b/src/test/groovy/DataGen.groovy index af609f99..69d329fe 100644 --- a/src/test/groovy/DataGen.groovy +++ b/src/test/groovy/DataGen.groovy @@ -7,6 +7,7 @@ import com.coder.gateway.sdk.v2.models.WorkspaceTransition class DataGen { static WorkspaceAgentModel workspace(String name, String workspaceName = name) { return new WorkspaceAgentModel( + UUID.randomUUID(), UUID.randomUUID(), workspaceName, name, From 7d0d8c4671043003925662574184082fc4bcb000 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 16 Oct 2023 11:19:51 -0800 Subject: [PATCH 2/4] Fix DataGen not quite mirroring toAgentModels() It should omit agent information when the workspace specifies no agents and the name should combine the workspace and agent names. --- .../coder/gateway/models/WorkspaceAgentModel.kt | 2 +- src/test/groovy/DataGen.groovy | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt index 61276100..d9678422 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt @@ -17,7 +17,7 @@ data class WorkspaceAgentModel( val agentID: UUID?, val workspaceID: UUID, val workspaceName: String, - val name: String, // Name of the workspace OR the agent if this is for an agent. + val name: String, // Name of the workspace OR workspace.agent if this is for an agent. val templateID: UUID, val templateName: String, val templateIconPath: String, diff --git a/src/test/groovy/DataGen.groovy b/src/test/groovy/DataGen.groovy index 69d329fe..206909f2 100644 --- a/src/test/groovy/DataGen.groovy +++ b/src/test/groovy/DataGen.groovy @@ -5,12 +5,19 @@ import com.coder.gateway.sdk.v2.models.WorkspaceStatus import com.coder.gateway.sdk.v2.models.WorkspaceTransition class DataGen { - static WorkspaceAgentModel workspace(String name, String workspaceName = name) { + // Create a random workspace agent model. If the workspace name is omitted + // then return a model without any agent bits, similar to what + // toAgentModels() does if the workspace does not specify any agents. + // TODO: Maybe better to randomly generate the workspace and then call + // toAgentModels() on it. Also the way an "agent" model can have no + // agent in it seems weird; can we refactor to remove + // WorkspaceAgentModel and use the original structs from the API? + static WorkspaceAgentModel workspace(String name, String workspaceName = "", UUID agentId = UUID.randomUUID()) { return new WorkspaceAgentModel( + workspaceName == "" ? null : agentId, UUID.randomUUID(), - UUID.randomUUID(), - workspaceName, - name, + workspaceName == "" ? name : workspaceName, + workspaceName == "" ? name : (workspaceName + "." + name), UUID.randomUUID(), "template-name", "template-icon-path", From 3c5b20c42b9908b13de0c2beb2480e3e85b6aba0 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 16 Oct 2023 11:22:18 -0800 Subject: [PATCH 3/4] Update workspace generator function name This generates agent models, not workspaces. Renaming opens the way for generating random workspaces. --- src/test/groovy/CoderCLIManagerTest.groovy | 4 +- .../groovy/CoderWorkspacesStepViewTest.groovy | 54 +++++++++---------- src/test/groovy/DataGen.groovy | 2 +- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/test/groovy/CoderCLIManagerTest.groovy b/src/test/groovy/CoderCLIManagerTest.groovy index effa5605..c6b7e1a6 100644 --- a/src/test/groovy/CoderCLIManagerTest.groovy +++ b/src/test/groovy/CoderCLIManagerTest.groovy @@ -413,7 +413,7 @@ class CoderCLIManagerTest extends Specification { .replace("/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64", CoderCLIManager.escape(ccm.localBinaryPath.toString())) when: - ccm.configSsh(workspaces.collect { DataGen.workspace(it) }, headerCommand) + ccm.configSsh(workspaces.collect { DataGen.workspaceAgentModel(it) }, headerCommand) then: sshConfigPath.toFile().text == expectedConf @@ -473,7 +473,7 @@ class CoderCLIManagerTest extends Specification { def ccm = new CoderCLIManager(new URL("https://test.coder.invalid"), tmpdir) when: - ccm.configSsh(["foo", "bar"].collect { DataGen.workspace(it) }, headerCommand) + ccm.configSsh(["foo", "bar"].collect { DataGen.workspaceAgentModel(it) }, headerCommand) then: thrown(Exception) diff --git a/src/test/groovy/CoderWorkspacesStepViewTest.groovy b/src/test/groovy/CoderWorkspacesStepViewTest.groovy index c684e963..d07dde3d 100644 --- a/src/test/groovy/CoderWorkspacesStepViewTest.groovy +++ b/src/test/groovy/CoderWorkspacesStepViewTest.groovy @@ -9,46 +9,46 @@ class CoderWorkspacesStepViewTest extends Specification { def table = new WorkspacesTable() table.listTableModel.items = List.of( // An off workspace. - DataGen.workspace("ws1", "ws1"), + DataGen.workspaceAgentModel("ws1", "ws1"), // On workspaces. - DataGen.workspace("agent1", "ws2"), - DataGen.workspace("agent2", "ws2"), - DataGen.workspace("agent3", "ws3"), + DataGen.workspaceAgentModel("agent1", "ws2"), + DataGen.workspaceAgentModel("agent2", "ws2"), + DataGen.workspaceAgentModel("agent3", "ws3"), // Another off workspace. - DataGen.workspace("ws4", "ws4"), + DataGen.workspaceAgentModel("ws4", "ws4"), // In practice we do not list both agents and workspaces // together but here test that anyway with an agent first and // then with a workspace first. - DataGen.workspace("agent2", "ws5"), - DataGen.workspace("ws5", "ws5"), - DataGen.workspace("ws6", "ws6"), - DataGen.workspace("agent3", "ws6"), + DataGen.workspaceAgentModel("agent2", "ws5"), + DataGen.workspaceAgentModel("ws5", "ws5"), + DataGen.workspaceAgentModel("ws6", "ws6"), + DataGen.workspaceAgentModel("agent3", "ws6"), ) expect: table.getNewSelection(selected) == expected where: - selected | expected - null | -1 // No selection. - DataGen.workspace("gone", "gone") | -1 // No workspace that matches. - DataGen.workspace("ws1", "ws1") | 0 // Workspace exact match. - DataGen.workspace("gone", "ws1") | 0 // Agent gone, select workspace. - DataGen.workspace("ws2", "ws2") | 1 // Workspace gone, select first agent. - DataGen.workspace("agent1", "ws2") | 1 // Agent exact match. - DataGen.workspace("agent2", "ws2") | 2 // Agent exact match. - DataGen.workspace("ws3", "ws3") | 3 // Workspace gone, select first agent. - DataGen.workspace("agent3", "ws3") | 3 // Agent exact match. - DataGen.workspace("gone", "ws4") | 4 // Agent gone, select workspace. - DataGen.workspace("ws4", "ws4") | 4 // Workspace exact match. - DataGen.workspace("agent2", "ws5") | 5 // Agent exact match. - DataGen.workspace("gone", "ws5") | 5 // Agent gone, another agent comes first. - DataGen.workspace("ws5", "ws5") | 6 // Workspace exact match. - DataGen.workspace("ws6", "ws6") | 7 // Workspace exact match. - DataGen.workspace("gone", "ws6") | 7 // Agent gone, workspace comes first. - DataGen.workspace("agent3", "ws6") | 8 // Agent exact match. + selected | expected + null | -1 // No selection. + DataGen.workspaceAgentModel("gone", "gone") | -1 // No workspace that matches. + DataGen.workspaceAgentModel("ws1", "ws1") | 0 // Workspace exact match. + DataGen.workspaceAgentModel("gone", "ws1") | 0 // Agent gone, select workspace. + DataGen.workspaceAgentModel("ws2", "ws2") | 1 // Workspace gone, select first agent. + DataGen.workspaceAgentModel("agent1", "ws2") | 1 // Agent exact match. + DataGen.workspaceAgentModel("agent2", "ws2") | 2 // Agent exact match. + DataGen.workspaceAgentModel("ws3", "ws3") | 3 // Workspace gone, select first agent. + DataGen.workspaceAgentModel("agent3", "ws3") | 3 // Agent exact match. + DataGen.workspaceAgentModel("gone", "ws4") | 4 // Agent gone, select workspace. + DataGen.workspaceAgentModel("ws4", "ws4") | 4 // Workspace exact match. + DataGen.workspaceAgentModel("agent2", "ws5") | 5 // Agent exact match. + DataGen.workspaceAgentModel("gone", "ws5") | 5 // Agent gone, another agent comes first. + DataGen.workspaceAgentModel("ws5", "ws5") | 6 // Workspace exact match. + DataGen.workspaceAgentModel("ws6", "ws6") | 7 // Workspace exact match. + DataGen.workspaceAgentModel("gone", "ws6") | 7 // Agent gone, workspace comes first. + DataGen.workspaceAgentModel("agent3", "ws6") | 8 // Agent exact match. } } diff --git a/src/test/groovy/DataGen.groovy b/src/test/groovy/DataGen.groovy index 206909f2..71e8e060 100644 --- a/src/test/groovy/DataGen.groovy +++ b/src/test/groovy/DataGen.groovy @@ -12,7 +12,7 @@ class DataGen { // toAgentModels() on it. Also the way an "agent" model can have no // agent in it seems weird; can we refactor to remove // WorkspaceAgentModel and use the original structs from the API? - static WorkspaceAgentModel workspace(String name, String workspaceName = "", UUID agentId = UUID.randomUUID()) { + static WorkspaceAgentModel workspaceAgentModel(String name, String workspaceName = "", UUID agentId = UUID.randomUUID()) { return new WorkspaceAgentModel( workspaceName == "" ? null : agentId, UUID.randomUUID(), From 691c276cda28a46f8ccfc692d66992540f828dbc Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 16 Oct 2023 11:37:42 -0800 Subject: [PATCH 4/4] Break out getting matching agents I also made a tweak to check that the agent ID is not null since toAgentModels() will return the workspace without any agent bits set if there are no agents. And the wrong error message would show when either the id or name were missing. I also flipped them around while fixing this to match the order above it. --- .../gateway/CoderGatewayConnectionProvider.kt | 72 +++++++---- .../CoderGatewayConnectionProviderTest.groovy | 114 ++++++++++++++++++ src/test/groovy/DataGen.groovy | 93 +++++++++++++- 3 files changed, 253 insertions(+), 26 deletions(-) create mode 100644 src/test/groovy/CoderGatewayConnectionProviderTest.groovy diff --git a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt index 58b83a98..c28b1483 100644 --- a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt @@ -3,10 +3,12 @@ package com.coder.gateway import com.coder.gateway.models.TokenSource +import com.coder.gateway.models.WorkspaceAgentModel import com.coder.gateway.sdk.CoderCLIManager import com.coder.gateway.sdk.CoderRestClient import com.coder.gateway.sdk.ex.AuthenticationResponseException import com.coder.gateway.sdk.toURL +import com.coder.gateway.sdk.v2.models.Workspace import com.coder.gateway.sdk.v2.models.WorkspaceStatus import com.coder.gateway.sdk.v2.models.toAgentModels import com.coder.gateway.sdk.withPath @@ -67,30 +69,8 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { WorkspaceStatus.RUNNING -> Unit // All is well } - val agents = workspace.toAgentModels() - if (agents.isEmpty()) { - throw IllegalArgumentException("The workspace \"$workspaceName\" has no agents") - } - - // If the agent is missing and the workspace has only one, use that. - // Prefer the ID over the name if both are set. - val agent = if (!parameters[AGENT_ID].isNullOrBlank()) - agents.firstOrNull {it.agentID.toString() == parameters[AGENT_ID]} - else if (!parameters[AGENT_NAME].isNullOrBlank()) - agents.firstOrNull { it.name == "$workspaceName.${parameters[AGENT_NAME]}"} - else if (agents.size == 1) agents.first() - else null - - if (agent == null) { - if (parameters[AGENT_ID].isNullOrBlank() && parameters[AGENT_NAME].isNullOrBlank()) { - // TODO: Show a dropdown and ask for an agent. - throw IllegalArgumentException("Unable to determine which agent to connect to; one of \"$AGENT_NAME\" or \"$AGENT_ID\" must be set because \"$workspaceName\" has more than one agent") - } else if (parameters[AGENT_ID].isNullOrBlank()) { - throw IllegalArgumentException("The workspace \"$workspaceName\" does not have an agent with ID \"${parameters[AGENT_ID]}\"") - } else { - throw IllegalArgumentException("The workspace \"$workspaceName\" does not have an agent named \"${parameters[AGENT_NAME]}\"") - } - } + // TODO: Show a dropdown and ask for an agent if missing. + val agent = getMatchingAgent(parameters, workspace) if (agent.agentStatus.pending()) { // TODO: Wait for the agent to be ready. @@ -211,5 +191,49 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { companion object { val logger = Logger.getInstance(CoderGatewayConnectionProvider::class.java.simpleName) + + /** + * Return the agent matching the provided agent ID or name in the + * parameters. The name is ignored if the ID is set. If neither was + * supplied and the workspace has only one agent, return that. + * Otherwise throw an error. + * + * @throws [MissingArgumentException, IllegalArgumentException] + */ + @JvmStatic + fun getMatchingAgent(parameters: Map, workspace: Workspace): WorkspaceAgentModel { + // A WorkspaceAgentModel will still be returned if there are no + // agents; in this case it represents the workspace instead. + // TODO: Seems confusing for something with "agent" in the name to + // potentially not actually be an agent; can we replace + // WorkspaceAgentModel with the original structs from the API? + val agents = workspace.toAgentModels() + if (agents.isEmpty() || (agents.size == 1 && agents.first().agentID == null)) { + throw IllegalArgumentException("The workspace \"${workspace.name}\" has no agents") + } + + // If the agent is missing and the workspace has only one, use that. + // Prefer the ID over the name if both are set. + val agent = if (!parameters[AGENT_ID].isNullOrBlank()) + agents.firstOrNull { it.agentID.toString() == parameters[AGENT_ID] } + else if (!parameters[AGENT_NAME].isNullOrBlank()) + agents.firstOrNull { it.name == "${workspace.name}.${parameters[AGENT_NAME]}"} + else if (agents.size == 1) agents.first() + else null + + if (agent == null) { + if (!parameters[AGENT_ID].isNullOrBlank()) { + throw IllegalArgumentException("The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters[AGENT_ID]}\"") + } else if (!parameters[AGENT_NAME].isNullOrBlank()){ + throw IllegalArgumentException("The workspace \"${workspace.name}\"does not have an agent named \"${parameters[AGENT_NAME]}\"") + } else { + throw MissingArgumentException("Unable to determine which agent to connect to; one of \"$AGENT_NAME\" or \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent") + } + } + + return agent + } } } + +class MissingArgumentException(message: String) : IllegalArgumentException(message) diff --git a/src/test/groovy/CoderGatewayConnectionProviderTest.groovy b/src/test/groovy/CoderGatewayConnectionProviderTest.groovy new file mode 100644 index 00000000..5d5008ff --- /dev/null +++ b/src/test/groovy/CoderGatewayConnectionProviderTest.groovy @@ -0,0 +1,114 @@ +package com.coder.gateway + +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +@Unroll +class CoderGatewayConnectionProviderTest extends Specification { + @Shared + def agents = [ + agent_name_3: "b0e4c54d-9ba9-4413-8512-11ca1e826a24", + agent_name_2: "fb3daea4-da6b-424d-84c7-36b90574cfef", + agent_name: "9a920eee-47fb-4571-9501-e4b3120c12f2", + ] + def oneAgent = [ + agent_name_3: "b0e4c54d-9ba9-4413-8512-11ca1e826a24" + ] + + def "gets matching agent"() { + expect: + def ws = DataGen.workspace("ws", agents) + CoderGatewayConnectionProvider.getMatchingAgent(parameters, ws).agentID == UUID.fromString(expected) + + where: + parameters | expected + [agent: "agent_name"] | "9a920eee-47fb-4571-9501-e4b3120c12f2" + [agent_id: "9a920eee-47fb-4571-9501-e4b3120c12f2"] | "9a920eee-47fb-4571-9501-e4b3120c12f2" + [agent: "agent_name_2"] | "fb3daea4-da6b-424d-84c7-36b90574cfef" + [agent_id: "fb3daea4-da6b-424d-84c7-36b90574cfef"] | "fb3daea4-da6b-424d-84c7-36b90574cfef" + [agent: "agent_name_3"] | "b0e4c54d-9ba9-4413-8512-11ca1e826a24" + [agent_id: "b0e4c54d-9ba9-4413-8512-11ca1e826a24"] | "b0e4c54d-9ba9-4413-8512-11ca1e826a24" + + // Prefer agent_id. + [agent: "agent_name", agent_id: "b0e4c54d-9ba9-4413-8512-11ca1e826a24"] | "b0e4c54d-9ba9-4413-8512-11ca1e826a24" + } + + def "fails to get matching agent"() { + when: + def ws = DataGen.workspace("ws", agents) + CoderGatewayConnectionProvider.getMatchingAgent(parameters, ws) + + then: + def err = thrown(expected) + err.message.contains(message) + + where: + parameters | expected | message + [:] | MissingArgumentException | "Unable to determine" + [agent: ""] | MissingArgumentException | "Unable to determine" + [agent_id: ""] | MissingArgumentException | "Unable to determine" + [agent: null] | MissingArgumentException | "Unable to determine" + [agent_id: null] | MissingArgumentException | "Unable to determine" + [agent: "ws"] | IllegalArgumentException | "agent named" + [agent: "ws.agent_name"] | IllegalArgumentException | "agent named" + [agent: "agent_name_4"] | IllegalArgumentException | "agent named" + [agent_id: "not-a-uuid"] | IllegalArgumentException | "agent with ID" + [agent_id: "ceaa7bcf-1612-45d7-b484-2e0da9349168"] | IllegalArgumentException | "agent with ID" + + // Will ignore agent if agent_id is set even if agent matches. + [agent: "agent_name", agent_id: "ceaa7bcf-1612-45d7-b484-2e0da9349168"] | IllegalArgumentException | "agent with ID" + } + + def "gets the first agent when workspace has only one"() { + expect: + def ws = DataGen.workspace("ws", oneAgent) + CoderGatewayConnectionProvider.getMatchingAgent(parameters, ws).agentID == UUID.fromString("b0e4c54d-9ba9-4413-8512-11ca1e826a24") + + where: + parameters << [ + [:], + [agent: ""], + [agent_id: ""], + [agent: null], + [agent_id: null], + ] + } + + def "fails to get agent when workspace has only one"() { + when: + def ws = DataGen.workspace("ws", oneAgent) + CoderGatewayConnectionProvider.getMatchingAgent(parameters, ws) + + then: + def err = thrown(expected) + err.message.contains(message) + + where: + parameters | expected | message + [agent: "ws"] | IllegalArgumentException | "agent named" + [agent: "ws.agent_name_3"] | IllegalArgumentException | "agent named" + [agent: "agent_name_4"] | IllegalArgumentException | "agent named" + [agent_id: "ceaa7bcf-1612-45d7-b484-2e0da9349168"] | IllegalArgumentException | "agent with ID" + } + + def "fails to get agent from workspace without agents"() { + when: + def ws = DataGen.workspace("ws") + CoderGatewayConnectionProvider.getMatchingAgent(parameters, ws) + + then: + def err = thrown(expected) + err.message.contains(message) + + where: + parameters | expected | message + [:] | IllegalArgumentException | "has no agents" + [agent: ""] | IllegalArgumentException | "has no agents" + [agent_id: ""] | IllegalArgumentException | "has no agents" + [agent: null] | IllegalArgumentException | "has no agents" + [agent_id: null] | IllegalArgumentException | "has no agents" + [agent: "agent_name"] | IllegalArgumentException | "has no agents" + [agent_id: "9a920eee-47fb-4571-9501-e4b3120c12f2"] | IllegalArgumentException | "has no agents" + } +} diff --git a/src/test/groovy/DataGen.groovy b/src/test/groovy/DataGen.groovy index 71e8e060..62d8abd2 100644 --- a/src/test/groovy/DataGen.groovy +++ b/src/test/groovy/DataGen.groovy @@ -1,8 +1,7 @@ import com.coder.gateway.models.WorkspaceAgentModel import com.coder.gateway.models.WorkspaceAndAgentStatus import com.coder.gateway.models.WorkspaceVersionStatus -import com.coder.gateway.sdk.v2.models.WorkspaceStatus -import com.coder.gateway.sdk.v2.models.WorkspaceTransition +import com.coder.gateway.sdk.v2.models.* class DataGen { // Create a random workspace agent model. If the workspace name is omitted @@ -31,4 +30,94 @@ class DataGen { null ) } + + static Workspace workspace(String name, Map agents = [:]) { + UUID wsId = UUID.randomUUID() + UUID ownerId = UUID.randomUUID() + List resources = agents.collect{ agentName, agentId -> new WorkspaceResource( + UUID.randomUUID(), // id + new Date().toInstant(), // created_at + UUID.randomUUID(), // job_id + WorkspaceTransition.START, + "type", + "name", + false, // hide + "icon", + List.of(new WorkspaceAgent( + UUID.fromString(agentId), + new Date().toInstant(), // created_at + new Date().toInstant(), // updated_at + null, // first_connected_at + null, // last_connected_at + null, // disconnected_at + WorkspaceAgentStatus.CONNECTED, + agentName, + UUID.randomUUID(), // resource_id + null, // instance_id + "arch", // architecture + [:], // environment_variables + "os", // operating_system + null, // startup_script + null, // directory + null, // expanded_directory + "version", // version + List.of(), // apps + null, // latency + 0, // connection_timeout_seconds + "url", // troubleshooting_url + WorkspaceAgentLifecycleState.READY, + false, // login_before_ready + )), + null, // metadata + 0, // daily_cost + )} + return new Workspace( + wsId, + new Date().toInstant(), // created_at + new Date().toInstant(), // updated_at + ownerId, + "owner-name", + UUID.randomUUID(), // template_id + "template-name", + "template-display-name", + "template-icon", + false, // template_allow_user_cancel_workspace_jobs + new WorkspaceBuild( + UUID.randomUUID(), // id + new Date().toInstant(), // created_at + new Date().toInstant(), // updated_at + wsId, + name, + ownerId, + "owner-name", + UUID.randomUUID(), // template_version_id + 0, // build_number + WorkspaceTransition.START, + UUID.randomUUID(), // initiator_id + "initiator-name", + new ProvisionerJob( + UUID.randomUUID(), // id + new Date().toInstant(), // created_at + null, // started_at + null, // completed_at + null, // canceled_at + null, // error + ProvisionerJobStatus.SUCCEEDED, + null, // worker_id + UUID.randomUUID(), // file_id + [:], // tags + ), + BuildReason.INITIATOR, + resources, + null, // deadline + WorkspaceStatus.RUNNING, + 0, // daily_cost + ), + false, // outdated + name, + null, // autostart_schedule + null, // ttl_ms + new Date().toInstant(), // last_used_at + ) + } }