diff --git a/app-cli/captainduckduck-deploy.js b/app-cli/captainduckduck-deploy.js deleted file mode 100755 index a0190e2..0000000 --- a/app-cli/captainduckduck-deploy.js +++ /dev/null @@ -1,748 +0,0 @@ -#!/usr/bin/env node -const program = require('commander'); -const fs = require('fs-extra'); -const path = require('path'); -const chalk = require('chalk'); -const inquirer = require('inquirer'); -const configstore = require('configstore'); -const request = require('request'); -const commandExistsSync = require('command-exists').sync; -const ProgressBar = require('progress'); -const ora = require('ora'); -const {exec} = require('child_process'); - -const packagejson = require('./package.json'); - -const configs = new configstore(packagejson.name, { - captainMachines: [], - apps: [] -}); - -const BRANCH_TO_PUSH = 'branchToPush'; -const APP_NAME = 'appName'; -const MACHINE_TO_DEPLOY = 'machineToDeploy'; -const EMPTY_STRING = ''; - -console.log(' '); -console.log(' '); - -program - .description('Deploy current directory to a Captain machine.') - .option('-t, --tarFile ', 'Specify file to be uploaded (rather than using git archive)') - .option('-d, --default', 'Run with default options') - .option('-s, --stateless', 'Run deploy stateless') - .option('-h, --host ', 'Only for stateless mode: Host of the captain machine') - .option('-a, --appName ', 'Only for stateless mode: Name of the app') - .option('-p, --pass ', 'Only for stateless mode: Password for Captain') - .option('-b, --branch ', 'Only for stateless mode: Branch name (default master)') - .parse(process.argv); - - -if (program.args.length) { - console.error(chalk.red('Unrecognized commands:')); - program.args.forEach(function (arg) { - console.log(chalk.red(arg)); - }); - console.error(chalk.red('Deploy does not require any options. ')); - process.exit(1); -} - -function printErrorAndExit(error) { - console.log(chalk.bold.red(error)); - console.log(' '); - console.log(' '); - process.exit(0); -} - -function getSuppliedTarFile() { - return program.tarFile; -} - -if (!getSuppliedTarFile()) { - - if (!fs.pathExistsSync('./.git')) { - printErrorAndExit('**** ERROR: You are not in a git root directory. This command will only deploys the current directory ****'); - } - - if (!fs.pathExistsSync('./captain-definition')) { - printErrorAndExit('**** ERROR: captain-definition file cannot be found. Please see docs! ****'); - } - - const contents = fs.readFileSync('./captain-definition', 'utf8'); - let contentsJson = null; - - try { - contentsJson = JSON.parse(contents); - } catch (e) { - console.log(e); - console.log(''); - printErrorAndExit('**** ERROR: captain-definition file is not a valid JSON! ****'); - } - - if (!contentsJson.schemaVersion) { - printErrorAndExit('**** ERROR: captain-definition needs schemaVersion. Please see docs! ****'); - } - - if (!contentsJson.templateId && !contentsJson.dockerfileLines) { - printErrorAndExit('**** ERROR: captain-definition needs templateId or dockerfileLines. Please see docs! ****'); - } - - if (contentsJson.templateId && contentsJson.dockerfileLines) { - printErrorAndExit('**** ERROR: captain-definition needs templateId or dockerfileLines, NOT BOTH! Please see docs! ****'); - } -} - -let listOfMachines = [{ - name: '-- CANCEL --', - value: EMPTY_STRING, - short: EMPTY_STRING -}]; - -let machines = configs.get('captainMachines'); -for (let i = 0; i < machines.length; i++) { - let m = machines[i]; - listOfMachines.push({ - name: m.name + ' at ' + m.baseUrl, - value: m.name, - short: m.name + ' at ' + m.baseUrl - }) -} - -// Gets default value for propType that is stored in a directory. -// Replaces getAppForDirectory -function getPropForDirectory(propType) { - let apps = configs.get('apps'); - for (let i = 0; i < apps.length; i++) { - let app = apps[i]; - if (app.cwd === process.cwd()) { - return app[propType]; - } - } - return undefined; -} - -// Sets default value for propType that is stored in a directory to propValue. -// Replaces saveAppForDirectory -function savePropForDirectory(propType, propValue) { - let apps = configs.get('apps'); - for (let i = 0; i < apps.length; i++) { - let app = apps[i]; - if (app.cwd === process.cwd()) { - app[propType] = propValue; - configs.set('apps', apps); - return; - } - } - - apps.push({ - cwd: process.cwd(), - [propType]: propValue - }); - - configs.set('apps', apps); -} - - -function getDefaultMachine() { - let machine = getPropForDirectory(MACHINE_TO_DEPLOY); - if (machine) { - return machine.name; - } - if (listOfMachines.length == 2) { - return 1; - } - return EMPTY_STRING; -} - -console.log('Preparing deployment to Captain...'); -console.log(' '); - - -const questions = [ - { - type: 'list', - name: 'captainNameToDeploy', - default: getDefaultMachine(), - message: 'Select the Captain Machine you want to deploy to:', - choices: listOfMachines - }, - { - type: 'input', - default: getPropForDirectory(BRANCH_TO_PUSH) || 'master', - name: BRANCH_TO_PUSH, - message: 'Enter the "git" branch you would like to deploy:', - when: function (answers) { - return !!answers.captainNameToDeploy; - } - }, - { - type: 'input', - default: getPropForDirectory(APP_NAME), - name: APP_NAME, - message: 'Enter the Captain app name this directory will be deployed to:', - when: function (answers) { - return !!answers.captainNameToDeploy; - } - }, - { - type: 'confirm', - name: 'confirmedToDeploy', - message: 'Note that uncommitted files and files in gitignore (if any) will not be pushed to server. Please confirm so that deployment process can start.', - default: true, - when: function (answers) { - return !!answers.captainNameToDeploy; - } - } -]; - -let defaultInvalid = false; - -if (program.default) { - - if (!getDefaultMachine() || !getPropForDirectory(BRANCH_TO_PUSH) || !getPropForDirectory(APP_NAME)) { - console.log('Default deploy failed. Please select deploy options.'); - defaultInvalid = true; - } - else { - console.log('Deploying to ' + getPropForDirectory(MACHINE_TO_DEPLOY).name); - deployTo(getPropForDirectory(MACHINE_TO_DEPLOY), getPropForDirectory(BRANCH_TO_PUSH), getPropForDirectory(APP_NAME)); - } - -} - -const isStateless = program.stateless && program.host && program.appName && program.pass; - -if (isStateless) { - // login first - console.log('Trying to login to', program.host) - requestLoginAuth(program.host, program.pass, function (authToken) { - // deploy - console.log('Starting stateless deploy to', program.host, program.branch, program.appName); - deployTo({ - baseUrl: program.host, - authToken, - }, program.branch || 'master', program.appName); - }); -} -else if (!program.default || defaultInvalid) { - - inquirer.prompt(questions).then(function (answers) { - - console.log(' '); - console.log(' '); - - if (!answers.confirmedToDeploy) { - console.log('Operation cancelled by the user...'); - console.log(' '); - } - else { - let machines = configs.get('captainMachines'); - let machineToDeploy = null; - for (let i = 0; i < machines.length; i++) { - if (machines[i].name === answers.captainNameToDeploy) { - console.log('Deploying to ' + answers.captainNameToDeploy); - machineToDeploy = machines[i]; - break; - } - } - - console.log(' '); - deployTo(machineToDeploy, answers.branchToPush, answers.appName); - } - - }); - -} - -function deployTo(machineToDeploy, branchToPush, appName) { - - function checkAuthAndSendFile(zipFileFullPath, gitHash) { - function isAuthTokenValidCallback(isValid) { - if (isValid) { - sendFileToCaptain(machineToDeploy, zipFileFullPath, appName, gitHash, branchToPush); - } - else { - - requestLogin(machineToDeploy.name, machineToDeploy.baseUrl, function callback(machineToDeployNew) { - - deployTo(machineToDeployNew, branchToPush, appName); - - }); - } - } - - isAuthTokenValid(machineToDeploy, appName, isAuthTokenValidCallback); - } - - if (getSuppliedTarFile()) { - checkAuthAndSendFile(path.join(process.cwd(), getSuppliedTarFile()), 'sendviatarfile'); - return; - } - - if (!commandExistsSync('git')) { - console.log(chalk.red('"git" command not found...')); - console.log(chalk.red('Captain needs "git" to create tar file of your source files...')); - console.log(' '); - process.exit(1); - } - - let zipFileNameToDeploy = 'temporary-captain-to-deploy.tar'; - let zipFileFullPath = path.join(process.cwd(), zipFileNameToDeploy); - - console.log('Saving tar file to:'); - console.log(zipFileFullPath); - console.log(' '); - - try { - fs.removeSync(zipFileFullPath); - } catch (ignoreError) { - - } - - exec('git archive --format tar --output "' + zipFileFullPath + '" ' + branchToPush, function (err, stdout, stderr) { - if (err) { - console.log(chalk.red('TAR file failed')); - console.log(chalk.red(err + ' ')); - console.log(' '); - fs.removeSync(zipFileFullPath); - return; - } - - exec('git rev-parse ' + branchToPush, function (err, stdout, stderr) { - - const gitHash = (stdout || '').trim(); - - if (err || !(/^[a-f0-9]{40}$/.test(gitHash))) { - console.log(chalk.red('Cannot find hash of last commit on this branch: ' + branchToPush)); - console.log(chalk.red(gitHash + ' ')); - console.log(chalk.red(err + ' ')); - console.log(' '); - return; - } - - console.log('Pushing last commit on ' + branchToPush + ': ' + gitHash); - - checkAuthAndSendFile(zipFileFullPath, gitHash); - - }); - }); - -} - -function sendFileToCaptain(machineToDeploy, zipFileFullPath, appName, gitHash, branchToPush) { - - console.log('Uploading file to ' + machineToDeploy.baseUrl); - - const fileSize = fs.statSync(zipFileFullPath).size; - const fileStream = fs.createReadStream(zipFileFullPath); - - const barOpts = { - width: 20, - total: fileSize, - clear: true - }; - const bar = new ProgressBar(' uploading [:bar] :percent (ETA :etas)', barOpts); - fileStream.on('data', function (chunk) { - bar.tick(chunk.length); - }); - - let spinner; - - fileStream.on('end', function () { - console.log(' '); - console.log('This might take several minutes. PLEASE BE PATIENT...'); - spinner = ora('Building your source code...').start(); - spinner.color = 'yellow'; - }); - - - let options = { - url: machineToDeploy.baseUrl + '/api/v1/user/appData/' + appName + '/?detached=1', - headers: { - 'x-namespace': 'captain', - 'x-captain-auth': machineToDeploy.authToken - }, - method: 'POST', - formData: { - sourceFile: fileStream, - gitHash: gitHash - } - }; - - function callback(error, response, body) { - - if (spinner) { - spinner.stop(); - } - - if (fs.pathExistsSync(zipFileFullPath)) { - if (!getSuppliedTarFile()) { - fs.removeSync(zipFileFullPath); - } - } - - try { - - if (!error && response.statusCode === 200) { - - let data = JSON.parse(body); - - if (data.status === 1106) { - // expired token - requestLogin(machineToDeploy.name, machineToDeploy.baseUrl, function callback(machineToDeployNew) { - - deployTo(machineToDeployNew, branchToPush, appName); - - }); - - return; - } - - if (data.status !== 100 && data.status !== 101) { - throw new Error(JSON.stringify(data, null, 2)); - } - - savePropForDirectory(APP_NAME, appName); - savePropForDirectory(BRANCH_TO_PUSH, branchToPush); - savePropForDirectory(MACHINE_TO_DEPLOY, machineToDeploy); - - if (data.status === 100) { - console.log(chalk.green('Deployed successfully: ') + appName); - console.log(' '); - } else if (data.status === 101) { - console.log(chalk.green('Building started: ') + appName); - console.log(' '); - startFetchingBuildLogs(machineToDeploy, appName); - } - - return; - } - - if (error) { - throw new Error(error) - } - - throw new Error(response ? JSON.stringify(response, null, 2) : 'Response NULL'); - - } catch (error) { - - console.error(chalk.red('\nSomething bad happened. Cannot deploy "' + appName + '"\n')); - - if (error.message) { - try { - var errorObj = JSON.parse(error.message); - if (errorObj.status) { - console.error(chalk.red('\nError code: ' + errorObj.status)); - console.error(chalk.red('\nError message:\n\n ' + errorObj.description)); - } else { - throw new Error("NOT API ERROR"); - } - } catch (ignoreError) { - console.error(chalk.red(error.message)); - } - } else { - console.error(chalk.red(error)); - } - console.log(' '); - } - } - - request(options, callback); - -} - -var lastLineNumberPrinted = -10000; // we want to show all lines to begin with! - -function startFetchingBuildLogs(machineToDeploy, appName) { - - let options = { - url: machineToDeploy.baseUrl + '/api/v1/user/appData/' + appName, - headers: { - 'x-namespace': 'captain', - 'x-captain-auth': machineToDeploy.authToken - }, - method: 'GET' - }; - - function onLogRetrieved(data) { - - if (data) { - var lines = data.logs.lines; - var firstLineNumberOfLogs = data.logs.firstLineNumber; - var firstLinesToPrint = 0; - if (firstLineNumberOfLogs > lastLineNumberPrinted) { - - if (firstLineNumberOfLogs < 0) { - // This is the very first fetch, probably firstLineNumberOfLogs is around -50 - firstLinesToPrint = -firstLineNumberOfLogs; - } else { - console.log('[[ TRUNCATED ]]'); - } - - } else { - firstLinesToPrint = lastLineNumberPrinted - firstLineNumberOfLogs; - } - - lastLineNumberPrinted = firstLineNumberOfLogs + lines.length; - - for (var i = firstLinesToPrint; i < lines.length; i++) { - console.log((lines[i] || '').trim()); - } - } - - if (data && !data.isAppBuilding) { - console.log(' '); - if (!data.isBuildFailed) { - console.log(chalk.green('Deployed successfully: ') + appName); - console.log(chalk.magenta('App is available at ') - + (machineToDeploy.baseUrl.replace('//captain.', '//' + appName + '.').replace('https://', 'http://')) - ); - } else { - console.error(chalk.red('\nSomething bad happened. Cannot deploy "' + appName + '"\n')); - } - console.log(' '); - return; - } - - setTimeout(function () { - startFetchingBuildLogs(machineToDeploy, appName); - }, 2000); - } - - - function callback(error, response, body) { - - try { - - if (!error && response.statusCode === 200) { - - let data = JSON.parse(body); - - if (data.status !== 100) { - throw new Error(JSON.stringify(data, null, 2)); - } - - onLogRetrieved(data.data); - - return; - } - - if (error) { - throw new Error(error) - } - - throw new Error(response ? JSON.stringify(response, null, 2) : 'Response NULL'); - - } catch (error) { - - console.error(chalk.red('\nSomething while retrieving app build logs.. "' + error + '"\n')); - - onLogRetrieved(null); - } - } - - request(options, callback); -} - -function isAuthTokenValid(machineToDeploy, appName, isAuthTokenValidCallback) { - - let options = { - url: machineToDeploy.baseUrl + '/api/v1/user/appDefinitions/', - headers: { - 'x-namespace': 'captain', - 'x-captain-auth': machineToDeploy.authToken - }, - method: 'GET' - }; - - - function callback(error, response, body) { - - try { - - if (!error && response.statusCode === 200) { - - let data = JSON.parse(body); - - if (data.status === 1106 || data.status === 1105) { - isAuthTokenValidCallback(false); - } - else { - isAuthTokenValidCallback(true); - } - } - - } catch (error) { - // This is just a sanity check. We only fire FALSE (i.e. expired) if we know it's expired or password is wrong - isAuthTokenValidCallback(true); - } - } - - request(options, callback); -} - -function requestLoginAuth(serverAddress, password, authCallback) { - let options = { - url: serverAddress + '/api/v1/login', - headers: { - 'x-namespace': 'captain' - }, - method: 'POST', - form: { - password: password - } - }; - - function callback(error, response, body) { - - try { - - if (!error && response.statusCode === 200) { - - let data = JSON.parse(body); - - if (data.status !== 100) { - throw new Error(JSON.stringify(data, null, 2)); - } - - authCallback(data.token); - - return; - } - - if (error) { - throw new Error(error) - } - - throw new Error(response ? JSON.stringify(response, null, 2) : 'Response NULL'); - - } catch (error) { - - if (error.message) { - try { - var errorObj = JSON.parse(error.message); - if (errorObj.status) { - console.error(chalk.red('\nError code: ' + errorObj.status)); - console.error(chalk.red('\nError message:\n\n ' + errorObj.description)); - } else { - throw new Error("NOT API ERROR"); - } - } catch (ignoreError) { - console.error(chalk.red(error.message)); - } - } else { - console.error(chalk.red(error)); - } - console.log(' '); - - } - - process.exit(0); - } - - request(options, callback); -} - -function requestLogin(serverName, serverAddress, loginCallback) { - - console.log('Your auth token is not valid anymore. Try to login again.'); - - const questions = [ - { - type: 'password', - name: 'captainPassword', - message: 'Please enter your password for ' + serverAddress, - validate: function (value) { - - if (value && value.trim()) { - return true; - } - - return ('Please enter your password for ' + serverAddress); - } - } - ]; - - function updateAuthTokenInConfigStoreAndReturn(authToken) { - - let machines = configs.get('captainMachines'); - for (let i = 0; i < machines.length; i++) { - if (machines[i].name === serverName) { - var baseUrl = machines[i].authToken = authToken; - configs.set('captainMachines', machines); - console.log('You are now logged back in to ' + serverAddress); - return machines[i]; - } - } - } - - - inquirer.prompt(questions).then(function (passwordAnswers) { - - var password = passwordAnswers.captainPassword; - - let options = { - url: serverAddress + '/api/v1/login', - headers: { - 'x-namespace': 'captain' - }, - method: 'POST', - form: { - password: password - } - }; - - function callback(error, response, body) { - - try { - - if (!error && response.statusCode === 200) { - - let data = JSON.parse(body); - - if (data.status !== 100) { - throw new Error(JSON.stringify(data, null, 2)); - } - - var newMachineToDeploy = updateAuthTokenInConfigStoreAndReturn(data.token); - - loginCallback(newMachineToDeploy); - - return; - } - - if (error) { - throw new Error(error) - } - - throw new Error(response ? JSON.stringify(response, null, 2) : 'Response NULL'); - - } catch (error) { - - if (error.message) { - try { - var errorObj = JSON.parse(error.message); - if (errorObj.status) { - console.error(chalk.red('\nError code: ' + errorObj.status)); - console.error(chalk.red('\nError message:\n\n ' + errorObj.description)); - } else { - throw new Error("NOT API ERROR"); - } - } catch (ignoreError) { - console.error(chalk.red(error.message)); - } - } else { - console.error(chalk.red(error)); - } - console.log(' '); - - } - - process.exit(0); - } - - request(options, callback); - - }); - -} diff --git a/app-cli/captainduckduck-list.js b/app-cli/captainduckduck-list.js deleted file mode 100644 index dc3d049..0000000 --- a/app-cli/captainduckduck-list.js +++ /dev/null @@ -1,40 +0,0 @@ -const program = require('commander'); -const fs = require('fs'); -const path = require('path'); -const chalk = require('chalk'); -const inquirer = require('inquirer'); -const configstore = require('configstore'); -const request = require('request'); - -const packagejson = require('./package.json'); - - -const configs = new configstore(packagejson.name, { - captainMachines: [] -}); - - -program - .description('List all Captain machines currently logged in.') - .parse(process.argv); - -if (program.args.length) { - console.error(chalk.red('Unrecognized commands:')); - program.args.forEach(function (arg) { - console.log(chalk.red(arg)); - }); - console.error(chalk.red('List does not require any options. ')); - process.exit(1); -} -console.log(); -console.log('Logged in Captain Machines:'); -console.log(); -let machines = configs.get('captainMachines'); -for (let i = 0; i < machines.length; i++) { - - console.log('>> ' + chalk.greenBright(machines[i].name) + ' at ' + chalk.cyan(machines[i].baseUrl)); - -} -console.log(); - - diff --git a/app-cli/captainduckduck-login.js b/app-cli/captainduckduck-login.js deleted file mode 100644 index 2e9ac23..0000000 --- a/app-cli/captainduckduck-login.js +++ /dev/null @@ -1,225 +0,0 @@ -const program = require('commander'); -const fs = require('fs'); -const path = require('path'); -const chalk = require('chalk'); -const inquirer = require('inquirer'); -const configstore = require('configstore'); -const request = require('request'); - -const packagejson = require('./package.json'); - - -const configs = new configstore(packagejson.name, { - captainMachines: [] -}); - - -program - .description('Login to a CaptainDuckDuck machine. You can be logged in to multiple machines simultaneously.') - .parse(process.argv); - - -function cleanUpUrl(url) { - if (!url) { - return url; - } - url = url.trim(); - url = url.replace('http://', '').replace('https://', ''); - if (!url || !url.length) { - return url; - } - - if (url.substr(url.length - 1, 1) === '/') { - url = url.substr(0, url.length - 1); - } - return url; -} - - -if (program.args.length) { - console.error(chalk.red('Unrecognized commands:')); - program.args.forEach(function (arg) { - console.log(chalk.red(arg)); - }); - console.error(chalk.red('Login does not require any options. ')); - process.exit(1); -} - -function findDefaultCaptainName() { - - let machines = configs.get('captainMachines'); - let currentSuffix = machines.length + 1; - - function getCaptainFullName(suffix) { - if (suffix < 10) { - suffix = '0' + suffix; - } - return 'captain-' + suffix; - } - - function isSuffixValid(suffixNumber) { - for (let i = 0; i < machines.length; i++) { - let m = machines[i]; - if (m.name === getCaptainFullName(suffixNumber)) { - return false; - } - } - return true; - } - - while (!isSuffixValid(currentSuffix)) { - currentSuffix++; - } - - return getCaptainFullName(currentSuffix); -} - -console.log('Login to a Captain Machine'); - -const SAMPLE_DOMAIN = 'captain.captainroot.yourdomain.com'; - -const questions = [ - { - type: 'input', - default: SAMPLE_DOMAIN, - name: 'captainAddress', - message: 'Enter address of the Captain machine. \nIt is captain.[your-captain-root-domain] :', - validate: function (value) { - - if (value===SAMPLE_DOMAIN){ - return 'Enter a valid URL'; - } - - if (!cleanUpUrl(value)) { - return 'This is an invalid URL: ' + value; - } - - let machines = configs.get('captainMachines'); - for (let i = 0; i < machines.length; i++) { - if (cleanUpUrl(machines[i].baseUrl) === cleanUpUrl(value)) { - return value + ' already exist as ' + machines[i].name + '. If you want to replace the existing entry, you have to first use command, and then re-login.' - } - } - - if (value && value.trim()) { - return true; - } - - return 'Please enter a valid address.'; - } - }, - { - type: 'confirm', - name: 'captainHasRootSsl', - message: 'Is HTTPS activated for this Captain machine?', - default: true - }, - { - type: 'password', - name: 'captainPassword', - message: 'Enter your password:', - validate: function (value) { - - if (value && value.trim()) { - return true; - } - - return 'Please enter your password.'; - } - }, - { - type: 'input', - name: 'captainName', - message: 'Enter a name for this Captain machine:', - default: findDefaultCaptainName(), - validate: function (value) { - - let machines = configs.get('captainMachines'); - for (let i = 0; i < machines.length; i++) { - if (machines[i].name === value) { - return value + ' already exist. If you want to replace the existing entry, you have to first use command, and then re-login.' - } - } - - if (value.match(/^[-\d\w]+$/i)) { - return true; - } - - return 'Please enter a Captain Name.'; - } - } -]; - -inquirer.prompt(questions).then(function (answers) { - - - console.log(' '); - console.log(' '); - - let baseUrl = (answers.captainHasRootSsl ? 'https://' : 'http://') + cleanUpUrl(answers.captainAddress); - - let options = { - url: baseUrl + '/api/v1/login', - headers: { - 'x-namespace': 'captain' - }, - method: 'POST', - form: { - password: answers.captainPassword - } - }; - - function setCaptainMachine(data) { - let machines = configs.get('captainMachines'); - machines.push(data); - configs.set('captainMachines', machines); - } - - function callback(error, response, body) { - - let captainNameEnterByUser = answers.captainName; - - try { - - if (!error && response.statusCode === 200) { - - let data = JSON.parse(body); - - if (data.status !== 100) { - throw new Error(JSON.stringify(data, null, 2)); - } - - console.log(chalk.green('Logged in successfully to ') + baseUrl); - console.log(chalk.green('Authorization token is now saved as ' + captainNameEnterByUser)); - console.log(' '); - - - setCaptainMachine({ - authToken: data.token, - baseUrl: baseUrl, - name: captainNameEnterByUser - }); - - return; - } - - if (error) { - throw new Error(error) - } - - throw new Error(response ? JSON.stringify(response, null, 2) : 'Response NULL'); - - } catch (error) { - console.error(chalk.red('Something bad happened. Cannot save "' + captainNameEnterByUser + '"')); - let errorMessage = error.message ? error.message : error; - console.error(chalk.red(errorMessage)); - console.log(' '); - } - } - - request(options, callback); - -}); - - - diff --git a/app-cli/captainduckduck-logout.js b/app-cli/captainduckduck-logout.js deleted file mode 100644 index 0ae3d1b..0000000 --- a/app-cli/captainduckduck-logout.js +++ /dev/null @@ -1,91 +0,0 @@ -const program = require('commander'); -const fs = require('fs'); -const path = require('path'); -const chalk = require('chalk'); -const inquirer = require('inquirer'); -const configstore = require('configstore'); -const request = require('request'); - -const packagejson = require('./package.json'); - - -const configs = new configstore(packagejson.name, { - captainMachines: [] -}); - - -program - .description('Logout from a specific Captain machine.') - .parse(process.argv); - - -if (program.args.length) { - console.error(chalk.red('Unrecognized commands:')); - program.args.forEach(function (arg) { - console.log(chalk.red(arg)); - }); - console.error(chalk.red('Logout does not require any options. ')); - process.exit(1); -} - -let listOfMachines = [{ - name: '-- CANCEL --', - value: '', - short: '' -}]; -let machines = configs.get('captainMachines'); -for (let i = 0; i < machines.length; i++) { - let m = machines[i]; - listOfMachines.push({ - name: m.name + ' at ' + m.baseUrl, - value: m.name, - short: m.name + ' at ' + m.baseUrl - }) -} - -console.log('Logout from a Captain Machine and clear auth info'); - -const questions = [ - { - type: 'list', - name: 'captainNameToLogout', - message: 'Select the Captain Machine you want to logout from:', - choices: listOfMachines - }, - { - type: 'confirm', - name: 'confirmedToLogout', - message: 'Are you sure you want to logout from this Captain machine?', - default: false, - when: function (answers) { - return !!answers.captainNameToLogout; - } - } -]; - -inquirer.prompt(questions).then(function (answers) { - - console.log(' '); - console.log(' '); - - if (!answers.captainNameToLogout) { - console.log('Operation cancelled by the user...'); - } - else { - let machines = configs.get('captainMachines'); - for (let i = 0; i < machines.length; i++) { - if (machines[i].name === answers.captainNameToLogout) { - var baseUrl = machines[i].baseUrl; - machines.splice(i,1); - configs.set('captainMachines', machines); - console.log('You are now logged out from ' + answers.captainNameToLogout + ' at ' + baseUrl + '...'); - break; - } - } - } - console.log(' '); - -}); - - - diff --git a/app-cli/captainduckduck-serversetup.js b/app-cli/captainduckduck-serversetup.js deleted file mode 100755 index acb5cae..0000000 --- a/app-cli/captainduckduck-serversetup.js +++ /dev/null @@ -1,466 +0,0 @@ -const program = require('commander'); -const fs = require('fs'); -const path = require('path'); -const chalk = require('chalk'); -const inquirer = require('inquirer'); -const configstore = require('configstore'); -const request = require('request'); -const spinnerUtil = require('./utils/spinner'); - -const packagejson = require('./package.json'); - - -const configs = new configstore(packagejson.name, { - captainMachines: [] -}); - - -program - .description('Easy setup for your Captain server.') - .parse(process.argv); - - -if (program.args.length) { - console.error(chalk.red('Unrecognized commands:')); - program.args.forEach(function (arg) { - console.log(chalk.red(arg)); - }); - console.error(chalk.red('This command does not require any options. ')); - process.exit(1); -} - -function findDefaultCaptainName() { - - let machines = configs.get('captainMachines'); - let currentSuffix = machines.length + 1; - - function getCaptainFullName(suffix) { - if (suffix < 10) { - suffix = '0' + suffix; - } - return 'captain-' + suffix; - } - - function isSuffixValid(suffixNumber) { - for (let i = 0; i < machines.length; i++) { - let m = machines[i]; - if (m.name === getCaptainFullName(suffixNumber)) { - return false; - } - } - return true; - } - - while (!isSuffixValid(currentSuffix)) { - currentSuffix++; - } - - return getCaptainFullName(currentSuffix); -} - -function isIpAddress(ipaddress) { - if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ipaddress)) { - return (true) - } - return (false) -} - - -console.log(' '); -console.log('Setup your Captain server'); -console.log(' '); - -const SAMPLE_IP = '123.123.123.123'; - -var authTokenFromLogin = null; -var ipAddressOfServer = null; -var customDomainFromUser = null; -var newPasswordFirstTry = null; -var oldPassword = 'captain42'; - -function setCustomDomain(baseApiUrl, customDomain) { - - return apiEndpointWithLoading('Changing Domain...')('POST', baseApiUrl, '/api/v1/user/system/changerootdomain/', { - rootDomain: customDomain - }); -} - -function enableHttps(baseApiUrl, emailAddress) { - - return apiEndpointWithLoading('Enabling SSL...')('POST', baseApiUrl, '/api/v1/user/system/enablessl/', { - emailAddress: emailAddress - }); - -} - -function changePass(baseApiUrl, newPass) { - - return apiEndpointWithLoading('Changing Password...')('POST', baseApiUrl, '/api/v1/user/changepassword/', { - oldPassword: oldPassword, - newPassword: newPass - }); - -} - -function forceHttps(baseApiUrl) { - - return apiEndpointWithLoading('Forcing SSL...')('POST', baseApiUrl, '/api/v1/user/system/forcessl/', { - isEnabled: true - }); -} - -// returns promise that resolves to auth token -// rejects with ErrorCode or null -function login(baseApiUrl, password) { - - return apiEndpointWithLoading('Login...')('POST', baseApiUrl, '/api/v1/login', { - password: password - }).then(function (data) { - return data.token; - }) - -} - -function apiEndpointWithLoading(message) { - return (method, baseApiUrl, endpoint, dataToSend) => { - return apiEndpoint(method, baseApiUrl, endpoint, dataToSend, message) - } -} - -function apiEndpoint(method, baseApiUrl, endpoint, dataToSend, message) { - - let options = { - url: baseApiUrl + endpoint, - headers: { - 'x-namespace': 'captain', - 'x-captain-auth': authTokenFromLogin - }, - method: method, - form: dataToSend - }; - - return new Promise(function (res, rej) { - - let spinner; - if (message) { - spinner = spinnerUtil.start(message); - } - var callback = function (error, response, body) { - try { - - if (!error && response.statusCode === 200) { - - let data = JSON.parse(body); - - if (data.status !== 100) { - if (spinner) { - spinnerUtil.fail(spinner); - } - rej(data); - return; - } - if (spinner) { - spinnerUtil.succeed(spinner); - } - res(data); - - return; - } - - if (error) { - throw new Error(error) - } - - throw new Error(response ? JSON.stringify(response, null, 2) : 'Response NULL'); - - } catch (error) { - if (spinner) { - spinnerUtil.fail(spinner); - } - console.error(chalk.red('Something bad happened. Cannot connect to "' + baseApiUrl + endpoint + '"')); - let errorMessage = error.message ? error.message : error; - console.error(chalk.red(errorMessage)); - console.log(' '); - process.exit(0); - } - } - - request(options, callback); - - }); -} - -const questions = [ - { - type: 'list', - name: 'hasInstalledCaptain', - message: 'Have you already installed Captain on your server by running the following line:' - + '\nmkdir /captain && docker run -v /var/run/docker.sock:/var/run/docker.sock dockersaturn/captainduckduck ?', - default: 'Yes', - choices: ['Yes', 'No'], - filter: function (value) { - return new Promise(function (res, rej) { - - if (value === 'Yes') { - res(true); - return; - } - - console.log(''); - console.log(''); - console.log(' Cannot start the setup process if Captain is not installed.'); - console.log(' Please read tutorial on CaptainDuckDuck.com' - + ' to learn how to install CaptainDuckDuck on a server.'); - console.log(''); - - process.exit(0); - }); - } - }, - { - type: 'input', - default: SAMPLE_IP, - name: 'captainAddress', - message: 'Enter IP address of your captain server:', - filter: function (value) { - return new Promise(function (res, rej) { - - if (value === SAMPLE_IP) { - rej('Enter a valid IP Address'); - return; - } - - if (!isIpAddress(value.trim())) { - rej('This is an invalid IP Address: ' + value); - return; - } - - ipAddressOfServer = value.trim(); - - //login using captain42. - - return login('http://' + ipAddressOfServer + ':3000', oldPassword) - .then(function (authTokenFetched) { - authTokenFromLogin = authTokenFetched; - res(ipAddressOfServer); - }) - .catch(function (error) { - - // if error is anything but password wrong, exit here... - if (error.status == 1105) { - authTokenFromLogin = null; - res(ipAddressOfServer); - return; - } - - console.log(''); - - if (error.status) { - console.log(chalk.red('Error: ' + error.status)); - console.log(chalk.red('Error: ' + error.description)); - } else { - console.log(chalk.red('Error: ' + error)); - } - - process.exit(0); - - - }); - }) - } - }, - { - type: 'password', - name: 'captainOriginalPassword', - message: 'Enter your current password:', - when: function () { - return !authTokenFromLogin; - }, - filter: function (value) { - return new Promise(function (res, rej) { - - console.log(''); - - return login('http://' + ipAddressOfServer + ':3000', value) - .then(function (authTokenFetched) { - authTokenFromLogin = authTokenFetched; - oldPassword = value; - res(value); - }) - .catch(function (error) { - console.log(''); - - if (error.status) { - console.log(chalk.red('Error: ' + error.status)); - console.log(chalk.red('Error: ' + error.description)); - } else { - console.log(chalk.red('Error: ' + error)); - } - - process.exit(0); - }); - }) - } - }, - { - type: 'input', - name: 'captainRootDomain', - message: 'Enter a root domain for this Captain server. For example, enter test.yourdomain.com if you' + - ' setup your DNS to point *.test.yourdomain.com to ip address of your server' + ': ', - filter: function (value) { - return new Promise(function (res, rej) { - - value = value.trim(); - - customDomainFromUser = 'captain.' + value; - - return setCustomDomain('http://' + ipAddressOfServer + ':3000', value) - .then(function () { - res(customDomainFromUser); - }) - .catch(function (error) { - console.log(''); - if (error.status) { - console.log(chalk.red('Error: ' + error.status)); - console.log(chalk.red('Error: ' + error.description)); - } else { - console.log(chalk.red('Error: ' + error)); - } - process.exit(0); - }); - }) - } - }, - { - type: 'input', - name: 'emailAddress', - message: 'Enter your "valid" email address to enable HTTPS: ', - filter: function (value) { - return new Promise(function (res, rej) { - - console.log(''); - value = value.trim(); - - return enableHttps('http://' + customDomainFromUser, value) - .then(function () { - return forceHttps('https://' + customDomainFromUser); - }) - .then(function () { - res(value); - }) - .catch(function (error) { - console.log(''); - if (error.status) { - console.log(chalk.red('Error: ' + error.status)); - console.log(chalk.red('Error: ' + error.description)); - } else { - console.log(chalk.red('Error: ' + error)); - } - - process.exit(0); - }); - }) - } - }, - { - type: 'password', - name: 'newPasswordFirstTry', - message: 'Enter a new password:', - filter: function (value) { - return new Promise(function (res, rej) { - - newPasswordFirstTry = value; - res(value); - - }) - } - }, - { - type: 'password', - name: 'newPassword', - message: 'Enter a new password:', - filter: function (value) { - return new Promise(function (res, rej) { - - if (newPasswordFirstTry !== value) { - rej('Passwords do not match'); - //process.exit(0); - return; - } - - return changePass('https://' + customDomainFromUser, value) - .then(function () { - return login('https://' + customDomainFromUser, value) - }) - .then(function (token) { - authTokenFromLogin = token; - res(value); - }) - .catch(function (error) { - console.log(''); - if (error.status) { - console.log(chalk.red('Error: ' + error.status)); - console.log(chalk.red('Error: ' + error.description)); - } else { - console.log(chalk.red('Error: ' + error)); - } - - console.log(''); - console.log(chalk.red('IMPORTANT!! Server setup is completed by password is not changed.')); - console.log(chalk.red('You CANNOT use serversetup anymore. To continue: ')); - console.log(chalk.red('- Go to https://' + customDomainFromUser - + ' login with default password and change the password in settings.')); - console.log(chalk.red('- In terminal (here), type captainduckduck login and enter this as your root domain: ' + customDomainFromUser)); - - process.exit(0); - }); - }) - } - }, - { - type: 'input', - name: 'captainName', - message: 'Enter a name for this Captain machine:', - default: findDefaultCaptainName(), - validate: function (value) { - - let machines = configs.get('captainMachines'); - for (let i = 0; i < machines.length; i++) { - if (machines[i].name === value) { - return value + ' already exist. If you want to replace the existing entry, you have to first use command, and then re-login.' - } - } - - if (value.match(/^[-\d\w]+$/i)) { - return true; - } - - return 'Please enter a Captain Name.'; - } - } -]; - -inquirer.prompt(questions).then(function (answers) { - - - var captainAddress = 'https://' + customDomainFromUser; - let machines = configs.get('captainMachines'); - machines.push({ - authToken: authTokenFromLogin, - baseUrl: captainAddress, - name: answers.captainName - }); - configs.set('captainMachines', machines); - - console.log(' '); - console.log(' '); - console.log('Captain is available at ' + captainAddress); - console.log(' '); - console.log('For more details and docs see http://www.captainduckduck.com'); - console.log(' '); - console.log(' '); - -}); - - - diff --git a/app-cli/captainduckduck.js b/app-cli/captainduckduck.js index 54f933d..3a3b075 100755 --- a/app-cli/captainduckduck.js +++ b/app-cli/captainduckduck.js @@ -1,22 +1,48 @@ #!/usr/bin/env node -const packagejson = require('./package.json'); -const updateNotifier = require('update-notifier'); +const packagejson = require("./package.json") +const updateNotifier = require("update-notifier") -updateNotifier({ pkg: packagejson }).notify({ isGlobal: true }); +updateNotifier({ pkg: packagejson }).notify({ isGlobal: true }) +const program = require("commander") +const fs = require("fs") +const path = require("path") +const chalk = require("chalk") +// Command actions +const list = require("./lib/list") +const logout = require("./lib/logout") -const program = require('commander'); -const fs = require('fs'); -const path = require('path'); -const chalk = require('chalk'); +// Setup +program.version(packagejson.version + "").description(packagejson.description) program - .version(packagejson.version + '') - .description(packagejson.description) - .command('serversetup', 'Performs necessary actions and prepares your Captain server.') - .command('login', 'Login to a CaptainDuckDuck machine. You can be logged in to multiple machines simultaneously.') - .command('logout', 'Logout from a specific Captain machine.') - .command('list', 'List all Captain machines currently logged in.') - .command('deploy', 'Deploy your app (current directory) to a specific Captain machine. You\'ll be prompted to choose your Captain machine.') - .parse(process.argv); + .command( + "serversetup", + "Performs necessary actions and prepares your Captain server." + ) + .command( + "login", + "Login to a CaptainDuckDuck machine. You can be logged in to multiple machines simultaneously." + ) + .command( + "deploy", + "Deploy your app (current directory) to a specific Captain machine. You'll be prompted to choose your Captain machine." + ) + +// Commands +program + .command("logout") + .description("Logout from a specific Captain machine.") + .action(() => { + logout() + }) + +program + .command("list") + .description("List all Captain machines currently logged in.") + .action(() => { + list() + }) + +program.parse(process.argv) diff --git a/app-cli/lib/deploy.js b/app-cli/lib/deploy.js new file mode 100644 index 0000000..7fba8be --- /dev/null +++ b/app-cli/lib/deploy.js @@ -0,0 +1,440 @@ +#!/usr/bin/env node +const program = require("commander") +const fs = require("fs-extra") +const path = require("path") +const chalk = require("chalk") +const inquirer = require("inquirer") +const configstore = require("configstore") +const request = require("request") +const commandExistsSync = require("command-exists").sync +const { exec } = require("child_process") +const { sendFileToCaptain } = require("./utils/fileHandler") +const { + requestLogin, + requestLoginAuth, + isAuthTokenValid +} = require("./utils/loginHandler") +const { + validateIsGitRepository, + validateDefinitionFile +} = require("./utils/validationsHandler") +const { printErrorAndExit } = require("./utils/errorHandler") +const packagejson = require("./package.json") +const configs = new configstore(packagejson.name, { + captainMachines: [], + apps: [] +}) +const BRANCH_TO_PUSH = "branchToPush" +const APP_NAME = "appName" +const MACHINE_TO_DEPLOY = "machineToDeploy" +const EMPTY_STRING = "" + +console.log("\n") + +program + .description("Deploy current directory to a Captain machine.") + .option( + "-t, --tarFile ", + "Specify file to be uploaded (rather than using git archive)" + ) + .option("-d, --default", "Run with default options") + .option("-s, --stateless", "Run deploy stateless") + .option( + "-h, --host ", + "Only for stateless mode: Host of the captain machine" + ) + .option("-a, --appName ", "Only for stateless mode: Name of the app") + .option("-p, --pass ", "Only for stateless mode: Password for Captain") + .option( + "-b, --branch ", + "Only for stateless mode: Branch name (default master)" + ) + .parse(process.argv) + +if (program.args.length) { + console.error(chalk.red("Unrecognized commands:")) + program.args.forEach(function(arg) { + console.log(chalk.red(arg)) + }) + console.error(chalk.red("Deploy does not require any options. ")) + process.exit(1) +} + +function getSuppliedTarFile() { + return program.tarFile +} + +if (!getSuppliedTarFile()) { + validateIsGitRepository() + + validateDefinitionFile() + + const contents = fs.readFileSync("./captain-definition", "utf8") + let contentsJson = null + + try { + contentsJson = JSON.parse(contents) + } catch (e) { + console.log(e) + console.log("") + printErrorAndExit( + "**** ERROR: captain-definition file is not a valid JSON! ****" + ) + } + + if (!contentsJson.schemaVersion) { + printErrorAndExit( + "**** ERROR: captain-definition needs schemaVersion. Please see docs! ****" + ) + } + + if (!contentsJson.templateId && !contentsJson.dockerfileLines) { + printErrorAndExit( + "**** ERROR: captain-definition needs templateId or dockerfileLines. Please see docs! ****" + ) + } + + if (contentsJson.templateId && contentsJson.dockerfileLines) { + printErrorAndExit( + "**** ERROR: captain-definition needs templateId or dockerfileLines, NOT BOTH! Please see docs! ****" + ) + } +} + +let listOfMachines = [ + { + name: "-- CANCEL --", + value: EMPTY_STRING, + short: EMPTY_STRING + } +] + +let machines = configs.get("captainMachines") +for (let i = 0; i < machines.length; i++) { + let m = machines[i] + listOfMachines.push({ + name: m.name + " at " + m.baseUrl, + value: m.name, + short: m.name + " at " + m.baseUrl + }) +} + +// Gets default value for propType that is stored in a directory. +// Replaces getAppForDirectory +function getPropForDirectory(propType) { + let apps = configs.get("apps") + for (let i = 0; i < apps.length; i++) { + let app = apps[i] + if (app.cwd === process.cwd()) { + return app[propType] + } + } + return undefined +} + +function getDefaultMachine() { + let machine = getPropForDirectory(MACHINE_TO_DEPLOY) + if (machine) { + return machine.name + } + if (listOfMachines.length == 2) { + return 1 + } + return EMPTY_STRING +} + +console.log("Preparing deployment to Captain...\n") + +const questions = [ + { + type: "list", + name: "captainNameToDeploy", + default: getDefaultMachine(), + message: "Select the Captain Machine you want to deploy to:", + choices: listOfMachines + }, + { + type: "input", + default: getPropForDirectory(BRANCH_TO_PUSH) || "master", + name: BRANCH_TO_PUSH, + message: "Enter the 'git' branch you would like to deploy:", + when: function(answers) { + return !!answers.captainNameToDeploy + } + }, + { + type: "input", + default: getPropForDirectory(APP_NAME), + name: APP_NAME, + message: "Enter the Captain app name this directory will be deployed to:", + when: function(answers) { + return !!answers.captainNameToDeploy + } + }, + { + type: "confirm", + name: "confirmedToDeploy", + message: + "Note that uncommitted files and files in gitignore (if any) will not be pushed to server. Please confirm so that deployment process can start.", + default: true, + when: function(answers) { + return !!answers.captainNameToDeploy + } + } +] + +let defaultInvalid = false + +if (program.default) { + if ( + !getDefaultMachine() || + !getPropForDirectory(BRANCH_TO_PUSH) || + !getPropForDirectory(APP_NAME) + ) { + console.log("Default deploy failed. Please select deploy options.") + defaultInvalid = true + } else { + console.log("Deploying to " + getPropForDirectory(MACHINE_TO_DEPLOY).name) + deployTo( + getPropForDirectory(MACHINE_TO_DEPLOY), + getPropForDirectory(BRANCH_TO_PUSH), + getPropForDirectory(APP_NAME) + ) + } +} + +const isStateless = + program.stateless && program.host && program.appName && program.pass + +if (isStateless) { + // login first + console.log("Trying to login to", program.host) + requestLoginAuth(program.host, program.pass, function(authToken) { + // deploy + console.log( + "Starting stateless deploy to", + program.host, + program.branch, + program.appName + ) + deployTo( + { + baseUrl: program.host, + authToken + }, + program.branch || "master", + program.appName + ) + }) +} else if (!program.default || defaultInvalid) { + inquirer.prompt(questions).then(function(answers) { + console.log(" ") + console.log(" ") + + if (!answers.confirmedToDeploy) { + console.log("Operation cancelled by the user...") + console.log(" ") + } else { + let machines = configs.get("captainMachines") + let machineToDeploy = null + for (let i = 0; i < machines.length; i++) { + if (machines[i].name === answers.captainNameToDeploy) { + console.log("Deploying to " + answers.captainNameToDeploy) + machineToDeploy = machines[i] + break + } + } + + console.log(" ") + deployTo(machineToDeploy, answers.branchToPush, answers.appName) + } + }) +} + +function deployTo(machineToDeploy, branchToPush, appName) { + function checkAuthAndSendFile(zipFileFullPath, gitHash) { + function isAuthTokenValidCallback(isValid) { + if (isValid) { + sendFileToCaptain( + machineToDeploy, + zipFileFullPath, + appName, + gitHash, + branchToPush + ) + } else { + requestLogin( + machineToDeploy.name, + machineToDeploy.baseUrl, + function callback(machineToDeployNew) { + deployTo(machineToDeployNew, branchToPush, appName) + } + ) + } + } + + isAuthTokenValid(machineToDeploy, appName, isAuthTokenValidCallback) + } + + if (getSuppliedTarFile()) { + checkAuthAndSendFile( + path.join(process.cwd(), getSuppliedTarFile()), + "sendviatarfile" + ) + return + } + + if (!commandExistsSync("git")) { + console.log(chalk.red('"git" command not found...')) + console.log( + chalk.red( + 'Captain needs "git" to create tar file of your source files...' + ) + ) + console.log(" ") + process.exit(1) + } + + let zipFileNameToDeploy = "temporary-captain-to-deploy.tar" + let zipFileFullPath = path.join(process.cwd(), zipFileNameToDeploy) + + console.log("Saving tar file to:") + console.log(zipFileFullPath) + console.log(" ") + + try { + fs.removeSync(zipFileFullPath) + } catch (ignoreError) {} + + exec( + 'git archive --format tar --output "' + + zipFileFullPath + + '" ' + + branchToPush, + function(err, stdout, stderr) { + if (err) { + console.log(chalk.red("TAR file failed")) + console.log(chalk.red(err + " ")) + console.log(" ") + fs.removeSync(zipFileFullPath) + return + } + + exec("git rev-parse " + branchToPush, function(err, stdout, stderr) { + const gitHash = (stdout || "").trim() + + if (err || !/^[a-f0-9]{40}$/.test(gitHash)) { + console.log( + chalk.red( + "Cannot find hash of last commit on this branch: " + branchToPush + ) + ) + console.log(chalk.red(gitHash + " ")) + console.log(chalk.red(err + " ")) + console.log(" ") + return + } + + console.log("Pushing last commit on " + branchToPush + ": " + gitHash) + + checkAuthAndSendFile(zipFileFullPath, gitHash) + }) + } + ) +} + +var lastLineNumberPrinted = -10000 // we want to show all lines to begin with! + +function startFetchingBuildLogs(machineToDeploy, appName) { + let options = { + url: machineToDeploy.baseUrl + "/api/v1/user/appData/" + appName, + headers: { + "x-namespace": "captain", + "x-captain-auth": machineToDeploy.authToken + }, + method: "GET" + } + + function onLogRetrieved(data) { + if (data) { + var lines = data.logs.lines + var firstLineNumberOfLogs = data.logs.firstLineNumber + var firstLinesToPrint = 0 + if (firstLineNumberOfLogs > lastLineNumberPrinted) { + if (firstLineNumberOfLogs < 0) { + // This is the very first fetch, probably firstLineNumberOfLogs is around -50 + firstLinesToPrint = -firstLineNumberOfLogs + } else { + console.log("[[ TRUNCATED ]]") + } + } else { + firstLinesToPrint = lastLineNumberPrinted - firstLineNumberOfLogs + } + + lastLineNumberPrinted = firstLineNumberOfLogs + lines.length + + for (var i = firstLinesToPrint; i < lines.length; i++) { + console.log((lines[i] || "").trim()) + } + } + + if (data && !data.isAppBuilding) { + console.log(" ") + if (!data.isBuildFailed) { + console.log(chalk.green("Deployed successfully: ") + appName) + console.log( + chalk.magenta("App is available at ") + + machineToDeploy.baseUrl + .replace("//captain.", "//" + appName + ".") + .replace("https://", "http://") + ) + } else { + console.error( + chalk.red( + '\nSomething bad happened. Cannot deploy "' + appName + '"\n' + ) + ) + } + console.log(" ") + return + } + + setTimeout(function() { + startFetchingBuildLogs(machineToDeploy, appName) + }, 2000) + } + + function callback(error, response, body) { + try { + if (!error && response.statusCode === 200) { + let data = JSON.parse(body) + + if (data.status !== 100) { + throw new Error(JSON.stringify(data, null, 2)) + } + + onLogRetrieved(data.data) + + return + } + + if (error) { + throw new Error(error) + } + + throw new Error( + response ? JSON.stringify(response, null, 2) : "Response NULL" + ) + } catch (error) { + console.error( + chalk.red( + '\nSomething while retrieving app build logs.. "' + error + '"\n' + ) + ) + + onLogRetrieved(null) + } + } + + request(options, callback) +} diff --git a/app-cli/lib/list.js b/app-cli/lib/list.js new file mode 100644 index 0000000..0418def --- /dev/null +++ b/app-cli/lib/list.js @@ -0,0 +1,29 @@ +const chalk = require("chalk") +const configstore = require("configstore") +const packagejson = require("../package.json") +const configs = new configstore(packagejson.name, { + captainMachines: [] +}) + +function _displayMachine(machine) { + console.log( + ">> " + + chalk.greenBright(machine.name) + + " at " + + chalk.cyan(machine.baseUrl) + ) +} + +function list() { + console.log("\nLogged in Captain Machines:\n") + + const machines = configs.get("captainMachines") + + machines.map(machine => { + _displayMachine(machine) + }) + + console.log("") +} + +module.exports = list diff --git a/app-cli/lib/login.js b/app-cli/lib/login.js new file mode 100644 index 0000000..16fba02 --- /dev/null +++ b/app-cli/lib/login.js @@ -0,0 +1,239 @@ +const program = require("commander") +const fs = require("fs") +const path = require("path") +const chalk = require("chalk") +const inquirer = require("inquirer") +const configstore = require("configstore") +const request = require("request") +const packagejson = require("./package.json") +const configs = new configstore(packagejson.name, { + captainMachines: [] +}) + +program + .description( + "Login to a CaptainDuckDuck machine. You can be logged in to multiple machines simultaneously." + ) + .parse(process.argv) + +function cleanUpUrl(url) { + if (!url) { + return url + } + + url = url.trim() + + url = url.replace("http://", "").replace("https://", "") + + if (!url || !url.length) { + return url + } + + if (url.substr(url.length - 1, 1) === "/") { + url = url.substr(0, url.length - 1) + } + + return url +} + +if (program.args.length) { + console.error(chalk.red("Unrecognized commands:")) + + program.args.forEach(function(arg) { + console.log(chalk.red(arg)) + }) + + console.error(chalk.red("Login does not require any options. ")) + + process.exit(1) +} + +function findDefaultCaptainName() { + const machines = configs.get("captainMachines") + let currentSuffix = machines.length + 1 + + function getCaptainFullName(suffix) { + if (suffix < 10) { + suffix = "0" + suffix + } + + return "captain-" + suffix + } + + function isSuffixValid(suffixNumber) { + for (let i = 0; i < machines.length; i++) { + const m = machines[i] + + if (m.name === getCaptainFullName(suffixNumber)) { + return false + } + } + + return true + } + + while (!isSuffixValid(currentSuffix)) { + currentSuffix++ + } + + return getCaptainFullName(currentSuffix) +} + +console.log("Login to a Captain Machine") + +const SAMPLE_DOMAIN = "captain.captainroot.yourdomain.com" +const questions = [ + { + type: "input", + default: SAMPLE_DOMAIN, + name: "captainAddress", + message: + "Enter address of the Captain machine. \nIt is captain.[your-captain-root-domain] :", + validate: function(value) { + if (value === SAMPLE_DOMAIN) { + return "Enter a valid URL" + } + + if (!cleanUpUrl(value)) { + return "This is an invalid URL: " + value + } + + const machines = configs.get("captainMachines") + + for (let i = 0; i < machines.length; i++) { + if (cleanUpUrl(machines[i].baseUrl) === cleanUpUrl(value)) { + return ( + value + + " already exist as " + + machines[i].name + + ". If you want to replace the existing entry, you have to first use command, and then re-login." + ) + } + } + + if (value && value.trim()) { + return true + } + + return "Please enter a valid address." + } + }, + { + type: "confirm", + name: "captainHasRootSsl", + message: "Is HTTPS activated for this Captain machine?", + default: true + }, + { + type: "password", + name: "captainPassword", + message: "Enter your password:", + validate: function(value) { + if (value && value.trim()) { + return true + } + + return "Please enter your password." + } + }, + { + type: "input", + name: "captainName", + message: "Enter a name for this Captain machine:", + default: findDefaultCaptainName(), + validate: function(value) { + const machines = configs.get("captainMachines") + + for (let i = 0; i < machines.length; i++) { + if (machines[i].name === value) { + return ( + value + + " already exist. If you want to replace the existing entry, you have to first use command, and then re-login." + ) + } + } + + if (value.match(/^[-\d\w]+$/i)) { + return true + } + + return "Please enter a Captain Name." + } + } +] + +function setCaptainMachine(data) { + const machines = configs.get("captainMachines") + + machines.push(data) + + configs.set("captainMachines", machines) +} + +inquirer.prompt(questions).then(function(answers) { + console.log("\n") + + const baseUrl = + (answers.captainHasRootSsl ? "https://" : "http://") + + cleanUpUrl(answers.captainAddress) + const options = { + url: baseUrl + "/api/v1/login", + headers: { + "x-namespace": "captain" + }, + method: "POST", + form: { + password: answers.captainPassword + } + } + + function callback(error, response, body) { + const captainNameEnterByUser = answers.captainName + + try { + if (!error && response.statusCode === 200) { + const data = JSON.parse(body) + + if (data.status !== 100) { + throw new Error(JSON.stringify(data, null, 2)) + } + + console.log(chalk.green("Logged in successfully to ") + baseUrl) + + console.log( + chalk.green( + `Authorization token is now saved as ${captainNameEnterByUser} \n` + ) + ) + + setCaptainMachine({ + authToken: data.token, + baseUrl: baseUrl, + name: captainNameEnterByUser + }) + + return + } + + if (error) { + throw new Error(error) + } + + throw new Error( + response ? JSON.stringify(response, null, 2) : "Response NULL" + ) + } catch (error) { + console.error( + chalk.red( + `Something bad happened. Cannot save "${captainNameEnterByUser}"` + ) + ) + + const errorMessage = error.message ? error.message : error + + console.error(`${chalk.red(errorMessage)} \n`) + } + } + + request(options, callback) +}) diff --git a/app-cli/lib/logout.js b/app-cli/lib/logout.js new file mode 100644 index 0000000..e102f46 --- /dev/null +++ b/app-cli/lib/logout.js @@ -0,0 +1,79 @@ +const inquirer = require("inquirer") +const configstore = require("configstore") +const packagejson = require("../package.json") +const configs = new configstore(packagejson.name, { + captainMachines: [] +}) +const machines = configs.get("captainMachines") + +function getMachineList() { + const firstItemInOption = [ + { + name: "-- CANCEL --", + value: "", + short: "" + } + ] + const listOfMachines = machines.map(machine => { + return { + name: `${machine.name} at ${machine.baseUrl}`, + value: `${machine.name}`, + short: `${machine.name} at ${machine.baseUrl}` + } + }) + + return [...firstItemInOption, ...listOfMachines] +} + +function generateQuestions() { + const listOfMachines = getMachineList() + + return [ + { + type: "list", + name: "captainNameToLogout", + message: "Select the Captain Machine you want to logout from:", + choices: listOfMachines + }, + { + type: "confirm", + name: "confirmedToLogout", + message: "Are you sure you want to logout from this Captain machine?", + default: false, + when: answers => !!answers.captainNameToLogout + } + ] +} + +function removeMachineFromLocalStorage(machineName) { + const removedMachine = machines.filter( + machine => machine.name === machineName + ) + const newMachines = machines.filter(machine => machine !== machineName) + + configs.set("captainMachines", newMachines) + + console.log( + `You are now logged out from ${removedMachine.name} at ${ + removedMachine.baseUrl + }...\n` + ) +} + +function logout() { + const questions = generateQuestions() + + console.log("Logout from a Captain Machine and clear auth info") + + inquirer.prompt(questions).then(answers => { + if (!answers.captainNameToLogout) { + console.log("\nOperation cancelled by the user...\n") + + return + } + + removeMachineFromLocalStorage(answers.captainNameToLogout) + }) +} + +module.exports = logout diff --git a/app-cli/lib/serversetup.js b/app-cli/lib/serversetup.js new file mode 100644 index 0000000..5b01350 --- /dev/null +++ b/app-cli/lib/serversetup.js @@ -0,0 +1,514 @@ +const program = require("commander") +const chalk = require("chalk") +const inquirer = require("inquirer") +const configstore = require("configstore") +const request = require("request") +const spinnerUtil = require("./utils/spinner") +const packagejson = require("./package.json") +const configs = new configstore(packagejson.name, { + captainMachines: [] +}) + +program.description("Easy setup for your Captain server.").parse(process.argv) + +if (program.args.length) { + console.error(chalk.red("Unrecognized commands:")) + + program.args.forEach(function(arg) { + console.log(chalk.red(arg)) + }) + + console.error(chalk.red("This command does not require any options. ")) + + process.exit(1) +} + +function findDefaultCaptainName() { + const machines = configs.get("captainMachines") + let currentSuffix = machines.length + 1 + + function getCaptainFullName(suffix) { + if (suffix < 10) { + suffix = "0" + suffix + } + + return "captain-" + suffix + } + + function isSuffixValid(suffixNumber) { + for (let i = 0; i < machines.length; i++) { + const m = machines[i] + + if (m.name === getCaptainFullName(suffixNumber)) { + return false + } + } + + return true + } + + while (!isSuffixValid(currentSuffix)) { + currentSuffix++ + } + + return getCaptainFullName(currentSuffix) +} + +function isIpAddress(ipaddress) { + if ( + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( + ipaddress + ) + ) { + return true + } + + return false +} + +console.log("\nSetup your Captain server\n") + +const SAMPLE_IP = "123.123.123.123" +let authTokenFromLogin = null +let ipAddressOfServer = null +let customDomainFromUser = null +let newPasswordFirstTry = null +let oldPassword = "captain42" + +function setCustomDomain(baseApiUrl, customDomain) { + return apiEndpointWithLoading("Changing Domain...")( + "POST", + baseApiUrl, + "/api/v1/user/system/changerootdomain/", + { + rootDomain: customDomain + } + ) +} + +function enableHttps(baseApiUrl, emailAddress) { + return apiEndpointWithLoading("Enabling SSL...")( + "POST", + baseApiUrl, + "/api/v1/user/system/enablessl/", + { + emailAddress: emailAddress + } + ) +} + +function changePass(baseApiUrl, newPass) { + return apiEndpointWithLoading("Changing Password...")( + "POST", + baseApiUrl, + "/api/v1/user/changepassword/", + { + oldPassword: oldPassword, + newPassword: newPass + } + ) +} + +function forceHttps(baseApiUrl) { + return apiEndpointWithLoading("Forcing SSL...")( + "POST", + baseApiUrl, + "/api/v1/user/system/forcessl/", + { + isEnabled: true + } + ) +} + +// returns promise that resolves to auth token +// rejects with ErrorCode or null +function login(baseApiUrl, password) { + return apiEndpointWithLoading("Login...")( + "POST", + baseApiUrl, + "/api/v1/login", + { + password: password + } + ).then(function(data) { + return data.token + }) +} + +function apiEndpointWithLoading(message) { + return (method, baseApiUrl, endpoint, dataToSend) => { + return apiEndpoint(method, baseApiUrl, endpoint, dataToSend, message) + } +} + +function apiEndpoint(method, baseApiUrl, endpoint, dataToSend, message) { + const options = { + url: baseApiUrl + endpoint, + headers: { + "x-namespace": "captain", + "x-captain-auth": authTokenFromLogin + }, + method: method, + form: dataToSend + } + + return new Promise(function(res, rej) { + let spinner + + if (message) { + spinner = spinnerUtil.start(message) + } + + var callback = function(error, response, body) { + try { + if (!error && response.statusCode === 200) { + const data = JSON.parse(body) + + if (data.status !== 100) { + if (spinner) { + spinnerUtil.fail(spinner) + } + + rej(data) + + return + } + + if (spinner) { + spinnerUtil.succeed(spinner) + } + + res(data) + + return + } + + if (error) { + throw new Error(error) + } + + throw new Error( + response ? JSON.stringify(response, null, 2) : "Response NULL" + ) + } catch (error) { + if (spinner) { + spinnerUtil.fail(spinner) + } + + console.error( + chalk.red( + `Something bad happened. Cannot connect to "${baseApiUrl} ${endpoint}"` + ) + ) + + const errorMessage = error.message ? error.message : error + + console.error(`${chalk.red(errorMessage)}\n`) + + process.exit(0) + } + } + + request(options, callback) + }) +} + +const questions = [ + { + type: "list", + name: "hasInstalledCaptain", + message: + "Have you already installed Captain on your server by running the following line:" + + "\nmkdir /captain && docker run -v /var/run/docker.sock:/var/run/docker.sock dockersaturn/captainduckduck ?", + default: "Yes", + choices: ["Yes", "No"], + filter: function(value) { + return new Promise(function(res, rej) { + if (value === "Yes") { + res(true) + + return + } + + console.log( + "\n\nCannot start the setup process if Captain is not installed." + ) + + console.log( + "Please read tutorial on CaptainDuckDuck.com\n to learn how to install CaptainDuckDuck on a server.\n" + ) + + process.exit(0) + }) + } + }, + { + type: "input", + default: SAMPLE_IP, + name: "captainAddress", + message: "Enter IP address of your captain server:", + filter: function(value) { + return new Promise(function(res, rej) { + if (value === SAMPLE_IP) { + rej("Enter a valid IP Address") + + return + } + + if (!isIpAddress(value.trim())) { + rej("This is an invalid IP Address: " + value) + + return + } + + ipAddressOfServer = value.trim() + + //login using captain42. + + return login("http://" + ipAddressOfServer + ":3000", oldPassword) + .then(function(authTokenFetched) { + authTokenFromLogin = authTokenFetched + + res(ipAddressOfServer) + }) + .catch(function(error) { + // if error is anything but password wrong, exit here... + if (error.status == 1105) { + authTokenFromLogin = null + + res(ipAddressOfServer) + + return + } + + console.log("") + + if (error.status) { + console.log(chalk.red(`Error: ${error.status}`)) + + console.log(chalk.red(`Error: ${error.description}`)) + } else { + console.log(chalk.red(`Error: ${error}`)) + } + + process.exit(0) + }) + }) + } + }, + { + type: "password", + name: "captainOriginalPassword", + message: "Enter your current password:", + when: function() { + return !authTokenFromLogin + }, + filter: function(value) { + return new Promise(function(res, rej) { + console.log("") + + return login("http://" + ipAddressOfServer + ":3000", value) + .then(function(authTokenFetched) { + authTokenFromLogin = authTokenFetched + + oldPassword = value + + res(value) + }) + .catch(function(error) { + console.log("") + + if (error.status) { + console.log(chalk.red("Error: " + error.status)) + + console.log(chalk.red("Error: " + error.description)) + } else { + console.log(chalk.red("Error: " + error)) + } + + process.exit(0) + }) + }) + } + }, + { + type: "input", + name: "captainRootDomain", + message: + "Enter a root domain for this Captain server. For example, enter test.yourdomain.com if you" + + " setup your DNS to point *.test.yourdomain.com to ip address of your server" + + ": ", + filter: function(value) { + return new Promise(function(res, rej) { + value = value.trim() + + customDomainFromUser = "captain." + value + + return setCustomDomain("http://" + ipAddressOfServer + ":3000", value) + .then(function() { + res(customDomainFromUser) + }) + .catch(function(error) { + if (error.status) { + console.log(chalk.red(`Error: ${error.status}`)) + + console.log(chalk.red(`Error: ${error.description}`)) + } else { + console.log(chalk.red(`Error: ${error}`)) + } + + process.exit(0) + }) + }) + } + }, + { + type: "input", + name: "emailAddress", + message: "Enter your 'valid' email address to enable HTTPS: ", + filter: function(value) { + return new Promise(function(res, rej) { + console.log("") + + value = value.trim() + + return enableHttps("http://" + customDomainFromUser, value) + .then(function() { + return forceHttps("https://" + customDomainFromUser) + }) + .then(function() { + res(value) + }) + .catch(function(error) { + console.log("") + + if (error.status) { + console.log(chalk.red(`Error: ${error.status}`)) + + console.log(chalk.red(`Error: ${error.description}`)) + } else { + console.log(chalk.red(`Error: ${error}`)) + } + + process.exit(0) + }) + }) + } + }, + { + type: "password", + name: "newPasswordFirstTry", + message: "Enter a new password:", + filter: function(value) { + return new Promise(function(res, rej) { + newPasswordFirstTry = value + + res(value) + }) + } + }, + { + type: "password", + name: "newPassword", + message: "Enter a new password:", + filter: function(value) { + return new Promise(function(res, rej) { + if (newPasswordFirstTry !== value) { + rej("Passwords do not match") + //process.exit(0); + + return + } + + return changePass("https://" + customDomainFromUser, value) + .then(function() { + return login("https://" + customDomainFromUser, value) + }) + .then(function(token) { + authTokenFromLogin = token + + res(value) + }) + .catch(function(error) { + console.log("") + + if (error.status) { + console.log(chalk.red(`Error: ${error.status}`)) + + console.log(chalk.red(`Error: ${error.description}`)) + } else { + console.log(chalk.red(`Error: ${error}`)) + } + + console.log( + chalk.red( + "\nIMPORTANT!! Server setup is completed by password is not changed." + ) + ) + + console.log( + chalk.red("You CANNOT use serversetup anymore. To continue: ") + ) + + console.log( + chalk.red( + "- Go to https://" + + customDomainFromUser + + " login with default password and change the password in settings." + ) + ) + + console.log( + chalk.red( + "- In terminal (here), type captainduckduck login and enter this as your root domain: " + + customDomainFromUser + ) + ) + + process.exit(0) + }) + }) + } + }, + { + type: "input", + name: "captainName", + message: "Enter a name for this Captain machine:", + default: findDefaultCaptainName(), + validate: function(value) { + const machines = configs.get("captainMachines") + + for (let i = 0; i < machines.length; i++) { + if (machines[i].name === value) { + return `${value} already exist. If you want to replace the existing entry, you have to first use command, and then re-login.` + } + } + + if (value.match(/^[-\d\w]+$/i)) { + return true + } + + return "Please enter a Captain Name." + } + } +] + +inquirer.prompt(questions).then(function(answers) { + var captainAddress = "https://" + customDomainFromUser + + const machines = configs.get("captainMachines") + + machines.push({ + authToken: authTokenFromLogin, + baseUrl: captainAddress, + name: answers.captainName + }) + + configs.set("captainMachines", machines) + + console.log(`\n\nCaptain is available at ${captainAddress}`) + + console.log( + "\nFor more details and docs see http://www.captainduckduck.com\n\n" + ) +}) diff --git a/app-cli/utils/errorHandler.js b/app-cli/utils/errorHandler.js new file mode 100644 index 0000000..34a2ddf --- /dev/null +++ b/app-cli/utils/errorHandler.js @@ -0,0 +1,9 @@ +const chalk = require("chalk") + +function printErrorAndExit(error) { + console.log(`${chalk.bold.red(error)}\n\n`) + + process.exit(0) +} + +module.exports = printErrorAndExit diff --git a/app-cli/utils/fileHandler.js b/app-cli/utils/fileHandler.js new file mode 100644 index 0000000..1358c6a --- /dev/null +++ b/app-cli/utils/fileHandler.js @@ -0,0 +1,178 @@ +const fs = require("fs-extra") +const chalk = require("chalk") +const request = require("request") +const ProgressBar = require("progress") +const ora = require("ora") +const configstore = require("configstore") +const packagejson = require("./package.json") +const { requestLogin } = require("./utils/loginHandler") +const configs = new configstore(packagejson.name, { + captainMachines: [], + apps: [] +}) + +function sendFileToCaptain( + machineToDeploy, + zipFileFullPath, + appName, + gitHash, + branchToPush +) { + console.log("Uploading file to " + machineToDeploy.baseUrl) + + const fileSize = fs.statSync(zipFileFullPath).size + const fileStream = fs.createReadStream(zipFileFullPath) + const barOpts = { + width: 20, + total: fileSize, + clear: true + } + const bar = new ProgressBar( + " uploading [:bar] :percent (ETA :etas)", + barOpts + ) + + fileStream.on("data", function(chunk) { + bar.tick(chunk.length) + }) + + let spinner + + fileStream.on("end", function() { + console.log(" ") + + console.log("This might take several minutes. PLEASE BE PATIENT...") + + spinner = ora("Building your source code...").start() + + spinner.color = "yellow" + }) + + const options = { + url: + machineToDeploy.baseUrl + + "/api/v1/user/appData/" + + appName + + "/?detached=1", + headers: { + "x-namespace": "captain", + "x-captain-auth": machineToDeploy.authToken + }, + method: "POST", + formData: { + sourceFile: fileStream, + gitHash: gitHash + } + } + + function callback(error, response, body) { + if (spinner) { + spinner.stop() + } + + if (fs.pathExistsSync(zipFileFullPath) && !getSuppliedTarFile()) { + fs.removeSync(zipFileFullPath) + } + + try { + if (!error && response.statusCode === 200) { + const data = JSON.parse(body) + + if (data.status === 1106) { + // expired token + requestLogin( + machineToDeploy.name, + machineToDeploy.baseUrl, + function callback(machineToDeployNew) { + deployTo(machineToDeployNew, branchToPush, appName) + } + ) + + return + } + + if (data.status !== 100 && data.status !== 101) { + throw new Error(JSON.stringify(data, null, 2)) + } + + savePropForDirectory(APP_NAME, appName) + + savePropForDirectory(BRANCH_TO_PUSH, branchToPush) + + savePropForDirectory(MACHINE_TO_DEPLOY, machineToDeploy) + + if (data.status === 100) { + console.log(chalk.green("Deployed successfully: ") + appName) + console.log(" ") + } else if (data.status === 101) { + console.log(chalk.green("Building started: ") + appName) + console.log(" ") + + startFetchingBuildLogs(machineToDeploy, appName) + } + + return + } + + if (error) { + throw new Error(error) + } + + throw new Error( + response ? JSON.stringify(response, null, 2) : "Response NULL" + ) + } catch (error) { + console.error( + chalk.red('\nSomething bad happened. Cannot deploy "' + appName + '"\n') + ) + + if (error.message) { + try { + var errorObj = JSON.parse(error.message) + if (errorObj.status) { + console.error(chalk.red("\nError code: " + errorObj.status)) + console.error( + chalk.red("\nError message:\n\n " + errorObj.description) + ) + } else { + throw new Error("NOT API ERROR") + } + } catch (ignoreError) { + console.error(chalk.red(error.message)) + } + } else { + console.error(chalk.red(error)) + } + console.log(" ") + } + } + + request(options, callback) +} + +// Sets default value for propType that is stored in a directory to propValue. +// Replaces saveAppForDirectory +function savePropForDirectory(propType, propValue) { + const apps = configs.get("apps") + + for (let i = 0; i < apps.length; i++) { + const app = apps[i] + + if (app.cwd === process.cwd()) { + app[propType] = propValue + + configs.set("apps", apps) + + return + } + } + + apps.push({ + cwd: process.cwd(), + [propType]: propValue + }) + + configs.set("apps", apps) +} + +module.exports = sendFileToCaptain diff --git a/app-cli/utils/loginHandler.js b/app-cli/utils/loginHandler.js new file mode 100644 index 0000000..2a82ca1 --- /dev/null +++ b/app-cli/utils/loginHandler.js @@ -0,0 +1,194 @@ +const inquirer = require("inquirer") + +function requestLogin(serverName, serverAddress, loginCallback) { + console.log("Your auth token is not valid anymore. Try to login again.") + + const questions = [ + { + type: "password", + name: "captainPassword", + message: "Please enter your password for " + serverAddress, + validate: function(value) { + if (value && value.trim()) { + return true + } + + return "Please enter your password for " + serverAddress + } + } + ] + + inquirer.prompt(questions).then(function(passwordAnswers) { + var password = passwordAnswers.captainPassword + + let options = { + url: serverAddress + "/api/v1/login", + headers: { + "x-namespace": "captain" + }, + method: "POST", + form: { + password: password + } + } + + function callback(error, response, body) { + try { + if (!error && response.statusCode === 200) { + let data = JSON.parse(body) + + if (data.status !== 100) { + throw new Error(JSON.stringify(data, null, 2)) + } + + var newMachineToDeploy = updateAuthTokenInConfigStoreAndReturn( + data.token + ) + + loginCallback(newMachineToDeploy) + + return + } + + if (error) { + throw new Error(error) + } + + throw new Error( + response ? JSON.stringify(response, null, 2) : "Response NULL" + ) + } catch (error) { + if (error.message) { + try { + var errorObj = JSON.parse(error.message) + if (errorObj.status) { + console.error(chalk.red("\nError code: " + errorObj.status)) + console.error( + chalk.red("\nError message:\n\n " + errorObj.description) + ) + } else { + throw new Error("NOT API ERROR") + } + } catch (ignoreError) { + console.error(chalk.red(error.message)) + } + } else { + console.error(chalk.red(error)) + } + console.log(" ") + } + + process.exit(0) + } + + request(options, callback) + }) +} + +function requestLoginAuth(serverAddress, password, authCallback) { + let options = { + url: serverAddress + "/api/v1/login", + headers: { + "x-namespace": "captain" + }, + method: "POST", + form: { + password: password + } + } + + function callback(error, response, body) { + try { + if (!error && response.statusCode === 200) { + let data = JSON.parse(body) + + if (data.status !== 100) { + throw new Error(JSON.stringify(data, null, 2)) + } + + authCallback(data.token) + + return + } + + if (error) { + throw new Error(error) + } + + throw new Error( + response ? JSON.stringify(response, null, 2) : "Response NULL" + ) + } catch (error) { + if (error.message) { + try { + var errorObj = JSON.parse(error.message) + if (errorObj.status) { + console.error(chalk.red("\nError code: " + errorObj.status)) + console.error( + chalk.red("\nError message:\n\n " + errorObj.description) + ) + } else { + throw new Error("NOT API ERROR") + } + } catch (ignoreError) { + console.error(chalk.red(error.message)) + } + } else { + console.error(chalk.red(error)) + } + console.log(" ") + } + + process.exit(0) + } + + request(options, callback) +} + +function isAuthTokenValid(machineToDeploy, appName, isAuthTokenValidCallback) { + let options = { + url: machineToDeploy.baseUrl + "/api/v1/user/appDefinitions/", + headers: { + "x-namespace": "captain", + "x-captain-auth": machineToDeploy.authToken + }, + method: "GET" + } + + function callback(error, response, body) { + try { + if (!error && response.statusCode === 200) { + let data = JSON.parse(body) + + if (data.status === 1106 || data.status === 1105) { + isAuthTokenValidCallback(false) + } else { + isAuthTokenValidCallback(true) + } + } + } catch (error) { + // This is just a sanity check. We only fire FALSE (i.e. expired) if we know it's expired or password is wrong + isAuthTokenValidCallback(true) + } + } + + request(options, callback) +} + +function updateAuthTokenInConfigStoreAndReturn(authToken) { + let machines = configs.get("captainMachines") + for (let i = 0; i < machines.length; i++) { + if (machines[i].name === serverName) { + var baseUrl = (machines[i].authToken = authToken) + configs.set("captainMachines", machines) + console.log("You are now logged back in to " + serverAddress) + return machines[i] + } + } +} + +module.exports = { + requestLogin, + requestLoginAuth, + isAuthTokenValid +} diff --git a/app-cli/utils/spinner.js b/app-cli/utils/spinner.js index 3c031a4..1912ab0 100644 --- a/app-cli/utils/spinner.js +++ b/app-cli/utils/spinner.js @@ -1,24 +1,24 @@ -const ora = require('ora'); +const ora = require("ora") function start(message) { - return ora(message).start() + return ora(message).start() } function stop(spinner) { - spinner.stop(); + spinner.stop() } function succeed(spinner) { - spinner.succeed(); + spinner.succeed() } function fail(spinner) { - spinner.fail(); + spinner.fail() } module.exports = { - start: start, - stop: stop, - succeed: succeed, - fail: fail, -} \ No newline at end of file + start: start, + stop: stop, + succeed: succeed, + fail: fail +} diff --git a/app-cli/utils/validationsHandler.js b/app-cli/utils/validationsHandler.js new file mode 100644 index 0000000..ac6c12f --- /dev/null +++ b/app-cli/utils/validationsHandler.js @@ -0,0 +1,23 @@ +const fs = require("fs-extra") +const printErrorAndExit = require("./errorHandler") + +function validateIsGitRepository() { + if (!fs.pathExistsSync("./.git")) { + printErrorAndExit( + "**** ERROR: You are not in a git root directory. This command will only deploys the current directory ****" + ) + } +} + +function validateDefinitionFile() { + if (!fs.pathExistsSync("./captain-definition")) { + printErrorAndExit( + "**** ERROR: captain-definition file cannot be found. Please see docs! ****" + ) + } +} + +module.exports = { + validateIsGitRepository, + validateDefinitionFile +}