Skip to content

Commit 777e85e

Browse files
authored
Merge pull request #104 from coder/prevent-version-mismatch
Impl: prevent version mismatch - basic semantic versioning parser and comparator - logic to compare if two coder versions are API compatible - basic checking between a tested Coder version number and the Coder version - resolves #89
2 parents 4be829c + 1ea20f2 commit 777e85e

File tree

8 files changed

+318
-0
lines changed

8 files changed

+318
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
## Unreleased
66

7+
### Added
8+
- warning system when plugin might not be compatible with Coder REST API
9+
710
### Fixed
811
- outdated Coder CLI binaries are cleaned up
912

build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ fun properties(key: String) = project.findProperty(key).toString()
66
plugins {
77
// Java support
88
id("java")
9+
// Groovy support
10+
id("groovy")
911
// Kotlin support
1012
id("org.jetbrains.kotlin.jvm") version "1.7.21"
1113
// Gradle IntelliJ Plugin
@@ -31,6 +33,11 @@ dependencies {
3133
implementation("org.zeroturnaround:zt-exec:1.12") {
3234
exclude("org.slf4j")
3335
}
36+
37+
testImplementation(platform("org.apache.groovy:groovy-bom:4.0.6"))
38+
testImplementation("org.apache.groovy:groovy")
39+
testImplementation(platform("org.spockframework:spock-bom:2.3-groovy-4.0"))
40+
testImplementation("org.spockframework:spock-core")
3441
}
3542

3643
// Configure project's dependencies
@@ -139,4 +146,8 @@ tasks {
139146
// https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel
140147
channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first()))
141148
}
149+
150+
test {
151+
useJUnitPlatform()
152+
}
142153
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.coder.gateway
2+
3+
import com.coder.gateway.sdk.CoderSemVer
4+
import com.intellij.DynamicBundle
5+
import org.jetbrains.annotations.NonNls
6+
import org.jetbrains.annotations.PropertyKey
7+
8+
@NonNls
9+
private const val BUNDLE = "version.CoderSupportedVersions"
10+
11+
object CoderSupportedVersions : DynamicBundle(BUNDLE) {
12+
val lastTestedVersion = CoderSemVer.parse(message("lastTestedCoderVersion"))
13+
14+
@Suppress("SpreadOperator")
15+
@JvmStatic
16+
private fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = getMessage(key, *params)
17+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.coder.gateway.sdk
2+
3+
4+
class CoderSemVer(private val major: Long = 0, private val minor: Long = 0) {
5+
6+
init {
7+
require(major >= 0) { "Coder major version must be a positive number" }
8+
require(minor >= 0) { "Coder minor version must be a positive number" }
9+
}
10+
11+
fun isCompatibleWith(other: CoderSemVer): Boolean {
12+
// in the initial development phase minor changes when there are API incompatibilities
13+
if (this.major == 0L) {
14+
if (other.major > 0) return false
15+
return this.minor == other.minor
16+
}
17+
return this.major <= other.major
18+
}
19+
20+
override fun equals(other: Any?): Boolean {
21+
if (this === other) return true
22+
if (javaClass != other?.javaClass) return false
23+
24+
other as CoderSemVer
25+
26+
if (major != other.major) return false
27+
if (minor != other.minor) return false
28+
29+
return true
30+
}
31+
32+
override fun hashCode(): Int {
33+
var result = major.hashCode()
34+
result = 31 * result + minor.hashCode()
35+
return result
36+
}
37+
38+
override fun toString(): String {
39+
return "CoderSemVer(major=$major, minor=$minor)"
40+
}
41+
42+
43+
companion object {
44+
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()
45+
46+
@JvmStatic
47+
fun isValidVersion(semVer: String) = pattern.matchEntire(semVer.trimStart('v')) != null
48+
49+
@JvmStatic
50+
fun parse(semVer: String): CoderSemVer {
51+
val matchResult = pattern.matchEntire(semVer.trimStart('v')) ?: throw IllegalArgumentException("$semVer could not be parsed")
52+
return CoderSemVer(
53+
if (matchResult.groupValues[1].isNotEmpty()) matchResult.groupValues[1].toLong() else 0,
54+
if (matchResult.groupValues[2].isNotEmpty()) matchResult.groupValues[2].toLong() else 0,
55+
)
56+
}
57+
}
58+
}

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.coder.gateway.views.steps
22

33
import com.coder.gateway.CoderGatewayBundle
4+
import com.coder.gateway.CoderSupportedVersions
45
import com.coder.gateway.icons.CoderIcons
56
import com.coder.gateway.models.CoderWorkspacesWizardModel
67
import com.coder.gateway.models.WorkspaceAgentModel
@@ -15,6 +16,7 @@ import com.coder.gateway.models.WorkspaceVersionStatus
1516
import com.coder.gateway.sdk.Arch
1617
import com.coder.gateway.sdk.CoderCLIManager
1718
import com.coder.gateway.sdk.CoderRestClientService
19+
import com.coder.gateway.sdk.CoderSemVer
1820
import com.coder.gateway.sdk.OS
1921
import com.coder.gateway.sdk.ex.AuthenticationResponseException
2022
import com.coder.gateway.sdk.ex.TemplateResponseException
@@ -304,6 +306,24 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) :
304306
private fun loginAndLoadWorkspace(token: String) {
305307
try {
306308
coderClient.initClientSession(localWizardModel.coderURL.toURL(), token)
309+
if (!CoderSemVer.isValidVersion(coderClient.buildVersion)) {
310+
notificationBand.apply {
311+
isVisible = true
312+
icon = AllIcons.General.Warning
313+
text = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.invalid.coder.version", coderClient.buildVersion)
314+
}
315+
} else {
316+
val coderVersion = CoderSemVer.parse(coderClient.buildVersion)
317+
val testedCoderVersion = CoderSupportedVersions.lastTestedVersion
318+
319+
if (!testedCoderVersion.isCompatibleWith(coderVersion)) {
320+
notificationBand.apply {
321+
isVisible = true
322+
icon = AllIcons.General.Warning
323+
text = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.unsupported.coder.version", coderClient.buildVersion)
324+
}
325+
}
326+
}
307327
} catch (e: AuthenticationResponseException) {
308328
logger.error("Could not authenticate on ${localWizardModel.coderURL}. Reason $e")
309329
return

src/main/resources/messages/CoderGatewayBundle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ gateway.connector.view.coder.workspaces.start.text=Start Workspace
1515
gateway.connector.view.coder.workspaces.stop.text=Stop Workspace
1616
gateway.connector.view.coder.workspaces.update.text=Update Workspace Template
1717
gateway.connector.view.coder.workspaces.unsupported.os.info=Gateway supports only Linux machines. Support for macOS and Windows is planned.
18+
gateway.connector.view.coder.workspaces.invalid.coder.version=Could not parse Coder version {0}. Coder Gateway plugin might not be compatible with this version.
19+
gateway.connector.view.coder.workspaces.unsupported.coder.version=Coder version {0} might not be compatible with this plugin version.
1820
gateway.connector.view.coder.remoteproject.loading.text=Retrieving products...
1921
gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any IDE for workspace {0} because an error was encountered
2022
gateway.connector.view.coder.remoteproject.next.text=Start IDE and connect
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lastTestedCoderVersion=0.12.9
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package com.coder.gateway.sdk
2+
3+
class CoderSemVerTest extends spock.lang.Specification {
4+
5+
def 'semver versions are valid'() {
6+
expect:
7+
CoderSemVer.isValidVersion(semver)
8+
9+
where:
10+
semver << ['0.0.4',
11+
'1.2.3',
12+
'10.20.30',
13+
'1.1.2-prerelease+meta',
14+
'1.1.2+meta',
15+
'1.1.2+meta-valid',
16+
'1.0.0-alpha',
17+
'1.0.0-beta',
18+
'1.0.0-alpha.beta',
19+
'1.0.0-alpha.beta.1',
20+
'1.0.0-alpha.1',
21+
'1.0.0-alpha0.valid',
22+
'1.0.0-alpha.0valid',
23+
'1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay',
24+
'1.0.0-rc.1+build.1',
25+
'2.0.0-rc.1+build.123',
26+
'1.2.3-beta',
27+
'10.2.3-DEV-SNAPSHOT',
28+
'1.2.3-SNAPSHOT-123',
29+
'1.0.0',
30+
'2.0.0',
31+
'1.1.7',
32+
'2.0.0+build.1848',
33+
'2.0.1-alpha.1227',
34+
'1.0.0-alpha+beta',
35+
'1.2.3----RC-SNAPSHOT.12.9.1--.12+788',
36+
'1.2.3----R-S.12.9.1--.12+meta',
37+
'1.2.3----RC-SNAPSHOT.12.9.1--.12',
38+
'1.0.0+0.build.1-rc.10000aaa-kk-0.1',
39+
'2147483647.2147483647.2147483647',
40+
'1.0.0-0A.is.legal']
41+
}
42+
43+
def 'semver versions are parsed and correct major and minor values are extracted'() {
44+
expect:
45+
CoderSemVer.parse(semver) == expectedCoderSemVer
46+
47+
where:
48+
semver || expectedCoderSemVer
49+
'0.0.4' || new CoderSemVer(0L, 0L)
50+
'1.2.3' || new CoderSemVer(1L, 2L)
51+
'10.20.30' || new CoderSemVer(10L, 20L)
52+
'1.1.2-prerelease+meta' || new CoderSemVer(1L, 1L)
53+
'1.1.2+meta' || new CoderSemVer(1L, 1L)
54+
'1.1.2+meta-valid' || new CoderSemVer(1L, 1L)
55+
'1.0.0-alpha' || new CoderSemVer(1L, 0L)
56+
'1.0.0-beta' || new CoderSemVer(1L, 0L)
57+
'1.0.0-alpha.beta' || new CoderSemVer(1L, 0L)
58+
'1.0.0-alpha.beta.1' || new CoderSemVer(1L, 0L)
59+
'1.0.0-alpha.1' || new CoderSemVer(1L, 0L)
60+
'1.0.0-alpha0.valid' || new CoderSemVer(1L, 0L)
61+
'1.0.0-alpha.0valid' || new CoderSemVer(1L, 0L)
62+
'1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay' || new CoderSemVer(1L, 0L)
63+
'1.0.0-rc.1+build.1' || new CoderSemVer(1L, 0L)
64+
'2.0.0-rc.1+build.123' || new CoderSemVer(2L, 0L)
65+
'1.2.3-beta' || new CoderSemVer(1L, 2L)
66+
'10.2.3-DEV-SNAPSHOT' || new CoderSemVer(10L, 2L)
67+
'1.2.3-SNAPSHOT-123' || new CoderSemVer(1L, 2L)
68+
'1.0.0' || new CoderSemVer(1L, 0L)
69+
'2.0.0' || new CoderSemVer(2L, 0L)
70+
'1.1.7' || new CoderSemVer(1L, 1L)
71+
'2.0.0+build.1848' || new CoderSemVer(2L, 0L)
72+
'2.0.1-alpha.1227' || new CoderSemVer(2L, 0L)
73+
'1.0.0-alpha+beta' || new CoderSemVer(1L, 0L)
74+
'1.2.3----RC-SNAPSHOT.12.9.1--.12+788' || new CoderSemVer(1L, 2L)
75+
'1.2.3----R-S.12.9.1--.12+meta' || new CoderSemVer(1L, 2L)
76+
'1.2.3----RC-SNAPSHOT.12.9.1--.12' || new CoderSemVer(1L, 2L)
77+
'1.0.0+0.build.1-rc.10000aaa-kk-0.1' || new CoderSemVer(1L, 0L)
78+
'2147483647.2147483647.2147483647' || new CoderSemVer(2147483647L, 2147483647L)
79+
'1.0.0-0A.is.legal' || new CoderSemVer(1L, 0L)
80+
}
81+
82+
def 'semver like versions that start with a `v` are considered valid'() {
83+
expect:
84+
CoderSemVer.isValidVersion(semver)
85+
86+
where:
87+
semver << ['v0.0.4',
88+
'v1.2.3',
89+
'v10.20.30',
90+
'v1.1.2-prerelease+meta',
91+
'v1.1.2+meta',
92+
'v1.1.2+meta-valid',
93+
'v1.0.0-alpha',
94+
'v1.0.0-beta',
95+
'v1.0.0-alpha.beta',
96+
'v1.0.0-alpha.beta.1',
97+
'v1.0.0-alpha.1',
98+
'v1.0.0-alpha0.valid',
99+
'v1.0.0-alpha.0valid',
100+
'v1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay',
101+
'v1.0.0-rc.1+build.1',
102+
'v2.0.0-rc.1+build.123',
103+
'v1.2.3-beta',
104+
'v10.2.3-DEV-SNAPSHOT',
105+
'v1.2.3-SNAPSHOT-123',
106+
'v1.0.0',
107+
'v2.0.0',
108+
'v1.1.7',
109+
'v2.0.0+build.1848',
110+
'v2.0.1-alpha.1227',
111+
'v1.0.0-alpha+beta',
112+
'v1.2.3----RC-SNAPSHOT.12.9.1--.12+788',
113+
'v1.2.3----R-S.12.9.1--.12+meta',
114+
'v1.2.3----RC-SNAPSHOT.12.9.1--.12',
115+
'v1.0.0+0.build.1-rc.10000aaa-kk-0.1',
116+
'v2147483647.2147483647.2147483647',
117+
'v1.0.0-0A.is.legal']
118+
}
119+
120+
def 'semver like versions that start with a `v` are parsed and correct major and minor values are extracted'() {
121+
expect:
122+
CoderSemVer.parse(semver) == expectedCoderSemVer
123+
124+
where:
125+
semver || expectedCoderSemVer
126+
'v0.0.4' || new CoderSemVer(0L, 0L)
127+
'v1.2.3' || new CoderSemVer(1L, 2L)
128+
'v10.20.30' || new CoderSemVer(10L, 20L)
129+
'v1.1.2-prerelease+meta' || new CoderSemVer(1L, 1L)
130+
'v1.1.2+meta' || new CoderSemVer(1L, 1L)
131+
'v1.1.2+meta-valid' || new CoderSemVer(1L, 1L)
132+
'v1.0.0-alpha' || new CoderSemVer(1L, 0L)
133+
'v1.0.0-beta' || new CoderSemVer(1L, 0L)
134+
'v1.0.0-alpha.beta' || new CoderSemVer(1L, 0L)
135+
'v1.0.0-alpha.beta.1' || new CoderSemVer(1L, 0L)
136+
'v1.0.0-alpha.1' || new CoderSemVer(1L, 0L)
137+
'v1.0.0-alpha0.valid' || new CoderSemVer(1L, 0L)
138+
'v1.0.0-alpha.0valid' || new CoderSemVer(1L, 0L)
139+
'v1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay' || new CoderSemVer(1L, 0L)
140+
'v1.0.0-rc.1+build.1' || new CoderSemVer(1L, 0L)
141+
'v2.0.0-rc.1+build.123' || new CoderSemVer(2L, 0L)
142+
'v1.2.3-beta' || new CoderSemVer(1L, 2L)
143+
'v10.2.3-DEV-SNAPSHOT' || new CoderSemVer(10L, 2L)
144+
'v1.2.3-SNAPSHOT-123' || new CoderSemVer(1L, 2L)
145+
'v1.0.0' || new CoderSemVer(1L, 0L)
146+
'v2.0.0' || new CoderSemVer(2L, 0L)
147+
'v1.1.7' || new CoderSemVer(1L, 1L)
148+
'v2.0.0+build.1848' || new CoderSemVer(2L, 0L)
149+
'v2.0.1-alpha.1227' || new CoderSemVer(2L, 0L)
150+
'v1.0.0-alpha+beta' || new CoderSemVer(1L, 0L)
151+
'v1.2.3----RC-SNAPSHOT.12.9.1--.12+788' || new CoderSemVer(1L, 2L)
152+
'v1.2.3----R-S.12.9.1--.12+meta' || new CoderSemVer(1L, 2L)
153+
'v1.2.3----RC-SNAPSHOT.12.9.1--.12' || new CoderSemVer(1L, 2L)
154+
'v1.0.0+0.build.1-rc.10000aaa-kk-0.1' || new CoderSemVer(1L, 0L)
155+
'v2147483647.2147483647.2147483647' || new CoderSemVer(2147483647L, 2147483647L)
156+
'v1.0.0-0A.is.legal' || new CoderSemVer(1L, 0L)
157+
}
158+
159+
def 'two initial development versions are compatible when first minor is equal to the second minor'() {
160+
expect:
161+
new CoderSemVer(0, 1).isCompatibleWith(new CoderSemVer(0, 1))
162+
}
163+
164+
def 'two initial development versions are not compatible when first minor is less than the second minor'() {
165+
expect:
166+
!new CoderSemVer(0, 1).isCompatibleWith(new CoderSemVer(0, 2))
167+
}
168+
169+
def 'two initial development versions are not compatible when first minor is bigger than the second minor'() {
170+
expect:
171+
!new CoderSemVer(0, 2).isCompatibleWith(new CoderSemVer(0, 1))
172+
}
173+
174+
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'() {
175+
expect:
176+
!new CoderSemVer(0, 2).isCompatibleWith(new CoderSemVer(1, 2))
177+
178+
and:
179+
!new CoderSemVer(1, 2).isCompatibleWith(new CoderSemVer(0, 2))
180+
}
181+
182+
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'() {
183+
expect: 'versions compatible when same major and same minor'
184+
new CoderSemVer(1, 1).isCompatibleWith(new CoderSemVer(1, 1))
185+
186+
and: 'they are also compatible when major is the same but minor is different'
187+
new CoderSemVer(1, 1).isCompatibleWith(new CoderSemVer(1, 2))
188+
189+
and: 'they are also compatible when first major is less than the second major but with same minor'
190+
new CoderSemVer(1, 1).isCompatibleWith(new CoderSemVer(2, 1))
191+
192+
and: 'they are also compatible when first major is less than the second major and also with a different minor'
193+
new CoderSemVer(1, 1).isCompatibleWith(new CoderSemVer(2, 2))
194+
}
195+
196+
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'() {
197+
expect: 'versions are not compatible when first major is bigger than the second but with same minor'
198+
!new CoderSemVer(2, 1).isCompatibleWith(new CoderSemVer(1, 1))
199+
200+
and: 'they are also not compatible when minor first minor is less than the second minor'
201+
!new CoderSemVer(2, 1).isCompatibleWith(new CoderSemVer(1, 2))
202+
203+
and: 'also also not compatible when minor first minor is bigger than the second minor'
204+
!new CoderSemVer(2, 3).isCompatibleWith(new CoderSemVer(1, 2))
205+
}
206+
}

0 commit comments

Comments
 (0)