forked from tsai/budibase
committed by
GitHub
12 changed files with 2054 additions and 0 deletions
@ -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
Loading…
Reference in new issue