diff --git a/src/cli.js b/src/cli.js index 660a45d61..dbb405c60 100644 --- a/src/cli.js +++ b/src/cli.js @@ -13,7 +13,8 @@ import { run, runRequest, context, - folderManagement + folderManagement, + servicepack } from './main' import { fileExists } from './utils/file-utils' import path from 'path' @@ -82,6 +83,10 @@ function getUnaliasedCommand(command) { return 'deploy' } + if (command === 'servicepack') { + return 'servicepack' + } + if (command === 'build-DB' || command === 'DB' || command === 'db') { return 'db' } @@ -176,6 +181,10 @@ export async function cli(args) { ) break } + case 'servicepack': { + await servicepack(command.parameters) + break + } case 'db': { await buildDBs(command.parameters[1]) break diff --git a/src/main.js b/src/main.js index 9722a54d7..eb8f7bf38 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ import { build } from './sasjs-build' import { deploy } from './sasjs-deploy' +import { processServicepack } from './sasjs-servicepack' import { buildDB } from './sasjs-db' import { create } from './sasjs-create' import { printHelpText } from './sasjs-help' @@ -288,6 +289,19 @@ export async function context(command) { ) } +export async function servicepack(command) { + if (!command) + console.log( + chalk.redBright(`Please provide action for the 'servicepack' command.`) + ) + + await processServicepack(command).catch((err) => + console.log( + chalk.redBright('An error has occurred when processing servicepack.', err) + ) + ) +} + export async function folderManagement(command) { if (!command) console.log( diff --git a/src/sasjs-help/index.js b/src/sasjs-help/index.js index c266d2a66..35f37512e 100644 --- a/src/sasjs-help/index.js +++ b/src/sasjs-help/index.js @@ -98,6 +98,21 @@ export async function printHelpText() { NOTE: If no target name is specified/matched, it will build the first target present in config.json. + * ${chalk.greenBright( + 'servicepack ' + )} - performs operations on Service Packs (collections of jobs & folders). + * ${chalk.cyanBright('deploy')} - deploys service pack from json file. + command example: sasjs servicepack deploy -s ./path/services.json -t targetName + command example: sasjs servicepack deploy --source ./path/services.json --target targetName + + NOTE: Providing target name (--target targetName or -t targetName) is optional. + You can force deploy (overwrite an existing deploy) by passing the (-f) flag. + Default target name will be used if target name was omitted. + + NOTE: The sasjs servicepack operation is only supported for SAS Viya build targets. + More information available in the online documentation: https://sasjs.io/sasjs-cli-servicepack + + * ${chalk.greenBright( 'context ' )} - performs operations on contexts. diff --git a/src/sasjs-servicepack/deploy.js b/src/sasjs-servicepack/deploy.js new file mode 100644 index 000000000..4e990a21e --- /dev/null +++ b/src/sasjs-servicepack/deploy.js @@ -0,0 +1,122 @@ +import path from 'path' +import SASjs from '@sasjs/adapter/node' +import chalk from 'chalk' +import { + readFile, + folderExists, + createFile, + createFolder +} from '../utils/file-utils' +import { displayResult } from '../utils/displayResult' +import { + getAccessToken, + findTargetInConfiguration +} from '../utils/config-utils' + +export async function servicePackDeploy( + jsonFilePath = null, + targetName = null, + isForced = false +) { + console.log({ + jsonFilePath, + targetName, + isForced + }) + + if (path.extname(jsonFilePath) !== '.json') { + throw new Error('Provided data file must be valid json.') + } + + const { target, isLocal } = await findTargetInConfiguration(targetName, true) + + if (!target.serverType === 'SASVIYA') { + console.log( + chalk.redBright.bold( + `Deployment failed. This commmand is only available on VIYA servers.` + ) + ) + + return + } + + console.log( + chalk.cyanBright(`Executing deployServicePack to update SAS server.`) + ) + + let output = await deployToSasViyaWithServicePack( + jsonFilePath, + target, + isForced + ) + + let outputPath = path.join(process.cwd(), isLocal ? '/sasjsbuild' : '') + + if (!(await folderExists(outputPath))) { + await createFolder(outputPath) + } + + outputPath += '/output.json' + + await createFile(outputPath, output) + + displayResult( + null, + null, + `Request finished. Output is stored at '${outputPath}'` + ) +} + +async function deployToSasViyaWithServicePack( + jsonFilePath, + buildTarget, + isForced +) { + const sasjs = new SASjs({ + serverUrl: buildTarget.serverUrl, + appLoc: buildTarget.appLoc, + serverType: buildTarget.serverType + }) + + const CONSTANTS = require('../constants') + const buildDestinationFolder = CONSTANTS.buildDestinationFolder + + const finalFilePathJSON = path.join( + buildDestinationFolder, + `${buildTarget.name}.json` + ) + + let jsonContent + + if (jsonFilePath) { + jsonContent = await readFile(path.join(process.cwd(), jsonFilePath)) + } else { + jsonContent = await readFile(finalFilePathJSON) + } + + let jsonObject + + try { + jsonObject = JSON.parse(jsonContent) + } catch (err) { + throw new Error('Provided data file must be valid json.') + } + + const access_token = await getAccessToken(buildTarget) + + if (!access_token) { + console.log( + chalk.redBright.bold( + `Deployment failed. Request is not authenticated.\nRun 'sasjs add' command and provide 'client' and 'secret'.` + ) + ) + } + + return await sasjs.deployServicePack( + jsonObject, + null, + null, + access_token, + isForced + ) +} diff --git a/src/sasjs-servicepack/index.js b/src/sasjs-servicepack/index.js new file mode 100644 index 000000000..160c3de9b --- /dev/null +++ b/src/sasjs-servicepack/index.js @@ -0,0 +1,40 @@ +import { servicePackDeploy } from './deploy' +import { + getCommandParameter, + getCommandParameterLastMultiWord, + isFlagPresent +} from '../utils/command-utils' + +import chalk from 'chalk' + +export async function processServicepack(commandLine) { + const command = commandLine[1] + const commands = { + deploy: 'deploy' + } + + if (!commands.hasOwnProperty(command)) { + console.log( + chalk.redBright( + `Not supported servicepack command. Supported commands are:\n${Object.keys( + commands + ).join('\n')}` + ) + ) + + return + } + + const commandExample = + 'sasjs servicepack --source ../viyadeploy.json --target targetName' + + switch (command) { + case commands.deploy: + let targetName = getCommandParameterLastMultiWord('-t', '--target', commandLine, commandExample) + let jsonFilePath = getCommandParameter('-s', '--source', commandLine, commandExample) + let isForced = isFlagPresent('-f', commandLine) + + servicePackDeploy(jsonFilePath, targetName, isForced) + break + } +} diff --git a/src/utils/command-utils.js b/src/utils/command-utils.js new file mode 100644 index 000000000..27577570b --- /dev/null +++ b/src/utils/command-utils.js @@ -0,0 +1,64 @@ +import chalk from 'chalk' + +export function isFlagPresent(flag, commandLine) { + return commandLine.indexOf(flag) > -1 +} + +export function getCommandParameter( + commandFlag, + commandFlagLong, + commandLine, + commandExample = '' +) { + let parameterValueFlagIndex = commandLine.indexOf(commandFlagLong) + + if (parameterValueFlagIndex === -1) + parameterValueFlagIndex = commandLine.indexOf(commandFlag) + + if (parameterValueFlagIndex === -1) { + const errorMessage = chalk.redBright( + `'${commandFlag || commandFlagLong}' flag is missing. ${ + commandExample ? "(eg '" + commandExample + "')" : '' + }` + ) + throw new Error(errorMessage) + + return + } + + let parameterValue = commandLine[parameterValueFlagIndex + 1] + + return parameterValue +} + +export function getCommandParameterLastMultiWord( + commandFlag, + commandFlagLong, + commandLine, + commandExample = '' +) { + let parameterValue = [] + let parameterFlagIndex = commandLine.indexOf(commandFlagLong) + + if (parameterFlagIndex === -1) + parameterFlagIndex = commandLine.indexOf(commandFlag) + + if (parameterFlagIndex !== -1) { + for (let i = parameterFlagIndex + 1; i < commandLine.length; i++) { + if (commandLine[i].includes('-')) { + const errorMessage = `Parameter '${ + commandFlagLong || commandFlag + }' has to be provided as the last argument ${ + commandExample ? "(eg '" + commandExample + "')" : '' + }` + throw new Error(errorMessage) + } + + parameterValue.push(commandLine[i]) + } + } + + parameterValue = parameterValue.join(' ') + + return parameterValue +} diff --git a/test/command-utils.spec.js b/test/command-utils.spec.js new file mode 100644 index 000000000..a3443b197 --- /dev/null +++ b/test/command-utils.spec.js @@ -0,0 +1,135 @@ +import { + isFlagPresent, + getCommandParameter, + getCommandParameterLastMultiWord +} from '../src/utils/command-utils' + +const mockCommandLine = [ + 'test', + '-f', + '-t2', + 'testParam', + '--test3', + 'testLongParam', + '-l', + 'multi', + 'word' +] + +describe('isFlagPresent', () => { + test('passed existing flag', () => { + let flagPresent = isFlagPresent('-f', mockCommandLine) + + expect(flagPresent).toEqual(true) + }) + + test('passed non existing flag', () => { + let flagPresent = isFlagPresent('-a', mockCommandLine) + + expect(flagPresent).toEqual(false) + }) + + test('passed empty string', () => { + let flagPresent = isFlagPresent('', mockCommandLine) + + expect(flagPresent).toEqual(false) + }) + + test('passed null', () => { + let flagPresent = isFlagPresent(null, mockCommandLine) + + expect(flagPresent).toEqual(false) + }) + + test('passed undefined', () => { + let flagPresent = isFlagPresent(undefined, mockCommandLine) + + expect(flagPresent).toEqual(false) + }) +}) + +describe('getCommandParameter', () => { + test('passed short flag and long flag', () => { + let parameter = getCommandParameter('-t', '--test3', mockCommandLine) + + expect(parameter).toEqual('testLongParam') + }) + + test('passed short flag, without long flag', () => { + let parameter = getCommandParameter('-t2', null, mockCommandLine) + + expect(parameter).toEqual('testParam') + }) + + test('passed long flag, without short flag', () => { + let parameter = getCommandParameter(null, '--test3', mockCommandLine) + + expect(parameter).toEqual('testLongParam') + }) + + test('should throw and error when passed non-existing flags', () => { + expect(() => getCommandParameter('-n', '--non', mockCommandLine)).toThrow() + }) +}) + +describe('getCommandParameterLastMultiWord', () => { + test('passed short flag and long flag', () => { + let parameter = getCommandParameterLastMultiWord( + '-l', + '--last', + mockCommandLine + ) + + expect(parameter).toEqual('multi word') + }) + + test('passed short flag without long flag', () => { + let parameter = getCommandParameterLastMultiWord( + '-l', + null, + mockCommandLine + ) + + expect(parameter).toEqual('multi word') + }) + + test('passed long flag without short flag', () => { + let mockCommandLineCustom = [ + 'test', + '-f', + '-t2', + 'testParam', + '--test3', + 'testLongParam', + '--last', + 'multi', + 'word' + ] + + let parameter = getCommandParameterLastMultiWord( + null, + '--last', + mockCommandLineCustom + ) + + expect(parameter).toEqual('multi word') + }) + + test('throws an error when passed non-existing flags', () => { + let parameter = getCommandParameterLastMultiWord( + '-n', + '--non', + mockCommandLine + ) + + expect(parameter).toEqual('') + }) + + test('should throw and error when argument is not the last one', () => { + const input = ['-t', 'target', '-s', 'source'] + + expect(() => + getCommandParameterLastMultiWord('-t', '--target', input) + ).toThrow() + }) +})