mirror of
https://github.com/caprover/caprover
synced 2026-08-08 00:31:00 +00:00
441 lines
11 KiB
JavaScript
441 lines
11 KiB
JavaScript
#!/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 <value>",
|
|
"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 <value>",
|
|
"Only for stateless mode: Host of the captain machine"
|
|
)
|
|
.option("-a, --appName <value>", "Only for stateless mode: Name of the app")
|
|
.option("-p, --pass <value>", "Only for stateless mode: Password for Captain")
|
|
.option(
|
|
"-b, --branch <value>",
|
|
"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)
|
|
}
|