Skip to content

Impl: prevent version mismatch #104

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 6 commits into from
Dec 1, 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

## Unreleased

### Added
- warning system when plugin might not be compatible with Coder REST API

### Fixed
- outdated Coder CLI binaries are cleaned up

Expand Down
11 changes: 11 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ fun properties(key: String) = project.findProperty(key).toString()
plugins {
// Java support
id("java")
// Groovy support
id("groovy")
// Kotlin support
id("org.jetbrains.kotlin.jvm") version "1.7.21"
// Gradle IntelliJ Plugin
Expand All @@ -31,6 +33,11 @@ dependencies {
implementation("org.zeroturnaround:zt-exec:1.12") {
exclude("org.slf4j")
}

testImplementation(platform("org.apache.groovy:groovy-bom:4.0.6"))
testImplementation("org.apache.groovy:groovy")
testImplementation(platform("org.spockframework:spock-bom:2.3-groovy-4.0"))
testImplementation("org.spockframework:spock-core")
}

// Configure project's dependencies
Expand Down Expand Up @@ -139,4 +146,8 @@ tasks {
// https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel
channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first()))
}

test {
useJUnitPlatform()
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/com/coder/gateway/CoderSupportedVersions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.coder.gateway

import com.coder.gateway.sdk.CoderSemVer
import com.intellij.DynamicBundle
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey

@NonNls
private const val BUNDLE = "version.CoderSupportedVersions"

object CoderSupportedVersions : DynamicBundle(BUNDLE) {
val lastTestedVersion = CoderSemVer.parse(message("lastTestedCoderVersion"))

@Suppress("SpreadOperator")
@JvmStatic
private fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = getMessage(key, *params)
}
58 changes: 58 additions & 0 deletions src/main/kotlin/com/coder/gateway/sdk/CoderSemVer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.coder.gateway.sdk


class CoderSemVer(private val major: Long = 0, private val minor: Long = 0) {

init {
require(major >= 0) { "Coder major version must be a positive number" }
require(minor >= 0) { "Coder minor version must be a positive number" }
}

fun isCompatibleWith(other: CoderSemVer): Boolean {
// in the initial development phase minor changes when there are API incompatibilities
if (this.major == 0L) {
if (other.major > 0) return false
return this.minor == other.minor
}
return this.major <= other.major
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as CoderSemVer

if (major != other.major) return false
if (minor != other.minor) return false

return true
}

override fun hashCode(): Int {
var result = major.hashCode()
result = 31 * result + minor.hashCode()
return result
}

override fun toString(): String {
return "CoderSemVer(major=$major, minor=$minor)"
}


companion object {
private val pattern = """^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$""".toRegex()

@JvmStatic
fun isValidVersion(semVer: String) = pattern.matchEntire(semVer.trimStart('v')) != null

@JvmStatic
fun parse(semVer: String): CoderSemVer {
val matchResult = pattern.matchEntire(semVer.trimStart('v')) ?: throw IllegalArgumentException("$semVer could not be parsed")
return CoderSemVer(
if (matchResult.groupValues[1].isNotEmpty()) matchResult.groupValues[1].toLong() else 0,
if (matchResult.groupValues[2].isNotEmpty()) matchResult.groupValues[2].toLong() else 0,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.coder.gateway.views.steps

import com.coder.gateway.CoderGatewayBundle
import com.coder.gateway.CoderSupportedVersions
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.CoderWorkspacesWizardModel
import com.coder.gateway.models.WorkspaceAgentModel
Expand All @@ -15,6 +16,7 @@ import com.coder.gateway.models.WorkspaceVersionStatus
import com.coder.gateway.sdk.Arch
import com.coder.gateway.sdk.CoderCLIManager
import com.coder.gateway.sdk.CoderRestClientService
import com.coder.gateway.sdk.CoderSemVer
import com.coder.gateway.sdk.OS
import com.coder.gateway.sdk.ex.AuthenticationResponseException
import com.coder.gateway.sdk.ex.TemplateResponseException
Expand Down Expand Up @@ -304,6 +306,24 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) :
private fun loginAndLoadWorkspace(token: String) {
try {
coderClient.initClientSession(localWizardModel.coderURL.toURL(), token)
if (!CoderSemVer.isValidVersion(coderClient.buildVersion)) {
notificationBand.apply {
isVisible = true
icon = AllIcons.General.Warning
text = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.invalid.coder.version", coderClient.buildVersion)
}
} else {
val coderVersion = CoderSemVer.parse(coderClient.buildVersion)
val testedCoderVersion = CoderSupportedVersions.lastTestedVersion

if (!testedCoderVersion.isCompatibleWith(coderVersion)) {
notificationBand.apply {
isVisible = true
icon = AllIcons.General.Warning
text = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.unsupported.coder.version", coderClient.buildVersion)
}
}
}
} catch (e: AuthenticationResponseException) {
logger.error("Could not authenticate on ${localWizardModel.coderURL}. Reason $e")
return
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/messages/CoderGatewayBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ gateway.connector.view.coder.workspaces.start.text=Start Workspace
gateway.connector.view.coder.workspaces.stop.text=Stop Workspace
gateway.connector.view.coder.workspaces.update.text=Update Workspace Template
gateway.connector.view.coder.workspaces.unsupported.os.info=Gateway supports only Linux machines. Support for macOS and Windows is planned.
gateway.connector.view.coder.workspaces.invalid.coder.version=Could not parse Coder version {0}. Coder Gateway plugin might not be compatible with this version.
gateway.connector.view.coder.workspaces.unsupported.coder.version=Coder version {0} might not be compatible with this plugin version.
gateway.connector.view.coder.remoteproject.loading.text=Retrieving products...
gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any IDE for workspace {0} because an error was encountered
gateway.connector.view.coder.remoteproject.next.text=Start IDE and connect
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lastTestedCoderVersion=0.12.9
206 changes: 206 additions & 0 deletions src/test/groovy/CoderSemVerTest.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package com.coder.gateway.sdk

class CoderSemVerTest extends spock.lang.Specification {

def 'semver versions are valid'() {
expect:
CoderSemVer.isValidVersion(semver)

where:
semver << ['0.0.4',
'1.2.3',
'10.20.30',
'1.1.2-prerelease+meta',
'1.1.2+meta',
'1.1.2+meta-valid',
'1.0.0-alpha',
'1.0.0-beta',
'1.0.0-alpha.beta',
'1.0.0-alpha.beta.1',
'1.0.0-alpha.1',
'1.0.0-alpha0.valid',
'1.0.0-alpha.0valid',
'1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay',
'1.0.0-rc.1+build.1',
'2.0.0-rc.1+build.123',
'1.2.3-beta',
'10.2.3-DEV-SNAPSHOT',
'1.2.3-SNAPSHOT-123',
'1.0.0',
'2.0.0',
'1.1.7',
'2.0.0+build.1848',
'2.0.1-alpha.1227',
'1.0.0-alpha+beta',
'1.2.3----RC-SNAPSHOT.12.9.1--.12+788',
'1.2.3----R-S.12.9.1--.12+meta',
'1.2.3----RC-SNAPSHOT.12.9.1--.12',
'1.0.0+0.build.1-rc.10000aaa-kk-0.1',
'2147483647.2147483647.2147483647',
'1.0.0-0A.is.legal']
}

def 'semver versions are parsed and correct major and minor values are extracted'() {
expect:
CoderSemVer.parse(semver) == expectedCoderSemVer

where:
semver || expectedCoderSemVer
'0.0.4' || new CoderSemVer(0L, 0L)
'1.2.3' || new CoderSemVer(1L, 2L)
'10.20.30' || new CoderSemVer(10L, 20L)
'1.1.2-prerelease+meta' || new CoderSemVer(1L, 1L)
'1.1.2+meta' || new CoderSemVer(1L, 1L)
'1.1.2+meta-valid' || new CoderSemVer(1L, 1L)
'1.0.0-alpha' || new CoderSemVer(1L, 0L)
'1.0.0-beta' || new CoderSemVer(1L, 0L)
'1.0.0-alpha.beta' || new CoderSemVer(1L, 0L)
'1.0.0-alpha.beta.1' || new CoderSemVer(1L, 0L)
'1.0.0-alpha.1' || new CoderSemVer(1L, 0L)
'1.0.0-alpha0.valid' || new CoderSemVer(1L, 0L)
'1.0.0-alpha.0valid' || new CoderSemVer(1L, 0L)
'1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay' || new CoderSemVer(1L, 0L)
'1.0.0-rc.1+build.1' || new CoderSemVer(1L, 0L)
'2.0.0-rc.1+build.123' || new CoderSemVer(2L, 0L)
'1.2.3-beta' || new CoderSemVer(1L, 2L)
'10.2.3-DEV-SNAPSHOT' || new CoderSemVer(10L, 2L)
'1.2.3-SNAPSHOT-123' || new CoderSemVer(1L, 2L)
'1.0.0' || new CoderSemVer(1L, 0L)
'2.0.0' || new CoderSemVer(2L, 0L)
'1.1.7' || new CoderSemVer(1L, 1L)
'2.0.0+build.1848' || new CoderSemVer(2L, 0L)
'2.0.1-alpha.1227' || new CoderSemVer(2L, 0L)
'1.0.0-alpha+beta' || new CoderSemVer(1L, 0L)
'1.2.3----RC-SNAPSHOT.12.9.1--.12+788' || new CoderSemVer(1L, 2L)
'1.2.3----R-S.12.9.1--.12+meta' || new CoderSemVer(1L, 2L)
'1.2.3----RC-SNAPSHOT.12.9.1--.12' || new CoderSemVer(1L, 2L)
'1.0.0+0.build.1-rc.10000aaa-kk-0.1' || new CoderSemVer(1L, 0L)
'2147483647.2147483647.2147483647' || new CoderSemVer(2147483647L, 2147483647L)
'1.0.0-0A.is.legal' || new CoderSemVer(1L, 0L)
}

def 'semver like versions that start with a `v` are considered valid'() {
expect:
CoderSemVer.isValidVersion(semver)

where:
semver << ['v0.0.4',
'v1.2.3',
'v10.20.30',
'v1.1.2-prerelease+meta',
'v1.1.2+meta',
'v1.1.2+meta-valid',
'v1.0.0-alpha',
'v1.0.0-beta',
'v1.0.0-alpha.beta',
'v1.0.0-alpha.beta.1',
'v1.0.0-alpha.1',
'v1.0.0-alpha0.valid',
'v1.0.0-alpha.0valid',
'v1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay',
'v1.0.0-rc.1+build.1',
'v2.0.0-rc.1+build.123',
'v1.2.3-beta',
'v10.2.3-DEV-SNAPSHOT',
'v1.2.3-SNAPSHOT-123',
'v1.0.0',
'v2.0.0',
'v1.1.7',
'v2.0.0+build.1848',
'v2.0.1-alpha.1227',
'v1.0.0-alpha+beta',
'v1.2.3----RC-SNAPSHOT.12.9.1--.12+788',
'v1.2.3----R-S.12.9.1--.12+meta',
'v1.2.3----RC-SNAPSHOT.12.9.1--.12',
'v1.0.0+0.build.1-rc.10000aaa-kk-0.1',
'v2147483647.2147483647.2147483647',
'v1.0.0-0A.is.legal']
}

def 'semver like versions that start with a `v` are parsed and correct major and minor values are extracted'() {
expect:
CoderSemVer.parse(semver) == expectedCoderSemVer

where:
semver || expectedCoderSemVer
'v0.0.4' || new CoderSemVer(0L, 0L)
'v1.2.3' || new CoderSemVer(1L, 2L)
'v10.20.30' || new CoderSemVer(10L, 20L)
'v1.1.2-prerelease+meta' || new CoderSemVer(1L, 1L)
'v1.1.2+meta' || new CoderSemVer(1L, 1L)
'v1.1.2+meta-valid' || new CoderSemVer(1L, 1L)
'v1.0.0-alpha' || new CoderSemVer(1L, 0L)
'v1.0.0-beta' || new CoderSemVer(1L, 0L)
'v1.0.0-alpha.beta' || new CoderSemVer(1L, 0L)
'v1.0.0-alpha.beta.1' || new CoderSemVer(1L, 0L)
'v1.0.0-alpha.1' || new CoderSemVer(1L, 0L)
'v1.0.0-alpha0.valid' || new CoderSemVer(1L, 0L)
'v1.0.0-alpha.0valid' || new CoderSemVer(1L, 0L)
'v1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay' || new CoderSemVer(1L, 0L)
'v1.0.0-rc.1+build.1' || new CoderSemVer(1L, 0L)
'v2.0.0-rc.1+build.123' || new CoderSemVer(2L, 0L)
'v1.2.3-beta' || new CoderSemVer(1L, 2L)
'v10.2.3-DEV-SNAPSHOT' || new CoderSemVer(10L, 2L)
'v1.2.3-SNAPSHOT-123' || new CoderSemVer(1L, 2L)
'v1.0.0' || new CoderSemVer(1L, 0L)
'v2.0.0' || new CoderSemVer(2L, 0L)
'v1.1.7' || new CoderSemVer(1L, 1L)
'v2.0.0+build.1848' || new CoderSemVer(2L, 0L)
'v2.0.1-alpha.1227' || new CoderSemVer(2L, 0L)
'v1.0.0-alpha+beta' || new CoderSemVer(1L, 0L)
'v1.2.3----RC-SNAPSHOT.12.9.1--.12+788' || new CoderSemVer(1L, 2L)
'v1.2.3----R-S.12.9.1--.12+meta' || new CoderSemVer(1L, 2L)
'v1.2.3----RC-SNAPSHOT.12.9.1--.12' || new CoderSemVer(1L, 2L)
'v1.0.0+0.build.1-rc.10000aaa-kk-0.1' || new CoderSemVer(1L, 0L)
'v2147483647.2147483647.2147483647' || new CoderSemVer(2147483647L, 2147483647L)
'v1.0.0-0A.is.legal' || new CoderSemVer(1L, 0L)
}

def 'two initial development versions are compatible when first minor is equal to the second minor'() {
expect:
new CoderSemVer(0, 1).isCompatibleWith(new CoderSemVer(0, 1))
}

def 'two initial development versions are not compatible when first minor is less than the second minor'() {
expect:
!new CoderSemVer(0, 1).isCompatibleWith(new CoderSemVer(0, 2))
}

def 'two initial development versions are not compatible when first minor is bigger than the second minor'() {
expect:
!new CoderSemVer(0, 2).isCompatibleWith(new CoderSemVer(0, 1))
}

def 'versions are not compatible when one version is initial phase of development and the other is not, even though the minor is the same'() {
expect:
!new CoderSemVer(0, 2).isCompatibleWith(new CoderSemVer(1, 2))

and:
!new CoderSemVer(1, 2).isCompatibleWith(new CoderSemVer(0, 2))
}

def 'two versions which are not in development phase are compatible when first major is less or equal to the other, regardless of the minor'() {
expect: 'versions compatible when same major and same minor'
new CoderSemVer(1, 1).isCompatibleWith(new CoderSemVer(1, 1))

and: 'they are also compatible when major is the same but minor is different'
new CoderSemVer(1, 1).isCompatibleWith(new CoderSemVer(1, 2))

and: 'they are also compatible when first major is less than the second major but with same minor'
new CoderSemVer(1, 1).isCompatibleWith(new CoderSemVer(2, 1))

and: 'they are also compatible when first major is less than the second major and also with a different minor'
new CoderSemVer(1, 1).isCompatibleWith(new CoderSemVer(2, 2))
}

def 'two versions which are not in development phase are not compatible when first major is greater than the second major, regardless of the minor'() {
expect: 'versions are not compatible when first major is bigger than the second but with same minor'
!new CoderSemVer(2, 1).isCompatibleWith(new CoderSemVer(1, 1))

and: 'they are also not compatible when minor first minor is less than the second minor'
!new CoderSemVer(2, 1).isCompatibleWith(new CoderSemVer(1, 2))

and: 'also also not compatible when minor first minor is bigger than the second minor'
!new CoderSemVer(2, 3).isCompatibleWith(new CoderSemVer(1, 2))
}
}