From fcb926ed08aaf7719b42dfb7aa7da180a01393dd Mon Sep 17 00:00:00 2001 From: Eugene Kozlov <68875428+kozlove-aws@users.noreply.github.com> Date: Thu, 8 Oct 2020 11:52:58 -0500 Subject: [PATCH] feat: Update stage-deadline script with new version index (#139) * feat: add version-provider * feat: update stage-deadline script with new version index * fix: move version provider logic in separate script from handler Co-authored-by: Eugene Kozlov --- packages/aws-rfdk/bin/index-test.json | 49 +++ packages/aws-rfdk/bin/stage-deadline.ts | 218 ++++++++---- .../nodejs/version-provider/handler.ts | 59 ++++ .../lambdas/nodejs/version-provider/index.ts | 7 + .../test/version-provider.test.ts | 243 +++++++++++++ .../version-provider/version-provider.ts | 323 ++++++++++++++++++ 6 files changed, 829 insertions(+), 70 deletions(-) create mode 100644 packages/aws-rfdk/bin/index-test.json create mode 100644 packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/handler.ts create mode 100644 packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/index.ts create mode 100644 packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/version-provider.test.ts create mode 100644 packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/version-provider.ts diff --git a/packages/aws-rfdk/bin/index-test.json b/packages/aws-rfdk/bin/index-test.json new file mode 100644 index 000000000..927d7e677 --- /dev/null +++ b/packages/aws-rfdk/bin/index-test.json @@ -0,0 +1,49 @@ +{ + "Deadline": { + "versions": { + "10": { + "1": { + "8": { + "5": { + "linux": { + "bundle": "s3://thinkbox-installers/Deadline/10.1.8.5/Linux/Deadline-10.1.8.5-linux-installers.tar", + "clientInstaller": "s3://thinkbox-installers/Deadline/10.1.8.5/Linux/DeadlineClient-10.1.8.5-linux-x64-installer.run", + "repositoryInstaller": "s3://thinkbox-installers/Deadline/10.1.8.5/Linux/DeadlineRepository-10.1.8.5-linux-x64-installer.run" + }, + "mac": { + "bundle": "s3://thinkbox-installers/Deadline/10.1.8.5/Mac/Deadline-10.1.8.5-osx-installers.dmg" + }, + "windows": { + "bundle": "s3://thinkbox-installers/Deadline/10.1.8.5/Windows/Deadline-10.1.8.5-windows-installers.zip", + "clientInstaller": "s3://thinkbox-installers/Deadline/10.1.8.5/Windows/DeadlineClient-10.1.8.5-windows-installer.exe", + "repositoryInstaller": "s3://thinkbox-installers/Deadline/10.1.8.5/Windows/DeadlineRepository-10.1.8.5-windows-installer.exe" + } + } + }, + "9": { + "2": { + "linux": { + "bundle": "s3://thinkbox-installers/Deadline/10.1.9.2/Linux/Deadline-10.1.9.2-linux-installers.tar", + "clientInstaller": "s3://thinkbox-installers/Deadline/10.1.9.2/Linux/DeadlineClient-10.1.9.2-linux-x64-installer.run", + "repositoryInstaller": "s3://thinkbox-installers/Deadline/10.1.9.2/Linux/DeadlineRepository-10.1.9.2-linux-x64-installer.run" + }, + "mac": { + "bundle": "s3://thinkbox-installers/Deadline/10.1.9.2/Mac/Deadline-10.1.9.2-osx-installers.dmg" + }, + "windows": { + "bundle": "s3://thinkbox-installers/Deadline/10.1.9.2/Windows/Deadline-10.1.9.2-windows-installers.zip", + "clientInstaller": "s3://thinkbox-installers/Deadline/10.1.9.2/Windows/DeadlineClient-10.1.9.2-windows-installer.exe", + "repositoryInstaller": "s3://thinkbox-installers/Deadline/10.1.9.2/Windows/DeadlineRepository-10.1.9.2-windows-installer.exe" + } + } + } + } + } + }, + "latest": { + "linux": "10.1.9.2", + "mac": "10.1.9.2", + "windows": "10.1.8.5" + } + } +} diff --git a/packages/aws-rfdk/bin/stage-deadline.ts b/packages/aws-rfdk/bin/stage-deadline.ts index bbb8fc9a4..fb9d2cdbe 100644 --- a/packages/aws-rfdk/bin/stage-deadline.ts +++ b/packages/aws-rfdk/bin/stage-deadline.ts @@ -12,6 +12,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as url from 'url'; import { types } from 'util'; +import {IUris, Platform, Product, VersionProvider } from '../lib/core/lambdas/nodejs/version-provider'; import { Version } from '../lib/deadline'; const args = process.argv.slice(2); @@ -56,88 +57,138 @@ while (n < args.length) { n++; } -// Automatically populate the installer & recipe URI using the version, if it is provided. -if (deadlineReleaseVersion !== '') { - try { - const version = Version.parse(deadlineReleaseVersion); - if(version.isLessThan(Version.MINIMUM_SUPPORTED_DEADLINE_VERSION)) { - console.error(`ERROR: Unsupported Deadline Version ${version.toString()}. Minimum supported version is ${Version.MINIMUM_SUPPORTED_DEADLINE_VERSION} \n`); - usage(1); - } - } catch(e) { - console.error(`ERROR: ${(e as Error).message} \n`); - usage(1); - } - - // populate installer URI - if (deadlineInstallerURI && deadlineInstallerURI.length > 0) { - console.info('INFO: Since deadline release version is provided, "deadlineInstallerURI" will be ignored.'); - } - deadlineInstallerURI = `s3://thinkbox-installers/Deadline/${deadlineReleaseVersion}/Linux/DeadlineClient-${deadlineReleaseVersion}-linux-x64-installer.run`; - - // populate docker recipe URI - if (dockerRecipesURI && dockerRecipesURI.length > 0) { - console.info('INFO: Since deadline release version is provided, "dockerRecipesURI" will be ignored.'); - } - dockerRecipesURI = `s3://thinkbox-installers/DeadlineDocker/${deadlineReleaseVersion}/DeadlineDocker-${deadlineReleaseVersion}.tar.gz`; +if (!fs.existsSync(outputFolder)) { + fs.mkdirSync(outputFolder); +} else if (fs.readdirSync(outputFolder).length > 0) { + console.error('The target directory is not empty.'); + process.exit(1); } -// Show help if URI for deadline installer or URI for docker is not specified. -if (deadlineInstallerURI === '' || dockerRecipesURI === '') { - usage(1); +const handler = new VersionProvider(); + +// populate installer URI +if (deadlineInstallerURI === '') { + handler.getVersionUris({ platform: Platform.linux, product: Product.deadline, versionString: deadlineReleaseVersion}) + .then(result => { + const installerVersion = result.get(Platform.linux); + if (installerVersion) { + validateDeadlineVersion(`${installerVersion.MajorVersion}.${installerVersion.MinorVersion}.${installerVersion.ReleaseVersion}.${installerVersion.PatchVersion}`); + const installerUrl = (installerVersion.Uris).clientInstaller; + if (installerUrl) { + getDeadlineInstaller(installerUrl); + } + } + else { + console.error(`Deadline installer for version ${deadlineReleaseVersion} was not found.`); + exitAndCleanup(1); + } + }) + .catch(error => { + console.error(error.message); + exitAndCleanup(error.code); + }); +} +else { + getDeadlineInstaller(deadlineInstallerURI); } -const deadlineInstallerURL = new url.URL(deadlineInstallerURI); -const dockerRecipeURL = new url.URL(dockerRecipesURI); -if (deadlineInstallerURL.protocol !== 's3:') { - console.error(`ERROR: Invalid URI protocol ("${deadlineInstallerURL.protocol}") for --deadlineInstallerURI. Only "s3:" URIs are supported.`); - usage(1); +// populate docker recipe URI +if (dockerRecipesURI === '') { + handler.getVersionUris({ platform: Platform.linux, product: Product.deadlineDocker, versionString: deadlineReleaseVersion}) + .then(result => { + const installerVersion = result.get(Platform.linux); + if (installerVersion) { + getDockerRecipe((installerVersion.Uris).bundle); + } + else { + console.error(`Docker recipies for version ${deadlineReleaseVersion} was not found.`); + exitAndCleanup(1); + } + }) + .catch(error => { + console.error(error.message); + exitAndCleanup(error.code); + }); } - -if (dockerRecipeURL.protocol !== 's3:') { - console.error(`ERROR: Invalid URI protocol ("${dockerRecipeURL.protocol}") for --dockerRecipeURL. Only "s3:" URIs are supported.`); - usage(1); +else { + getDockerRecipe(dockerRecipesURI); } -if (!validateBucketName(deadlineInstallerURL.hostname) || !validateBucketName(dockerRecipeURL.hostname)) { - usage(1); -} +/** + * Download Deadline installer + * + * @param deadlineInstallerUri - Specifies a URI pointing to the Deadline Linux Client installer. This currently supports S3 URIs. + */ +function getDeadlineInstaller(deadlineInstallerUri: string) { + const deadlineInstallerURL = new url.URL(deadlineInstallerUri); -if (!fs.existsSync(outputFolder)) { - fs.mkdirSync(outputFolder); -} else if (fs.readdirSync(outputFolder).length > 0) { - console.error('The target directory is not empty.'); - process.exit(1); + if (deadlineInstallerURL.protocol !== 's3:') { + console.error(`ERROR: Invalid URI protocol ("${deadlineInstallerURL.protocol}") for --deadlineInstallerURI. Only "s3:" URIs are supported.`); + usage(1); + } + + if (!validateBucketName(deadlineInstallerURL.hostname)) { + usage(1); + } + + try { + // Get Deadline client installer + const deadlineInstallerPath = getFile({ + uri: deadlineInstallerURL, + targetFolder: path.join(outputFolder, 'bin'), + verbose, + }); + + // Make installer executable + makeExecutable(deadlineInstallerPath); + } catch (e) { + let errorMsg: string; + if (types.isNativeError(e)) { + errorMsg = e.message; + } else { + errorMsg = e.toString(); + } + console.error(`ERROR: ${errorMsg}`); + exitAndCleanup(1); + } } -try { - // Get Docker recipe - getAndExtractArchive({ - uri: dockerRecipeURL, - targetFolder: outputFolder, - verbose, - tarOptions: [`-x${verbose ? 'v' : ''}z`], - }); +/** + * Download and extract Docker recipe. + * + * @param dockerRecipesUri - Specifies a URI pointing to the Deadline Docker recipes. This currently supports S3 URIs. + */ +function getDockerRecipe(dockerRecipesUri: string) { + const dockerRecipeURL = new url.URL(dockerRecipesUri); - // Get Deadline client installer - const deadlineInstallerPath = getFile({ - uri: deadlineInstallerURL, - targetFolder: path.join(outputFolder, 'bin'), - verbose, - }); + if (dockerRecipeURL.protocol !== 's3:') { + console.error(`ERROR: Invalid URI protocol ("${dockerRecipeURL.protocol}") for --dockerRecipeURL. Only "s3:" URIs are supported.`); + usage(1); + } - // Make installer executable - makeExecutable(deadlineInstallerPath); -} catch (e) { - let errorMsg: string; - if (types.isNativeError(e)) { - errorMsg = e.message; - } else { - errorMsg = e.toString(); + if (!validateBucketName(dockerRecipeURL.hostname)) { + usage(1); + } + + try { + // Get Docker recipe + getAndExtractArchive({ + uri: dockerRecipeURL, + targetFolder: outputFolder, + verbose, + tarOptions: [`-x${verbose ? 'v' : ''}z`], + }); + } catch (e) { + let errorMsg: string; + if (types.isNativeError(e)) { + errorMsg = e.message; + } else { + errorMsg = e.toString(); + } + console.error(`ERROR: ${errorMsg}`); + exitAndCleanup(1); } - console.error(`ERROR: ${errorMsg}`); - process.exit(1); } /** @@ -221,8 +272,7 @@ Usage: stage-deadline [--output ] [--verbose] Arguments: - Specifies the official release of Deadline that should be staged. This must be of the form a.b.c.d. - Both '-d' or '-c' arguments will be ignored if provided with this value. + Specifies the official release of Deadline that should be staged. This must be of the form "a.b.c.d", "a.b.c", "a.b" or "a". Note: The minimum supported deadline version is ${Version.MINIMUM_SUPPORTED_DEADLINE_VERSION} @@ -231,11 +281,15 @@ Arguments: s3://thinkbox-installers/Deadline/10.1.x.y/Linux/DeadlineClient-10.1.x.y-linux-x64-installer.run + If this argument is provided will be ignored for Deadline Linux Client. + -c, --dockerRecipesURI Specifies a URI pointing to the Deadline Docker recipes. This currently supports S3 URIs of the form: s3://thinkbox-installers/DeadlineDocker/10.1.x.y/DeadlineDocker-10.1.x.y.tar.gz + If this argument is provided will be ignored for Deadline Docker recipes. + Options: -o, --output Specifies a path to an output directory where Deadline will be staged. The default is to use a "stage" @@ -244,6 +298,16 @@ Options: --verbose Increases the verbosity of the output `.trimLeft()); + exitAndCleanup(errorCode); +} + +/** + * Exit with error code and remove output folder. + * + * @param errorCode - THe code of error that will be returned. + */ +function exitAndCleanup(errorCode: number) { + fs.rmdirSync(outputFolder, {recursive: true}); process.exit(errorCode); } @@ -350,3 +414,17 @@ function getAndExtractArchive(props: GetExtractArchiveProps) { throw new Error(`File ${tarPath} has not been extracted successfully.`); } } + +function validateDeadlineVersion(versionString: string) { + // Automatically populate the installer & recipe URI using the version, if it is provided. + try { + const version = Version.parse(versionString); + if(version.isLessThan(Version.MINIMUM_SUPPORTED_DEADLINE_VERSION)) { + console.error(`ERROR: Unsupported Deadline Version ${version.toString()}. Minimum supported version is ${Version.MINIMUM_SUPPORTED_DEADLINE_VERSION} \n`); + usage(1); + } + } catch(e) { + console.error(`ERROR: ${(e as Error).message} \n`); + usage(1); + } +} diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/handler.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/handler.ts new file mode 100644 index 000000000..b8cca78d9 --- /dev/null +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/handler.ts @@ -0,0 +1,59 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-console */ + +import { LambdaContext } from '../lib/aws-lambda'; +import { CfnRequestEvent, SimpleCustomResource } from '../lib/custom-resource'; +import { + VersionProvider, + IVersionProviderProperties, + IVersionedUris, + Platform, +} from './version-provider'; + + +export class VersionProviderResource extends SimpleCustomResource { + readonly versionProvider: VersionProvider; + + constructor(indexFilePath?: string) { + super(); + this.versionProvider = new VersionProvider(indexFilePath); + } + + /** + * @inheritdoc + */ + /* istanbul ignore next */ // @ts-ignore + public validateInput(data: object): boolean { + return this.versionProvider.implementsIVersionProviderProperties(data); + } + + /** + * @inheritdoc + */ + // @ts-ignore -- we do not use the physicalId + public async doCreate(physicalId: string, resourceProperties: IVersionProviderProperties): Promise> { + return await this.versionProvider.getVersionUris(resourceProperties); + } + + /** + * @inheritdoc + */ + /* istanbul ignore next */ // @ts-ignore + public async doDelete(physicalId: string, resourceProperties: IVersionProviderProperties): Promise { + // Nothing to do -- we don't modify anything. + return; + } +} + +/** + * The handler used to provide the installer links for the requested version + */ +/* istanbul ignore next */ +export async function handler(event: CfnRequestEvent, context: LambdaContext): Promise { + const versionProvider = new VersionProviderResource(); + return await versionProvider.handler(event, context); +} diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/index.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/index.ts new file mode 100644 index 000000000..9b8a8b5eb --- /dev/null +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/index.ts @@ -0,0 +1,7 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './handler'; +export * from './version-provider'; diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/version-provider.test.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/version-provider.test.ts new file mode 100644 index 000000000..6ecebe3b6 --- /dev/null +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/version-provider.test.ts @@ -0,0 +1,243 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-console */ +/* eslint-disable dot-notation */ + +import { Platform, Product, VersionProvider } from '../version-provider'; + +const versionProvider = new VersionProvider('bin/index-test.json'); +const indexTest = versionProvider['readInstallersIndex'](); + +const productSection = indexTest[Product.deadline]; + +test('version parsing', () => { + const result = versionProvider['parseVersionString']('10.1.10.6'); + + expect(result).not.toBeNull(); + + if (result === null) { return; } + expect(result[0]).toEqual('10.1.10.6'); + expect(result[1]).toEqual('10'); + expect(result[2]).toEqual('1'); + expect(result[3]).toEqual('10'); + expect(result[4]).toEqual('6'); +}); + +test('partial version parsing', () => { + const result = versionProvider['parseVersionString']('10.1'); + + expect(result).not.toBeNull(); + + if (result === null) { return; } + expect(result[0]).toEqual('10.1'); + expect(result[1]).toEqual('10'); + expect(result[2]).toEqual('1'); + expect(result[3]).toBeUndefined(); + expect(result[4]).toBeUndefined(); +}); + +test.each(['10.1.9.2.1', '10.', '10.1.', '10.-1', 'a.b.c'])('incorrect version %s parsing', (versionString: string) => { + const result = versionProvider['parseVersionString'](versionString); + expect(result).toBeNull(); +}); + +test.each([[Platform.linux, '10.1.9.2'], + [Platform.mac, '10.1.9.2'], + [Platform.windows, '10.1.8.5'], +])('latest version ', (platform: Platform, versionString: string) => { + const result = versionProvider['getLatestVersion'](platform, productSection); + + expect(result).toEqual(versionString); +}); + +test.each([ + [Platform.linux, { + bundle: 's3://thinkbox-installers/Deadline/10.1.9.2/Linux/Deadline-10.1.9.2-linux-installers.tar', + clientInstaller: 's3://thinkbox-installers/Deadline/10.1.9.2/Linux/DeadlineClient-10.1.9.2-linux-x64-installer.run', + repositoryInstaller: 's3://thinkbox-installers/Deadline/10.1.9.2/Linux/DeadlineRepository-10.1.9.2-linux-x64-installer.run', + } ], + [Platform.windows, { + bundle: 's3://thinkbox-installers/Deadline/10.1.9.2/Windows/Deadline-10.1.9.2-windows-installers.zip', + clientInstaller: 's3://thinkbox-installers/Deadline/10.1.9.2/Windows/DeadlineClient-10.1.9.2-windows-installer.exe', + repositoryInstaller: 's3://thinkbox-installers/Deadline/10.1.9.2/Windows/DeadlineRepository-10.1.9.2-windows-installer.exe', + } ], + [Platform.mac, { + bundle: 's3://thinkbox-installers/Deadline/10.1.9.2/Mac/Deadline-10.1.9.2-osx-installers.dmg', + } ], +])('get Uri for platform', (platform: Platform, versionedUris: any) => { + versionProvider['getUrisForPlatform']( + Product.deadline, + productSection, + platform, + '10.1.9.2', + ).then(result => { + expect(result).not.toBeNull(); + + expect(result?.Uris).toEqual(versionedUris); + }, + ).catch(error => { + process.stderr.write(`${error.toString()}\n`); + process.exit(1); + }); +}); + +test('get deadline version', async () => { + const result = await versionProvider.getVersionUris({ + product: Product.deadline, + platform: Platform.linux, + versionString: '10.1', + }); + + expect(result).not.toBeNull(); + const installerVersion = result.get(Platform.linux); + expect(installerVersion).not.toBeNull(); + + if (result === null) { return; } + expect(installerVersion?.Uris).toEqual({ + bundle: 's3://thinkbox-installers/Deadline/10.1.9.2/Linux/Deadline-10.1.9.2-linux-installers.tar', + clientInstaller: 's3://thinkbox-installers/Deadline/10.1.9.2/Linux/DeadlineClient-10.1.9.2-linux-x64-installer.run', + repositoryInstaller: 's3://thinkbox-installers/Deadline/10.1.9.2/Linux/DeadlineRepository-10.1.9.2-linux-x64-installer.run', + }); + expect(installerVersion?.MajorVersion).toEqual('10'); + expect(installerVersion?.MinorVersion).toEqual('1'); + expect(installerVersion?.ReleaseVersion).toEqual('9'); + expect(installerVersion?.PatchVersion).toEqual('2'); +}); + +test('product is not in file', async () => { + await expect(versionProvider.getVersionUris({ + product: Product.deadlineDocker, + })).rejects.toThrowError(/Information about product DeadlineDocker can't be found/); +}); + +test('get deadline version for all platforms', async () => { + const result = await versionProvider.getVersionUris({ + product: Product.deadline, + }); + + expect(result).not.toBeNull(); + const linuxInstallerVersion = result.get(Platform.linux); + expect(linuxInstallerVersion).not.toBeNull(); + + if (result === null) { return; } + expect(linuxInstallerVersion?.Uris).toEqual({ + bundle: 's3://thinkbox-installers/Deadline/10.1.9.2/Linux/Deadline-10.1.9.2-linux-installers.tar', + clientInstaller: 's3://thinkbox-installers/Deadline/10.1.9.2/Linux/DeadlineClient-10.1.9.2-linux-x64-installer.run', + repositoryInstaller: 's3://thinkbox-installers/Deadline/10.1.9.2/Linux/DeadlineRepository-10.1.9.2-linux-x64-installer.run', + }); + expect(linuxInstallerVersion?.MajorVersion).toEqual('10'); + expect(linuxInstallerVersion?.MinorVersion).toEqual('1'); + expect(linuxInstallerVersion?.ReleaseVersion).toEqual('9'); + expect(linuxInstallerVersion?.PatchVersion).toEqual('2'); + + const macInstallerVersion = result.get(Platform.mac); + expect(macInstallerVersion).not.toBeNull(); + + if (result === null) { return; } + expect(macInstallerVersion?.Uris).toEqual({ + bundle: 's3://thinkbox-installers/Deadline/10.1.9.2/Mac/Deadline-10.1.9.2-osx-installers.dmg', + }); + expect(macInstallerVersion?.MajorVersion).toEqual('10'); + expect(macInstallerVersion?.MinorVersion).toEqual('1'); + expect(macInstallerVersion?.ReleaseVersion).toEqual('9'); + expect(macInstallerVersion?.PatchVersion).toEqual('2'); + + const windowsInstallerVersion = result.get(Platform.windows); + expect(windowsInstallerVersion).not.toBeNull(); + + if (result === null) { return; } + expect(windowsInstallerVersion?.Uris).toEqual({ + bundle: 's3://thinkbox-installers/Deadline/10.1.8.5/Windows/Deadline-10.1.8.5-windows-installers.zip', + clientInstaller: 's3://thinkbox-installers/Deadline/10.1.8.5/Windows/DeadlineClient-10.1.8.5-windows-installer.exe', + repositoryInstaller: 's3://thinkbox-installers/Deadline/10.1.8.5/Windows/DeadlineRepository-10.1.8.5-windows-installer.exe', + }); + expect(windowsInstallerVersion?.MajorVersion).toEqual('10'); + expect(windowsInstallerVersion?.MinorVersion).toEqual('1'); + expect(windowsInstallerVersion?.ReleaseVersion).toEqual('8'); + expect(windowsInstallerVersion?.PatchVersion).toEqual('5'); +}); + +test('validate correct input', async () => { + expect(versionProvider.implementsIVersionProviderProperties({ + product: Product.deadline, + versionString: '10.1.9.2', + platform: 'linux', + })).toBeTruthy(); +}); + +test('validate non-object input', async () => { + expect(versionProvider['implementsIVersionProviderProperties']('test')).toEqual(false); +}); + +test('validate input without product', async () => { + expect(versionProvider.implementsIVersionProviderProperties({ + versionString: 'version', + })).toEqual(false); +}); + +test('validate input with invalid versionString', async () => { + expect(versionProvider.implementsIVersionProviderProperties({ + product: Product.deadline, + versionString: 'version', + })).toEqual(false); +}); + +test('validate input with invalid platform', async () => { + expect(versionProvider['implementsIVersionProviderProperties']({ + product: Product.deadline, + platform: 'test', + })).toEqual(false); +}); + +test('not defined file path', () => { + expect(() => (new VersionProvider())['readInstallersIndex']()).toThrowError(/File path should be defined./); +}); + +test('invalid file path', () => { + expect(() => (new VersionProvider('test.txt'))['readInstallersIndex']()).toThrowError(/File test.txt was not found/); +}); + +test('get latest version without latest section', () => { + expect(() => versionProvider['getLatestVersion']('linux',{})).toThrowError(/Information about latest version can not be found/); +}); + +test('get latest version without informtion for platform', () => { + expect(() => versionProvider['getLatestVersion']('linux',{latest: {}})).toThrowError(/Information about latest version for platform linux can not be found/); +}); + +test('get requested Uri version for existing product.', () => { + const requestedVersion = versionProvider['parseVersionString']('10.1.9.2'); + expect(versionProvider['getRequestedUriVersion'](requestedVersion, { + 10: { + 1: { + 9: { + 2: { + linux: 's3://thinkbox-installers/DeadlineDocker/10.1.9.2/DeadlineDocker-10.1.9.2.tar.gz', + }, + }, + }, + }}, Platform.linux, Product.deadlineDocker )).toEqual({ + MajorVersion: '10', + MinorVersion: '1', + ReleaseVersion: '9', + PatchVersion: '2', + Uris: {bundle: 's3://thinkbox-installers/DeadlineDocker/10.1.9.2/DeadlineDocker-10.1.9.2.tar.gz'}, + }); +}); + +test('get requested Uri version for not existing product.', () => { + const requestedVersion = versionProvider['parseVersionString']('10.1.9.2'); + expect(versionProvider['getRequestedUriVersion'](requestedVersion, { + 10: { + 1: { + 9: { + 2: { + linux: 's3://thinkbox-installers/DeadlineDocker/10.1.9.2/DeadlineDocker-10.1.9.2.tar.gz', + }, + }, + }, + }}, Platform.windows, Product.deadlineDocker )).toEqual(undefined); +}); diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/version-provider.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/version-provider.ts new file mode 100644 index 000000000..6a27d2dd0 --- /dev/null +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/version-provider.ts @@ -0,0 +1,323 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-console */ + +import * as fs from 'fs'; +import * as http from 'http'; +import * as url from 'url'; + +export enum Platform { + linux = 'linux', + + mac = 'mac', + + windows = 'windows', +} + +export enum Product { + deadline = 'Deadline', + + deadlineDocker = 'DeadlineDocker', +} + +export interface IVersionProviderProperties { + readonly versionString?: string + + readonly product: Product; + + readonly platform?: Platform; +} + +export interface IUris { + readonly bundle: string; + + readonly clientInstaller?: string; + + readonly repositoryInstaller?: string; + + readonly certificateInstaller?: string; +} + +export interface IVersionedUris { + /** + * The major version number. + */ + readonly MajorVersion: string; + + /** + * The minor version number. + */ + readonly MinorVersion: string; + + /** + * The release version number. + */ + readonly ReleaseVersion: string; + + /** + * The patch version number. + */ + readonly PatchVersion: string; + + /** + * The URLs to installers + */ + readonly Uris: IUris; +} + +/** + * The version provider parse index JSON which can be downloaded or loaded from local file + * and returns URIs for specific product. + * By default returns the last version of URIs or specified full or partial version. + * If platform is not defined returns URIs for each platform. + */ +export class VersionProvider { + private readonly indexFilePath: string|undefined; + private readonly VALID_VERSION_REGEX = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?$/; + constructor(indexFilePath?: string) { + this.indexFilePath = indexFilePath; + } + + /** + * Returns URIs for specified product + * + * @param resourceProperties + */ + public async getVersionUris(resourceProperties: IVersionProviderProperties): Promise> { + /* istanbul ignore next */ + const indexJson = this.indexFilePath ? this.readInstallersIndex() : await this.downloadInstallerIndex(); + + const productSection = indexJson[resourceProperties.product]; + + if (!productSection) { + throw new Error(`Information about product ${resourceProperties.product} can't be found`); + } + + let installers = new Map(); + if (resourceProperties.platform) { + const versionedUris = await this.getUrisForPlatform( + resourceProperties.product, + productSection, + resourceProperties.platform, + resourceProperties.versionString); + + if (versionedUris) { + installers.set(resourceProperties.platform, versionedUris); + } + } else { + Object.values(Platform).forEach(async p => { + const versionedUris = await this.getUrisForPlatform( + resourceProperties.product, + productSection, + p, + resourceProperties.versionString); + + if (versionedUris) { + installers.set(p, versionedUris); + } + }); + } + + return installers; + } + + public implementsIVersionProviderProperties(value: any): boolean { + if (!value || typeof(value) !== 'object') { return false; } + + if (!value.product || !Object.values(Product).includes(value.product)) { + return false; + } + + if (value.versionString) { + if (null === this.parseVersionString(value.versionString)) { return false; } + } + + if (value.platform) { + if (!Object.values(Platform).includes(value.platform.toLowerCase())) { return false; } + } + + return true; + } + + /* istanbul ignore next */ // @ts-ignore + private async downloadInstallerIndex() { + const productionInfoURL = 'https://downloads.thinkboxsoftware.com/version_info.json'; + + const parsedUrl = url.parse(productionInfoURL); + + const options = { + host: parsedUrl.hostname, + path: parsedUrl.path, + }; + + return new Promise((resolve, reject) => { + http.get(options, (res: http.IncomingMessage) => { + let json = ''; + + res.on('data', (chunk: any) => { + // keep appending the response chunks until we get 'end' event. + json += chunk; + }); + + res.on('end', () => { + // complete response is available here: + if (res.statusCode === 200) { + try { + // convert the response to a json object and return. + const data = JSON.parse(json); + resolve(data); + } catch (e) { + reject(e); + } + } else { + reject(new Error(`Expected status code 200, but got ${res.statusCode}`)); + } + }); + }).on('error', (err: Error) => { + reject(err); + }); + }); + } + + /** + * This method reads index file and return parsed JSON object from this file content. + */ + private readInstallersIndex(): any { + if (!this.indexFilePath) { + throw new Error('File path should be defined.'); + } + if (!fs.existsSync(this.indexFilePath)) { + throw new Error(`File ${this.indexFilePath} was not found`); + } + const data = fs.readFileSync(this.indexFilePath, 'utf8'); + + // convert the response to a json object and return. + const json = JSON.parse(data); + return json; + } + + private parseVersionString(versionString: string): RegExpExecArray | null { + return this.VALID_VERSION_REGEX.exec(versionString); + } + + /** + * This method returns IVersionedUris for specific platform + * + * @param product + * @param productSection + * @param platform + * @param version + */ + private async getUrisForPlatform( + product: Product, + productSection: any, + platform: Platform, + version?: string, + ): Promise { + const versionString: string = version ? version : this.getLatestVersion(platform, productSection); + + const requestedVersion = this.parseVersionString( versionString ); + + // Based on the requested version, fetches the latest patch and its installer file paths. + return this.getRequestedUriVersion( + requestedVersion, + productSection.versions, + platform, + product, + ); + } + + /** + * This method returns the latest version for specified platform. + * + * @param platform + * @param indexedVersionInfo + */ + private getLatestVersion(platform: string, indexedVersionInfo: any): string { + const latestSection = indexedVersionInfo.latest; + if (!latestSection) { + throw new Error('Information about latest version can not be found'); + } + + const latestVersion = latestSection[platform]; + if (!latestVersion) { + throw new Error(`Information about latest version for platform ${platform} can not be found`); + } + + return latestVersion; + } + + /** + * This method looks for the requested version (partial or complete) in the + * indexed version information. Based on the input, it iterates through all + * four numbers in the version string and compares the requested version + * with the indexed info. + * If any of the requested version number is missing, it fetches the latest + * (highest) available version for it. + * + * @param requestedVersion + * @param indexedVersionInfo + */ + private getRequestedUriVersion( + requestedVersion: RegExpExecArray | null, + indexedVersionInfo: any, + platform: Platform, + product: Product, + ): IVersionedUris | undefined { + + let versionMap = indexedVersionInfo; + const versionArray: string[] = []; + + // iterate through all 4 major, minor, release and patch numbers, + // and get the matching version from the indexed version map. + for (let versionIndex = 0; versionIndex < 4; versionIndex++) { + let version: string; + if (requestedVersion?.[versionIndex + 1] == null) { + + // version is not provided, get the max version. + const numberValues: number[] = (Object.keys(versionMap)).map((val: string) => { + return parseInt(val, 10); + }); + version = (Math.max(...numberValues)).toString(); + + } else { + version = requestedVersion[versionIndex + 1]; + } + versionArray[versionIndex] = version; + versionMap = versionMap[version]; + } + + let uriIndex: IUris | undefined; + if ((platform in versionMap)) { + if (product == Product.deadline) { + const platformVersion = versionMap[platform]; + uriIndex = { + bundle: platformVersion.bundle, + clientInstaller: versionMap[platform].clientInstaller, + repositoryInstaller: versionMap[platform].repositoryInstaller, + certificateInstaller: versionMap[platform].certificateInstaller, + }; + + } else { // Product.deadlineDocker + uriIndex = { + bundle: versionMap[platform], + }; + } + } + + if (uriIndex) { + return { + MajorVersion: versionArray[0], + MinorVersion: versionArray[1], + ReleaseVersion: versionArray[2], + PatchVersion: versionArray[3], + Uris: uriIndex, + }; + } else { + return undefined; + } + } +}