Skip to content

Commit a3f7ce4

Browse files
fix(codecatalyst): Notify ssh extension min version required
Problem: When a user attempted to start a dev env, it failed since the ssh extension version was too old. Solution: Enforce a minumum version of the ssh extension. If the version is unsupported we will notify the user through an error message. This solution can be used by any future extension as well. Signed-off-by: Nikolas Komonen <[email protected]>
1 parent d2983c2 commit a3f7ce4

File tree

5 files changed

+143
-16
lines changed

5 files changed

+143
-16
lines changed

src/codecatalyst/tools.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { getLogger } from '../shared/logger'
2424
import { getIdeProperties } from '../shared/extensionUtilities'
2525
import { showConfirmationMessage } from '../shared/utilities/messages'
2626
import { getSshConfigPath } from '../shared/extensions/ssh'
27-
import { VSCODE_EXTENSION_ID } from '../shared/extensions'
27+
import { VSCODE_EXTENSION_ID, vscodeExtensionMinVersion } from '../shared/extensions'
2828

2929
interface DependencyPaths {
3030
readonly vsc: string
@@ -40,8 +40,13 @@ interface MissingTool {
4040
export const hostNamePrefix = 'aws-devenv-'
4141

4242
export async function ensureDependencies(): Promise<Result<DependencyPaths, CancellationError | Error>> {
43-
if (!isExtensionInstalled(VSCODE_EXTENSION_ID.remotessh)) {
44-
showInstallExtensionMsg(VSCODE_EXTENSION_ID.remotessh, 'Remote SSH', 'Connecting to Dev Environment')
43+
if (!isExtensionInstalled(VSCODE_EXTENSION_ID.remotessh, vscodeExtensionMinVersion.remotessh)) {
44+
showInstallExtensionMsg(
45+
VSCODE_EXTENSION_ID.remotessh,
46+
'Remote SSH',
47+
'Connecting to Dev Environment',
48+
vscodeExtensionMinVersion.remotessh
49+
)
4550

4651
return Result.err(
4752
new ToolkitError('Remote SSH extension not installed', {

src/shared/extensions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export const VSCODE_EXTENSION_ID = {
2525
remotessh: 'ms-vscode-remote.remote-ssh',
2626
}
2727

28+
export const vscodeExtensionMinVersion = {
29+
remotessh: '0.98.0',
30+
}
31+
2832
/**
2933
* Long-lived, extension-scoped, shared globals.
3034
*/

src/shared/utilities/vsCodeUtils.ts

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import * as pathutils from './pathUtils'
1010
import { getLogger } from '../logger/logger'
1111
import { CancellationError, Timeout, waitTimeout, waitUntil } from './timeoutUtils'
1212
import { telemetry } from '../telemetry/telemetry'
13+
import * as semver from 'semver'
14+
import { isNonNullable } from './tsUtils'
1315

1416
// TODO: Consider NLS initialization/configuration here & have packages to import localize from here
1517
export const localize = nls.loadMessageBundle()
@@ -53,8 +55,31 @@ export function isExtensionActive(extId: string): boolean {
5355
return !!extension && extension.isActive
5456
}
5557

56-
export function isExtensionInstalled(extId: string): boolean {
57-
return !!vscode.extensions.getExtension(extId)
58+
/**
59+
* Checks if an extension is installed and meets the version requirement
60+
* @param minVersion The minimum semver required for the extension
61+
*/
62+
export function isExtensionInstalled(
63+
extId: string,
64+
minVersion?: string,
65+
getExtension = vscode.extensions.getExtension
66+
): boolean {
67+
const ext = getExtension(extId)
68+
if (ext === undefined) {
69+
return false
70+
}
71+
72+
if (minVersion === undefined) {
73+
return true
74+
}
75+
76+
// check ext has valid version
77+
const extSemver = semver.coerce(ext.packageJSON.version)
78+
const minSemver = semver.coerce(minVersion)
79+
if (!isNonNullable(extSemver) || !isNonNullable(minSemver)) {
80+
return false
81+
}
82+
return semver.gte(extSemver, minSemver)
5883
}
5984

6085
/**
@@ -63,19 +88,14 @@ export function isExtensionInstalled(extId: string): boolean {
6388
export function showInstallExtensionMsg(
6489
extId: string,
6590
extName: string,
66-
feat = `${getIdeProperties().company} Toolkit`
91+
feat = `${getIdeProperties().company} Toolkit`,
92+
minVersion?: string
6793
): boolean {
68-
if (vscode.extensions.getExtension(extId)) {
94+
if (isExtensionInstalled(extId, minVersion)) {
6995
return true
7096
}
7197

72-
const msg = localize(
73-
'AWS.missingExtension',
74-
'{0} requires the {1} extension ({2}) to be installed and enabled.',
75-
feat,
76-
extName,
77-
extId
78-
)
98+
const msg = buildMissingExtensionMessage(extId, extName, minVersion, feat)
7999

80100
const installBtn = localize('AWS.missingExtension.install', 'Install...')
81101
const items = [installBtn]
@@ -90,6 +110,25 @@ export function showInstallExtensionMsg(
90110
return false
91111
}
92112

113+
export function buildMissingExtensionMessage(
114+
extId: string,
115+
extName: string,
116+
minVersion?: string,
117+
feat = `${getIdeProperties().company} Toolkit`
118+
): string {
119+
const minV = semver.coerce(minVersion)
120+
const expectedVersionMsg = isNonNullable(minV) ? ` of version >=${minV}` : ''
121+
122+
return localize(
123+
'AWS.missingExtension',
124+
"{0} requires the {1} extension ('{2}'{3}) to be installed and enabled.",
125+
feat,
126+
extName,
127+
extId,
128+
expectedVersionMsg
129+
)
130+
}
131+
93132
/**
94133
* Activates an extension and returns it, or does nothing if the extension is
95134
* not installed.

src/test/shared/utilities/vscodeUtils.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as assert from 'assert'
77
import { VSCODE_EXTENSION_ID } from '../../../shared/extensions'
88
import * as vscodeUtil from '../../../shared/utilities/vsCodeUtils'
9+
import * as vscode from 'vscode'
910

1011
describe('vscodeUtils', async function () {
1112
it('activateExtension(), isExtensionActive()', async function () {
@@ -20,3 +21,81 @@ describe('vscodeUtils', async function () {
2021
assert.deepStrictEqual(vscodeUtil.isExtensionActive(VSCODE_EXTENSION_ID.awstoolkit), true)
2122
})
2223
})
24+
25+
describe('isExtensionInstalled()', function () {
26+
const smallerVersion = '0.9.0'
27+
const extVersion = '1.0.0'
28+
const largerVersion = '2.0.0'
29+
const extId = 'my.ext.id'
30+
let ext: vscode.Extension<any>
31+
let getExtension: (extId: string) => vscode.Extension<any>
32+
33+
beforeEach(function () {
34+
ext = {
35+
packageJSON: {
36+
version: extVersion,
37+
},
38+
} as vscode.Extension<any>
39+
getExtension = _ => ext
40+
})
41+
42+
it('fails if extension could not be found', function () {
43+
const noExtFunc = (extId: string) => undefined
44+
assert.ok(!vscodeUtil.isExtensionInstalled(extId, undefined, noExtFunc))
45+
})
46+
47+
it('succeeds on same min version', function () {
48+
assert.ok(vscodeUtil.isExtensionInstalled(extId, extVersion, getExtension))
49+
})
50+
51+
it('succeeds on smaller min version', function () {
52+
assert.ok(vscodeUtil.isExtensionInstalled(extId, smallerVersion, getExtension))
53+
})
54+
55+
it('fails on larger min version', function () {
56+
assert.ok(!vscodeUtil.isExtensionInstalled(extId, largerVersion, getExtension))
57+
})
58+
59+
it('can handle labels on a version', function () {
60+
ext.packageJSON.version = `${extVersion}-SNAPSHOT`
61+
assert.ok(vscodeUtil.isExtensionInstalled(extId, `${smallerVersion}-ALPHA`, getExtension))
62+
})
63+
64+
it('is valid when no min version is provided', function () {
65+
assert.ok(vscodeUtil.isExtensionInstalled(extId, undefined, getExtension))
66+
})
67+
68+
it('fails on malformed version', function () {
69+
// malformed min version
70+
assert.ok(!vscodeUtil.isExtensionInstalled(extId, 'malformed.version', getExtension))
71+
72+
// malformed ext version
73+
ext.packageJSON.version = 'malformed.version'
74+
assert.ok(!vscodeUtil.isExtensionInstalled(extId, extVersion, getExtension))
75+
})
76+
})
77+
78+
describe('buildMissingExtensionMessage()', function () {
79+
const extId = 'MY.EXT.ID'
80+
const extName = 'MY EXTENSION'
81+
const minVer = '1.0.0'
82+
const feat = 'FEATURE'
83+
84+
// Test when a minVer is given
85+
it('minVer', function () {
86+
const message = vscodeUtil.buildMissingExtensionMessage(extId, extName, minVer, feat)
87+
assert.strictEqual(
88+
message,
89+
`${feat} requires the ${extName} extension (\'${extId}\' of version >=${minVer}) to be installed and enabled.`
90+
)
91+
})
92+
93+
// Test when a minVer is not given
94+
it('no minVer', function () {
95+
const message = vscodeUtil.buildMissingExtensionMessage(extId, extName, undefined, feat)
96+
assert.strictEqual(
97+
message,
98+
`${feat} requires the ${extName} extension (\'${extId}\') to be installed and enabled.`
99+
)
100+
})
101+
})

src/testE2E/codecatalyst/client.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { GetDevEnvironmentRequest } from 'aws-sdk/clients/codecatalyst'
2424
import { getTestWindow } from '../../test/shared/vscode/window'
2525
import { patchObject, registerAuthHook, using } from '../../test/setupUtil'
2626
import { isExtensionInstalled } from '../../shared/utilities/vsCodeUtils'
27-
import { VSCODE_EXTENSION_ID } from '../../shared/extensions'
27+
import { VSCODE_EXTENSION_ID, vscodeExtensionMinVersion } from '../../shared/extensions'
2828
import { captureEventOnce } from '../../test/testUtil'
2929
import { toStream } from '../../shared/utilities/collectionUtils'
3030
import { toCollection } from '../../shared/utilities/asyncCollection'
@@ -167,7 +167,7 @@ describe('Test how this codebase uses the CodeCatalyst API', function () {
167167
})
168168

169169
it('prompts to install the ssh extension if not available', async function () {
170-
if (isExtensionInstalled(VSCODE_EXTENSION_ID.remotessh)) {
170+
if (isExtensionInstalled(VSCODE_EXTENSION_ID.remotessh, vscodeExtensionMinVersion.remotessh)) {
171171
this.skip()
172172
}
173173

0 commit comments

Comments
 (0)