Merge pull request #1214 from Budibase/budi-day/cli
Budibase CLI backbone
This commit is contained in:
commit
ea6b8e983c
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"emit": true,
|
||||||
|
"key": true
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended"],
|
||||||
|
"rules": {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
node_modules/
|
||||||
|
docker-compose.yaml
|
||||||
|
envoy.yaml
|
||||||
|
hosting.properties
|
||||||
|
build/
|
||||||
|
docker-error.log
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "cli",
|
||||||
|
"version": "0.7.8",
|
||||||
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"bin": "src/index.js",
|
||||||
|
"author": "Budibase",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pkg . --out-path build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"commander": "^7.1.0",
|
||||||
|
"docker-compose": "^0.23.6",
|
||||||
|
"inquirer": "^8.0.0",
|
||||||
|
"lookpath": "^1.1.0",
|
||||||
|
"pkg": "^4.4.9",
|
||||||
|
"randomstring": "^1.1.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^7.20.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
exports.CommandWords = {
|
||||||
|
HOSTING: "hosting",
|
||||||
|
HELP: "help",
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
const Command = require("../structures/Command")
|
||||||
|
const { CommandWords } = require("../constants")
|
||||||
|
const { lookpath } = require("lookpath")
|
||||||
|
const { downloadFile, logErrorToFile, success, info } = require("../utils")
|
||||||
|
const { confirmation } = require("../questions")
|
||||||
|
const fs = require("fs")
|
||||||
|
const compose = require("docker-compose")
|
||||||
|
const envFile = require("./makeEnv")
|
||||||
|
|
||||||
|
const BUDIBASE_SERVICES = ["app-service", "worker-service"]
|
||||||
|
const ERROR_FILE = "docker-error.log"
|
||||||
|
const FILE_URLS = [
|
||||||
|
"https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml",
|
||||||
|
"https://raw.githubusercontent.com/Budibase/budibase/master/hosting/envoy.yaml",
|
||||||
|
]
|
||||||
|
|
||||||
|
async function downloadFiles() {
|
||||||
|
const promises = []
|
||||||
|
for (let url of FILE_URLS) {
|
||||||
|
const fileName = url.split("/").slice(-1)[0]
|
||||||
|
promises.push(downloadFile(url, `./${fileName}`))
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkDockerConfigured() {
|
||||||
|
const error =
|
||||||
|
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/self-hosting/hosting-methods/docker-compose#installing-docker"
|
||||||
|
const docker = await lookpath("docker")
|
||||||
|
const compose = await lookpath("docker-compose")
|
||||||
|
if (!docker || !compose) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkInitComplete() {
|
||||||
|
if (!fs.existsSync(envFile.filePath)) {
|
||||||
|
throw "Please run the hosting --init command before any other hosting command."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleError(func) {
|
||||||
|
try {
|
||||||
|
await func()
|
||||||
|
} catch (err) {
|
||||||
|
if (err && err.err) {
|
||||||
|
logErrorToFile(ERROR_FILE, err.err)
|
||||||
|
}
|
||||||
|
throw `Failed to start - logs written to file: ${ERROR_FILE}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
const shouldContinue = await confirmation(
|
||||||
|
"This will create multiple files in current directory, should continue?"
|
||||||
|
)
|
||||||
|
if (!shouldContinue) {
|
||||||
|
console.log("Stopping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await downloadFiles()
|
||||||
|
await envFile.make()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
checkInitComplete()
|
||||||
|
console.log(info("Starting services, this may take a moment."))
|
||||||
|
const port = envFile.get("MAIN_PORT")
|
||||||
|
await handleError(async () => {
|
||||||
|
await compose.upAll({ cwd: "./", log: false })
|
||||||
|
})
|
||||||
|
console.log(
|
||||||
|
success(
|
||||||
|
`Services started, please go to http://localhost:${port} for next steps.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function status() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
checkInitComplete()
|
||||||
|
console.log(info("Budibase status"))
|
||||||
|
await handleError(async () => {
|
||||||
|
const response = await compose.ps()
|
||||||
|
console.log(response.out)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stop() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
checkInitComplete()
|
||||||
|
console.log(info("Stopping services, this may take a moment."))
|
||||||
|
await handleError(async () => {
|
||||||
|
await compose.stop()
|
||||||
|
})
|
||||||
|
console.log(success("Services have been stopped successfully."))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
checkInitComplete()
|
||||||
|
if (
|
||||||
|
await confirmation(
|
||||||
|
"Do you wish to update you docker-compose.yaml and envoy.yaml?"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await downloadFiles()
|
||||||
|
}
|
||||||
|
await handleError(async () => {
|
||||||
|
const status = await compose.ps()
|
||||||
|
const parts = status.out.split("\n")
|
||||||
|
const isUp = parts[2] && parts[2].indexOf("Up") !== -1
|
||||||
|
if (isUp) {
|
||||||
|
console.log(info("Stopping services, this may take a moment."))
|
||||||
|
await compose.stop()
|
||||||
|
}
|
||||||
|
console.log(info("Beginning update, this may take a few minutes."))
|
||||||
|
await compose.pullMany(BUDIBASE_SERVICES, { log: true })
|
||||||
|
if (isUp) {
|
||||||
|
console.log(success("Update complete, restarting services..."))
|
||||||
|
await start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = new Command(`${CommandWords.HOSTING}`)
|
||||||
|
.addHelp("Controls self hosting on the Budibase platform.")
|
||||||
|
.addSubOption(
|
||||||
|
"--init",
|
||||||
|
"Configure a self hosted platform in current directory.",
|
||||||
|
init
|
||||||
|
)
|
||||||
|
.addSubOption(
|
||||||
|
"--start",
|
||||||
|
"Start the configured platform in current directory.",
|
||||||
|
start
|
||||||
|
)
|
||||||
|
.addSubOption(
|
||||||
|
"--status",
|
||||||
|
"Check the status of currently running services.",
|
||||||
|
status
|
||||||
|
)
|
||||||
|
.addSubOption(
|
||||||
|
"--stop",
|
||||||
|
"Stop the configured platform in the current directory.",
|
||||||
|
stop
|
||||||
|
)
|
||||||
|
.addSubOption(
|
||||||
|
"--update",
|
||||||
|
"Update the Budibase images to the latest version.",
|
||||||
|
update
|
||||||
|
)
|
||||||
|
|
||||||
|
exports.command = command
|
|
@ -0,0 +1,59 @@
|
||||||
|
const { string, number } = require("../questions")
|
||||||
|
const { success } = require("../utils")
|
||||||
|
const fs = require("fs")
|
||||||
|
const path = require("path")
|
||||||
|
const randomString = require("randomstring")
|
||||||
|
|
||||||
|
const FILE_PATH = path.resolve("./.env")
|
||||||
|
|
||||||
|
function getContents(port, hostingKey) {
|
||||||
|
return `
|
||||||
|
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
||||||
|
MAIN_PORT=${port}
|
||||||
|
|
||||||
|
# Use this password when configuring your self hosting settings
|
||||||
|
HOSTING_KEY=${hostingKey}
|
||||||
|
|
||||||
|
# This section contains all secrets pertaining to the system
|
||||||
|
JWT_SECRET=${randomString.generate()}
|
||||||
|
MINIO_ACCESS_KEY=${randomString.generate()}
|
||||||
|
MINIO_SECRET_KEY=${randomString.generate()}
|
||||||
|
COUCH_DB_PASSWORD=${randomString.generate()}
|
||||||
|
COUCH_DB_USER=${randomString.generate()}
|
||||||
|
|
||||||
|
# This section contains variables that do not need to be altered under normal circumstances
|
||||||
|
APP_PORT=4002
|
||||||
|
WORKER_PORT=4003
|
||||||
|
MINIO_PORT=4004
|
||||||
|
COUCH_DB_PORT=4005
|
||||||
|
BUDIBASE_ENVIRONMENT=PRODUCTION`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.filePath = FILE_PATH
|
||||||
|
|
||||||
|
module.exports.make = async () => {
|
||||||
|
const hostingKey = await string(
|
||||||
|
"Please input the password you'd like to use as your hosting key: "
|
||||||
|
)
|
||||||
|
const hostingPort = await number(
|
||||||
|
"Please enter the port on which you want your installation to run: ",
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
const fileContents = getContents(hostingPort, hostingKey)
|
||||||
|
fs.writeFileSync(FILE_PATH, fileContents)
|
||||||
|
console.log(
|
||||||
|
success(
|
||||||
|
"Configuration has been written successfully - please check .env file for more details."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.get = property => {
|
||||||
|
const props = fs.readFileSync(FILE_PATH, "utf8").split(property)
|
||||||
|
if (props[0].charAt(0) === "=") {
|
||||||
|
property = props[0]
|
||||||
|
} else {
|
||||||
|
property = props[1]
|
||||||
|
}
|
||||||
|
return property.split("=")[1].split("\n")[0]
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
const { getCommands } = require("./options")
|
||||||
|
const { Command } = require("commander")
|
||||||
|
const { getHelpDescription } = require("./utils")
|
||||||
|
|
||||||
|
// add hosting config
|
||||||
|
async function init() {
|
||||||
|
const program = new Command()
|
||||||
|
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
|
||||||
|
.helpOption(false)
|
||||||
|
program.helpOption()
|
||||||
|
// add commands
|
||||||
|
for (let command of getCommands()) {
|
||||||
|
command.configure(program)
|
||||||
|
}
|
||||||
|
// this will stop the program if no command found
|
||||||
|
await program.parseAsync(process.argv)
|
||||||
|
}
|
||||||
|
|
||||||
|
init().catch(err => {
|
||||||
|
console.error(`Unexpected error - `, err)
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
const hosting = require("./hosting")
|
||||||
|
|
||||||
|
exports.getCommands = () => {
|
||||||
|
return [hosting.command]
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
const inquirer = require("inquirer")
|
||||||
|
|
||||||
|
exports.confirmation = async question => {
|
||||||
|
const config = {
|
||||||
|
type: "confirm",
|
||||||
|
message: question,
|
||||||
|
default: true,
|
||||||
|
name: "confirmation",
|
||||||
|
}
|
||||||
|
return (await inquirer.prompt(config)).confirmation
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.string = async (question, defaultString = null) => {
|
||||||
|
const config = {
|
||||||
|
type: "input",
|
||||||
|
name: "string",
|
||||||
|
message: question,
|
||||||
|
}
|
||||||
|
if (defaultString) {
|
||||||
|
config.default = defaultString
|
||||||
|
}
|
||||||
|
return (await inquirer.prompt(config)).string
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.number = async (question, defaultNumber) => {
|
||||||
|
const config = {
|
||||||
|
type: "input",
|
||||||
|
name: "number",
|
||||||
|
message: question,
|
||||||
|
validate: value => {
|
||||||
|
let valid = !isNaN(parseFloat(value))
|
||||||
|
return valid || "Please enter a number"
|
||||||
|
},
|
||||||
|
filter: Number,
|
||||||
|
}
|
||||||
|
if (defaultNumber) {
|
||||||
|
config.default = defaultNumber
|
||||||
|
}
|
||||||
|
return (await inquirer.prompt(config)).number
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
const { getSubHelpDescription, getHelpDescription, error } = require("../utils")
|
||||||
|
|
||||||
|
class Command {
|
||||||
|
constructor(command, func = null) {
|
||||||
|
// if there are options, need to just get the command name
|
||||||
|
this.command = command
|
||||||
|
this.opts = []
|
||||||
|
this.func = func
|
||||||
|
}
|
||||||
|
|
||||||
|
addHelp(help) {
|
||||||
|
this.help = help
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubOption(command, help, func) {
|
||||||
|
this.opts.push({ command, help, func })
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(program) {
|
||||||
|
const thisCmd = this
|
||||||
|
let command = program.command(thisCmd.command)
|
||||||
|
if (this.help) {
|
||||||
|
command = command.description(getHelpDescription(thisCmd.help))
|
||||||
|
}
|
||||||
|
for (let opt of thisCmd.opts) {
|
||||||
|
command = command.option(
|
||||||
|
`${opt.command}`,
|
||||||
|
getSubHelpDescription(opt.help)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
command.helpOption(
|
||||||
|
"--help",
|
||||||
|
getSubHelpDescription(`Get help with ${this.command} options`)
|
||||||
|
)
|
||||||
|
command.action(async options => {
|
||||||
|
try {
|
||||||
|
let executed = false
|
||||||
|
if (thisCmd.func) {
|
||||||
|
await thisCmd.func(options)
|
||||||
|
executed = true
|
||||||
|
}
|
||||||
|
for (let opt of thisCmd.opts) {
|
||||||
|
if (options[opt.command.replace("--", "")]) {
|
||||||
|
await opt.func(options)
|
||||||
|
executed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!executed) {
|
||||||
|
console.log(error(`Unknown ${this.command} option.`))
|
||||||
|
command.help()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(error(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Command
|
|
@ -0,0 +1,46 @@
|
||||||
|
const chalk = require("chalk")
|
||||||
|
const fs = require("fs")
|
||||||
|
const axios = require("axios")
|
||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
exports.downloadFile = async (url, filePath) => {
|
||||||
|
filePath = path.resolve(filePath)
|
||||||
|
const writer = fs.createWriteStream(filePath)
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
url,
|
||||||
|
method: "GET",
|
||||||
|
responseType: "stream",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.data.pipe(writer)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
writer.on("finish", resolve)
|
||||||
|
writer.on("error", reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getHelpDescription = string => {
|
||||||
|
return chalk.cyan(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getSubHelpDescription = string => {
|
||||||
|
return chalk.green(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.error = error => {
|
||||||
|
return chalk.red(`Error - ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.success = success => {
|
||||||
|
return chalk.green(success)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.info = info => {
|
||||||
|
return chalk.cyan(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.logErrorToFile = (file, error) => {
|
||||||
|
fs.writeFileSync(path.resolve(`./${file}`), `Budiase Error\n${error}`)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue