-
Notifications
You must be signed in to change notification settings - Fork 297
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): new version notifications (#454)
The CLI will print a notification if there is a new version of the CDK8s CLI available in npm. The result is cached under ~/.cdk8s-cli.version with a TTL of 30min. Resolves #452 --- <img width="963" alt="Screen Shot 2020-12-15 at 23 06 38" src="https://user-images.githubusercontent.com/598796/102272933-3a0c6800-3f2a-11eb-9fc0-60a10cb341bb.png"> *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
- Loading branch information
Elad Ben-Israel
authored
Dec 24, 2020
1 parent
48ca970
commit 065756e
Showing
7 changed files
with
295 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,33 @@ | ||
import * as colors from 'colors'; | ||
import * as yargs from 'yargs'; | ||
import { upgradeAvailable } from '../upgrades'; | ||
|
||
const args = yargs | ||
.commandDir('cmds') | ||
.recommendCommands() | ||
.wrap(yargs.terminalWidth()) | ||
.showHelpOnFail(false) | ||
.env('CDK8S') | ||
.epilogue('Options can be specified via environment variables with the "CDK8S_" prefix (e.g. "CDK8S_OUTPUT")') | ||
.help() | ||
.argv; | ||
|
||
if (args._.length === 0) { | ||
yargs.showHelp(); | ||
async function main() { | ||
const versions = upgradeAvailable(); | ||
if (versions) { | ||
console.error('------------------------------------------------------------------------------------------------'); | ||
console.error(colors.yellow(`A new version ${versions.latest} of cdk8s-cli is available (current ${versions.current}).`)); | ||
console.error(colors.yellow('Run "npm install -g cdk8s-cli" to install the latest version on your system.')); | ||
console.error(colors.yellow('For additional installation methods, see https://cdk8s.io/docs/latest/getting-started')); | ||
console.error('------------------------------------------------------------------------------------------------'); | ||
} | ||
|
||
const ya = yargs | ||
.commandDir('cmds') | ||
.recommendCommands() | ||
.wrap(yargs.terminalWidth()) | ||
.showHelpOnFail(false) | ||
.env('CDK8S') | ||
.epilogue('Options can be specified via environment variables with the "CDK8S_" prefix (e.g. "CDK8S_OUTPUT")') | ||
.help(); | ||
|
||
const args = ya.argv; | ||
if (args._.length === 0) { | ||
yargs.showHelp(); | ||
} | ||
} | ||
|
||
main().catch(e => { | ||
console.error(e.stack); | ||
process.exit(1); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
// import { shell } from './util'; | ||
import { homedir } from 'os'; | ||
import { join } from 'path'; | ||
import * as cdk8s from 'cdk8s'; | ||
import { readFileSync, statSync, writeFileSync } from 'fs-extra'; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-require-imports | ||
const pkg = require('../package.json'); | ||
|
||
const CACHE = { | ||
cacheTtlSec: 60 * 30, // 30min | ||
cacheFile: join(homedir(), '.cdk8s-cli.version'), | ||
}; | ||
|
||
/** | ||
* Checks if there is a new version of the CLI available on npm. | ||
* | ||
* @returns `undefined` if there is no upgrade available. If there is a new | ||
* version, returns an object with `latest` (the latest version) and `current` | ||
* (the current version). | ||
*/ | ||
export function upgradeAvailable() { | ||
const latest = getLatestVersion(pkg.name, CACHE); | ||
if (latest !== pkg.version) { | ||
return { latest, current: pkg.version }; | ||
} else { | ||
return undefined; | ||
} | ||
} | ||
|
||
/** | ||
* Returns the latest version of an npm module. The version is cached for 30min | ||
* to ~/.cdk8s-cli.version. | ||
* | ||
* Never throws. | ||
* | ||
* @param module The module name | ||
* @param options cache options | ||
*/ | ||
export function getLatestVersion(module: string, options: { cacheFile: string; cacheTtlSec: number }) { | ||
let latest = readCache(); | ||
if (!latest) { | ||
try { | ||
const info = cdk8s.Yaml.load(`http://registry.npmjs.org/${module}/latest`); | ||
if (!info || info.length < 1) { | ||
return undefined; | ||
} | ||
latest = info[0].version; | ||
} catch (e) { | ||
return undefined; | ||
} | ||
|
||
// cannot determine version, return undefined. | ||
if (!latest) { | ||
return undefined; | ||
} | ||
|
||
writeCache(latest); | ||
} | ||
|
||
return latest; | ||
|
||
function readCache(): string | undefined { | ||
try { | ||
const lastModified = statSync(options.cacheFile).mtime; | ||
const diff = (new Date().getTime()) - lastModified.getTime(); | ||
if (diff > options.cacheTtlSec * 1000) { | ||
return undefined; | ||
} | ||
|
||
return readFileSync(options.cacheFile, 'utf-8').trim(); | ||
} catch (e) { | ||
return undefined; | ||
} | ||
} | ||
|
||
function writeCache(version: string) { | ||
try { | ||
writeFileSync(options.cacheFile, version); | ||
} catch (e) { | ||
return; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import * as fs from 'fs-extra'; | ||
import { tmpdir } from 'os'; | ||
import { join } from 'path'; | ||
import { Yaml } from 'cdk8s'; | ||
import { getLatestVersion } from '../src/upgrades'; | ||
|
||
describe('getLatestVersion', () => { | ||
const yamlLoad = jest.spyOn(Yaml, 'load'); | ||
let cacheFile: string; | ||
|
||
beforeEach(() => { | ||
const workdir = fs.mkdtempSync(join(tmpdir(), 'test')); | ||
cacheFile = join(workdir, 'version-cache.txt'); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
test('http get successful with no local cache', async () => { | ||
// GIVEN | ||
yamlLoad.mockReturnValue([{ version: '1.2.3' }]); | ||
|
||
// WHEN | ||
const result = getLatestVersion('dummy-module', { | ||
cacheFile, | ||
cacheTtlSec: 30, | ||
}); | ||
|
||
// THEN | ||
|
||
// getLatestVersion returns the version from npm | ||
expect(result).toBe('1.2.3'); | ||
|
||
// Yaml.load() is called with the correct URL | ||
expect(yamlLoad).toBeCalledWith('http://registry.npmjs.org/dummy-module/latest'); | ||
|
||
// local cache now includes the version | ||
expect(fs.readFileSync(cacheFile, 'utf-8')).toStrictEqual('1.2.3'); | ||
}); | ||
|
||
test('local cache exists and valid', () => { | ||
// GIVEN | ||
fs.writeFileSync(cacheFile, '7.7.7'); | ||
|
||
// WHEN | ||
const result = getLatestVersion('dummy-module', { | ||
cacheFile, | ||
cacheTtlSec: 30, | ||
}); | ||
|
||
// THEN | ||
|
||
// getLatestVersion returns the version from npm | ||
expect(result).toBe('7.7.7'); | ||
|
||
// Yaml.load() should not be called | ||
expect(yamlLoad).not.toBeCalled(); | ||
|
||
// local cache now includes the version | ||
expect(fs.readFileSync(cacheFile, 'utf-8')).toStrictEqual('7.7.7'); | ||
}); | ||
|
||
test('local cache exists and invalid', () => { | ||
// GIVEN | ||
yamlLoad.mockReturnValue([{ version: '43.12.13' }]); | ||
fs.writeFileSync(cacheFile, '88.88.88'); | ||
|
||
// WHEN | ||
const result = getLatestVersion('dummy-module', { | ||
cacheFile, | ||
cacheTtlSec: -1, | ||
}); | ||
|
||
// THEN | ||
|
||
// getLatestVersion returns the version from npm | ||
expect(result).toBe('43.12.13'); | ||
|
||
// Yaml.load() should be called | ||
expect(yamlLoad).toBeCalled(); | ||
|
||
// local cache now includes the version | ||
expect(fs.readFileSync(cacheFile, 'utf-8')).toStrictEqual('43.12.13'); | ||
}); | ||
|
||
test('no local cache, http get failed => undefined', () => { | ||
// GIVEN | ||
yamlLoad.mockImplementation(() => { | ||
throw new Error('unable to download'); | ||
}); | ||
|
||
// WHEN | ||
const result = getLatestVersion('dummy-module', { | ||
cacheFile, | ||
cacheTtlSec: 30, | ||
}); | ||
|
||
// THEN | ||
|
||
// Yaml.load() is called with the correct URL | ||
expect(yamlLoad).toBeCalledWith('http://registry.npmjs.org/dummy-module/latest'); | ||
|
||
// getLatestVersion returns the version from npm | ||
expect(result).toBe(undefined); | ||
|
||
// local cache now includes the version | ||
expect(fs.existsSync(cacheFile)).toBeFalsy(); | ||
}); | ||
|
||
test('npm returns a malformed result', () => { | ||
// GIVEN | ||
yamlLoad.mockReturnValue([{ bboom: 123 }]); | ||
|
||
// WHEN | ||
const result = getLatestVersion('dummy-module', { | ||
cacheFile, | ||
cacheTtlSec: 30, | ||
}); | ||
|
||
// THEN | ||
expect(result).toBeUndefined(); | ||
expect(yamlLoad).toBeCalledWith('http://registry.npmjs.org/dummy-module/latest'); | ||
expect(fs.existsSync(cacheFile)).toBeFalsy(); | ||
}); | ||
|
||
test('fail to write to local cache', () => { | ||
// GIVEN | ||
yamlLoad.mockReturnValue([{ version: '43.12.13' }]); | ||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { | ||
throw new Error('unable to write file'); | ||
}); | ||
|
||
// WHEN | ||
const result = getLatestVersion('dummy-module', { | ||
cacheFile, cacheTtlSec: 30 | ||
}); | ||
|
||
// THEN | ||
expect(fs.existsSync(cacheFile)).toBeFalsy(); | ||
expect(result).toBe('43.12.13'); | ||
}); | ||
|
||
test('fails to download & to write to local cache', () => { | ||
// GIVEN | ||
yamlLoad.mockImplementation(() => { | ||
throw new Error('unable to download'); | ||
}); | ||
|
||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { | ||
throw new Error('unable to write file'); | ||
}); | ||
|
||
// WHEN | ||
const result = getLatestVersion('dummy-module', { | ||
cacheFile, cacheTtlSec: 30 | ||
}); | ||
|
||
// THEN | ||
expect(fs.existsSync(cacheFile)).toBeFalsy(); | ||
expect(result).toBeUndefined(); | ||
}) | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters