@budibase/cli 2.0.7-alpha.1 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -6
- package/src/hosting/genUser.js +17 -0
- package/src/hosting/index.js +20 -155
- package/src/hosting/init.js +73 -0
- package/src/hosting/makeFiles.js +135 -0
- package/src/hosting/start.js +34 -0
- package/src/hosting/status.js +17 -0
- package/src/hosting/stop.js +17 -0
- package/src/hosting/update.js +49 -0
- package/src/hosting/utils.js +87 -0
- package/src/hosting/watch.js +34 -0
- package/src/plugins/index.js +22 -1
- package/src/structures/Command.js +28 -9
- package/src/structures/ConfigManager.js +1 -2
- package/src/utils.js +12 -0
- package/src/hosting/makeEnv.js +0 -66
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"outputPath": "build"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@budibase/backend-core": "2.0.
|
|
30
|
-
"@budibase/string-templates": "2.0.
|
|
31
|
-
"@budibase/types": "2.0.
|
|
29
|
+
"@budibase/backend-core": "^2.0.8",
|
|
30
|
+
"@budibase/string-templates": "^2.0.8",
|
|
31
|
+
"@budibase/types": "^2.0.8",
|
|
32
32
|
"axios": "0.21.2",
|
|
33
33
|
"chalk": "4.1.0",
|
|
34
34
|
"cli-progress": "3.11.2",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"docker-compose": "0.23.6",
|
|
37
37
|
"dotenv": "16.0.1",
|
|
38
38
|
"download": "8.0.0",
|
|
39
|
+
"find-free-port": "^2.0.0",
|
|
39
40
|
"inquirer": "8.0.0",
|
|
40
41
|
"joi": "17.6.0",
|
|
41
42
|
"lookpath": "1.1.0",
|
|
@@ -45,12 +46,13 @@
|
|
|
45
46
|
"pouchdb": "7.3.0",
|
|
46
47
|
"pouchdb-replication-stream": "1.2.9",
|
|
47
48
|
"randomstring": "1.1.5",
|
|
48
|
-
"tar": "6.1.11"
|
|
49
|
+
"tar": "6.1.11",
|
|
50
|
+
"yaml": "^2.1.1"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
53
|
"copyfiles": "^2.4.1",
|
|
52
54
|
"eslint": "^7.20.0",
|
|
53
55
|
"renamer": "^4.0.0"
|
|
54
56
|
},
|
|
55
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "f1ac02246d715e1f08b4caa36f36e08f947a25e1"
|
|
56
58
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { success } = require("../utils")
|
|
2
|
+
const { updateDockerComposeService } = require("./utils")
|
|
3
|
+
const randomString = require("randomstring")
|
|
4
|
+
|
|
5
|
+
exports.generateUser = async () => {
|
|
6
|
+
const email = "admin@admin.com"
|
|
7
|
+
const password = randomString.generate({ length: 6 })
|
|
8
|
+
updateDockerComposeService(service => {
|
|
9
|
+
service.environment["BB_ADMIN_USER_EMAIL"] = email
|
|
10
|
+
service.environment["BB_ADMIN_USER_PASSWORD"] = password
|
|
11
|
+
})
|
|
12
|
+
console.log(
|
|
13
|
+
success(
|
|
14
|
+
`User admin credentials configured, access with email: ${email} - password: ${password}`
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
}
|
package/src/hosting/index.js
CHANGED
|
@@ -1,164 +1,18 @@
|
|
|
1
1
|
const Command = require("../structures/Command")
|
|
2
|
-
const { CommandWords
|
|
3
|
-
const {
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} = require("../utils")
|
|
11
|
-
const { confirmation } = require("../questions")
|
|
12
|
-
const fs = require("fs")
|
|
13
|
-
const compose = require("docker-compose")
|
|
14
|
-
const makeEnv = require("./makeEnv")
|
|
15
|
-
const axios = require("axios")
|
|
16
|
-
const { captureEvent } = require("../events")
|
|
17
|
-
|
|
18
|
-
const BUDIBASE_SERVICES = ["app-service", "worker-service", "proxy-service"]
|
|
19
|
-
const ERROR_FILE = "docker-error.log"
|
|
20
|
-
const FILE_URLS = [
|
|
21
|
-
"https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml",
|
|
22
|
-
]
|
|
23
|
-
const DO_USER_DATA_URL = "http://169.254.169.254/metadata/v1/user-data"
|
|
24
|
-
|
|
25
|
-
async function downloadFiles() {
|
|
26
|
-
const promises = []
|
|
27
|
-
for (let url of FILE_URLS) {
|
|
28
|
-
const fileName = url.split("/").slice(-1)[0]
|
|
29
|
-
promises.push(downloadFile(url, `./${fileName}`))
|
|
30
|
-
}
|
|
31
|
-
await Promise.all(promises)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function checkDockerConfigured() {
|
|
35
|
-
const error =
|
|
36
|
-
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
|
|
37
|
-
const docker = await lookpath("docker")
|
|
38
|
-
const compose = await lookpath("docker-compose")
|
|
39
|
-
if (!docker || !compose) {
|
|
40
|
-
throw error
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function checkInitComplete() {
|
|
45
|
-
if (!fs.existsSync(makeEnv.filePath)) {
|
|
46
|
-
throw "Please run the hosting --init command before any other hosting command."
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function handleError(func) {
|
|
51
|
-
try {
|
|
52
|
-
await func()
|
|
53
|
-
} catch (err) {
|
|
54
|
-
if (err && err.err) {
|
|
55
|
-
logErrorToFile(ERROR_FILE, err.err)
|
|
56
|
-
}
|
|
57
|
-
throw `Failed to start - logs written to file: ${ERROR_FILE}`
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function init(type) {
|
|
62
|
-
const isQuick = type === InitTypes.QUICK || type === InitTypes.DIGITAL_OCEAN
|
|
63
|
-
await checkDockerConfigured()
|
|
64
|
-
if (!isQuick) {
|
|
65
|
-
const shouldContinue = await confirmation(
|
|
66
|
-
"This will create multiple files in current directory, should continue?"
|
|
67
|
-
)
|
|
68
|
-
if (!shouldContinue) {
|
|
69
|
-
console.log("Stopping.")
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
captureEvent(AnalyticsEvents.SelfHostInit, {
|
|
74
|
-
type,
|
|
75
|
-
})
|
|
76
|
-
await downloadFiles()
|
|
77
|
-
const config = isQuick ? makeEnv.QUICK_CONFIG : {}
|
|
78
|
-
if (type === InitTypes.DIGITAL_OCEAN) {
|
|
79
|
-
try {
|
|
80
|
-
const output = await axios.get(DO_USER_DATA_URL)
|
|
81
|
-
const response = parseEnv(output.data)
|
|
82
|
-
for (let [key, value] of Object.entries(makeEnv.ConfigMap)) {
|
|
83
|
-
if (response[key]) {
|
|
84
|
-
config[value] = response[key]
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
} catch (err) {
|
|
88
|
-
// don't need to handle error, just don't do anything
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
await makeEnv.make(config)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function start() {
|
|
95
|
-
await checkDockerConfigured()
|
|
96
|
-
checkInitComplete()
|
|
97
|
-
console.log(
|
|
98
|
-
info(
|
|
99
|
-
"Starting services, this may take a moment - first time this may take a few minutes to download images."
|
|
100
|
-
)
|
|
101
|
-
)
|
|
102
|
-
const port = makeEnv.get("MAIN_PORT")
|
|
103
|
-
await handleError(async () => {
|
|
104
|
-
// need to log as it makes it more clear
|
|
105
|
-
await compose.upAll({ cwd: "./", log: true })
|
|
106
|
-
})
|
|
107
|
-
console.log(
|
|
108
|
-
success(
|
|
109
|
-
`Services started, please go to http://localhost:${port} for next steps.`
|
|
110
|
-
)
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function status() {
|
|
115
|
-
await checkDockerConfigured()
|
|
116
|
-
checkInitComplete()
|
|
117
|
-
console.log(info("Budibase status"))
|
|
118
|
-
await handleError(async () => {
|
|
119
|
-
const response = await compose.ps()
|
|
120
|
-
console.log(response.out)
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function stop() {
|
|
125
|
-
await checkDockerConfigured()
|
|
126
|
-
checkInitComplete()
|
|
127
|
-
console.log(info("Stopping services, this may take a moment."))
|
|
128
|
-
await handleError(async () => {
|
|
129
|
-
await compose.stop()
|
|
130
|
-
})
|
|
131
|
-
console.log(success("Services have been stopped successfully."))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function update() {
|
|
135
|
-
await checkDockerConfigured()
|
|
136
|
-
checkInitComplete()
|
|
137
|
-
if (await confirmation("Do you wish to update you docker-compose.yaml?")) {
|
|
138
|
-
await downloadFiles()
|
|
139
|
-
}
|
|
140
|
-
await handleError(async () => {
|
|
141
|
-
const status = await compose.ps()
|
|
142
|
-
const parts = status.out.split("\n")
|
|
143
|
-
const isUp = parts[2] && parts[2].indexOf("Up") !== -1
|
|
144
|
-
if (isUp) {
|
|
145
|
-
console.log(info("Stopping services, this may take a moment."))
|
|
146
|
-
await compose.stop()
|
|
147
|
-
}
|
|
148
|
-
console.log(info("Beginning update, this may take a few minutes."))
|
|
149
|
-
await compose.pullMany(BUDIBASE_SERVICES, { log: true })
|
|
150
|
-
if (isUp) {
|
|
151
|
-
console.log(success("Update complete, restarting services..."))
|
|
152
|
-
await start()
|
|
153
|
-
}
|
|
154
|
-
})
|
|
155
|
-
}
|
|
2
|
+
const { CommandWords } = require("../constants")
|
|
3
|
+
const { init } = require("./init")
|
|
4
|
+
const { start } = require("./start")
|
|
5
|
+
const { stop } = require("./stop")
|
|
6
|
+
const { status } = require("./status")
|
|
7
|
+
const { update } = require("./update")
|
|
8
|
+
const { generateUser } = require("./genUser")
|
|
9
|
+
const { watchPlugins } = require("./watch")
|
|
156
10
|
|
|
157
11
|
const command = new Command(`${CommandWords.HOSTING}`)
|
|
158
12
|
.addHelp("Controls self hosting on the Budibase platform.")
|
|
159
13
|
.addSubOption(
|
|
160
14
|
"--init [type]",
|
|
161
|
-
"Configure a self hosted platform in current directory, type can be unspecified or '
|
|
15
|
+
"Configure a self hosted platform in current directory, type can be unspecified, 'quick' or 'single'.",
|
|
162
16
|
init
|
|
163
17
|
)
|
|
164
18
|
.addSubOption(
|
|
@@ -181,5 +35,16 @@ const command = new Command(`${CommandWords.HOSTING}`)
|
|
|
181
35
|
"Update the Budibase images to the latest version.",
|
|
182
36
|
update
|
|
183
37
|
)
|
|
38
|
+
.addSubOption(
|
|
39
|
+
"--watch-plugin-dir [directory]",
|
|
40
|
+
"Add plugin directory watching to a Budibase install.",
|
|
41
|
+
watchPlugins
|
|
42
|
+
)
|
|
43
|
+
.addSubOption(
|
|
44
|
+
"--gen-user",
|
|
45
|
+
"Create an admin user automatically as part of first start.",
|
|
46
|
+
generateUser
|
|
47
|
+
)
|
|
48
|
+
.addSubOption("--single", "Specify this with init to use the single image.")
|
|
184
49
|
|
|
185
50
|
exports.command = command
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const { InitTypes, AnalyticsEvents } = require("../constants")
|
|
2
|
+
const { confirmation } = require("../questions")
|
|
3
|
+
const { captureEvent } = require("../events")
|
|
4
|
+
const makeFiles = require("./makeFiles")
|
|
5
|
+
const axios = require("axios")
|
|
6
|
+
const { parseEnv } = require("../utils")
|
|
7
|
+
const { checkDockerConfigured, downloadFiles } = require("./utils")
|
|
8
|
+
const { watchPlugins } = require("./watch")
|
|
9
|
+
const { generateUser } = require("./genUser")
|
|
10
|
+
|
|
11
|
+
const DO_USER_DATA_URL = "http://169.254.169.254/metadata/v1/user-data"
|
|
12
|
+
|
|
13
|
+
async function getInitConfig(type, isQuick, port) {
|
|
14
|
+
const config = isQuick ? makeFiles.QUICK_CONFIG : {}
|
|
15
|
+
if (type === InitTypes.DIGITAL_OCEAN) {
|
|
16
|
+
try {
|
|
17
|
+
const output = await axios.get(DO_USER_DATA_URL)
|
|
18
|
+
const response = parseEnv(output.data)
|
|
19
|
+
for (let [key, value] of Object.entries(makeFiles.ConfigMap)) {
|
|
20
|
+
if (response[key]) {
|
|
21
|
+
config[value] = response[key]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
// don't need to handle error, just don't do anything
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// override port
|
|
29
|
+
if (port) {
|
|
30
|
+
config[makeFiles.ConfigMap.MAIN_PORT] = port
|
|
31
|
+
}
|
|
32
|
+
return config
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
exports.init = async opts => {
|
|
36
|
+
let type, isSingle, watchDir, genUser, port
|
|
37
|
+
if (typeof opts === "string") {
|
|
38
|
+
type = opts
|
|
39
|
+
} else {
|
|
40
|
+
type = opts["init"]
|
|
41
|
+
isSingle = opts["single"]
|
|
42
|
+
watchDir = opts["watchPluginDir"]
|
|
43
|
+
genUser = opts["genUser"]
|
|
44
|
+
port = opts["port"]
|
|
45
|
+
}
|
|
46
|
+
const isQuick = type === InitTypes.QUICK || type === InitTypes.DIGITAL_OCEAN
|
|
47
|
+
await checkDockerConfigured()
|
|
48
|
+
if (!isQuick) {
|
|
49
|
+
const shouldContinue = await confirmation(
|
|
50
|
+
"This will create multiple files in current directory, should continue?"
|
|
51
|
+
)
|
|
52
|
+
if (!shouldContinue) {
|
|
53
|
+
console.log("Stopping.")
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
captureEvent(AnalyticsEvents.SelfHostInit, {
|
|
58
|
+
type,
|
|
59
|
+
})
|
|
60
|
+
const config = await getInitConfig(type, isQuick, port)
|
|
61
|
+
if (!isSingle) {
|
|
62
|
+
await downloadFiles()
|
|
63
|
+
await makeFiles.makeEnv(config)
|
|
64
|
+
} else {
|
|
65
|
+
await makeFiles.makeSingleCompose(config)
|
|
66
|
+
}
|
|
67
|
+
if (watchDir) {
|
|
68
|
+
await watchPlugins(watchDir)
|
|
69
|
+
}
|
|
70
|
+
if (genUser) {
|
|
71
|
+
await generateUser()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const { number } = require("../questions")
|
|
2
|
+
const { success, stringifyToDotEnv } = require("../utils")
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const path = require("path")
|
|
5
|
+
const randomString = require("randomstring")
|
|
6
|
+
const yaml = require("yaml")
|
|
7
|
+
const { getAppService } = require("./utils")
|
|
8
|
+
|
|
9
|
+
const SINGLE_IMAGE = "budibase/budibase:latest"
|
|
10
|
+
const VOL_NAME = "budibase_data"
|
|
11
|
+
const COMPOSE_PATH = path.resolve("./docker-compose.yaml")
|
|
12
|
+
const ENV_PATH = path.resolve("./.env")
|
|
13
|
+
|
|
14
|
+
function getSecrets(opts = { single: false }) {
|
|
15
|
+
const secrets = [
|
|
16
|
+
"JWT_SECRET",
|
|
17
|
+
"MINIO_ACCESS_KEY",
|
|
18
|
+
"MINIO_SECRET_KEY",
|
|
19
|
+
"REDIS_PASSWORD",
|
|
20
|
+
"INTERNAL_API_KEY",
|
|
21
|
+
]
|
|
22
|
+
const obj = {}
|
|
23
|
+
secrets.forEach(secret => (obj[secret] = randomString.generate()))
|
|
24
|
+
// setup couch creds separately
|
|
25
|
+
if (opts && opts.single) {
|
|
26
|
+
obj["COUCHDB_USER"] = "admin"
|
|
27
|
+
obj["COUCHDB_PASSWORD"] = randomString.generate()
|
|
28
|
+
} else {
|
|
29
|
+
obj["COUCH_DB_USER"] = "admin"
|
|
30
|
+
obj["COUCH_DB_PASSWORD"] = randomString.generate()
|
|
31
|
+
}
|
|
32
|
+
return obj
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getSingleCompose(port) {
|
|
36
|
+
const singleComposeObj = {
|
|
37
|
+
version: "3",
|
|
38
|
+
services: {
|
|
39
|
+
budibase: {
|
|
40
|
+
restart: "unless-stopped",
|
|
41
|
+
image: SINGLE_IMAGE,
|
|
42
|
+
ports: [`${port}:80`],
|
|
43
|
+
environment: getSecrets({ single: true }),
|
|
44
|
+
volumes: [`${VOL_NAME}:/data`],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
volumes: {
|
|
48
|
+
[VOL_NAME]: {
|
|
49
|
+
driver: "local",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
return yaml.stringify(singleComposeObj)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getEnv(port) {
|
|
57
|
+
const partOne = stringifyToDotEnv({
|
|
58
|
+
MAIN_PORT: port,
|
|
59
|
+
})
|
|
60
|
+
const partTwo = stringifyToDotEnv(getSecrets())
|
|
61
|
+
const partThree = stringifyToDotEnv({
|
|
62
|
+
APP_PORT: 4002,
|
|
63
|
+
WORKER_PORT: 4003,
|
|
64
|
+
MINIO_PORT: 4004,
|
|
65
|
+
COUCH_DB_PORT: 4005,
|
|
66
|
+
REDIS_PORT: 6379,
|
|
67
|
+
WATCHTOWER_PORT: 6161,
|
|
68
|
+
BUDIBASE_ENVIRONMENT: "PRODUCTION",
|
|
69
|
+
})
|
|
70
|
+
return [
|
|
71
|
+
"# Use the main port in the builder for your self hosting URL, e.g. localhost:10000",
|
|
72
|
+
partOne,
|
|
73
|
+
"# This section contains all secrets pertaining to the system",
|
|
74
|
+
partTwo,
|
|
75
|
+
"# This section contains variables that do not need to be altered under normal circumstances",
|
|
76
|
+
partThree,
|
|
77
|
+
].join("\n")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
exports.ENV_PATH = ENV_PATH
|
|
81
|
+
exports.COMPOSE_PATH = COMPOSE_PATH
|
|
82
|
+
|
|
83
|
+
module.exports.ConfigMap = {
|
|
84
|
+
MAIN_PORT: "port",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports.QUICK_CONFIG = {
|
|
88
|
+
key: "budibase",
|
|
89
|
+
port: 10000,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function make(path, contentsFn, inputs = {}) {
|
|
93
|
+
const port =
|
|
94
|
+
inputs.port ||
|
|
95
|
+
(await number(
|
|
96
|
+
"Please enter the port on which you want your installation to run: ",
|
|
97
|
+
10000
|
|
98
|
+
))
|
|
99
|
+
const fileContents = contentsFn(port)
|
|
100
|
+
fs.writeFileSync(path, fileContents)
|
|
101
|
+
console.log(
|
|
102
|
+
success(
|
|
103
|
+
`Configuration has been written successfully - please check ${path} for more details.`
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports.makeEnv = async (inputs = {}) => {
|
|
109
|
+
return make(ENV_PATH, getEnv, inputs)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports.makeSingleCompose = async (inputs = {}) => {
|
|
113
|
+
return make(COMPOSE_PATH, getSingleCompose, inputs)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports.getEnvProperty = property => {
|
|
117
|
+
const props = fs.readFileSync(ENV_PATH, "utf8").split(property)
|
|
118
|
+
if (props[0].charAt(0) === "=") {
|
|
119
|
+
property = props[0]
|
|
120
|
+
} else {
|
|
121
|
+
property = props[1]
|
|
122
|
+
}
|
|
123
|
+
return property.split("=")[1].split("\n")[0]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports.getComposeProperty = property => {
|
|
127
|
+
const { service } = getAppService(COMPOSE_PATH)
|
|
128
|
+
if (property === "port" && Array.isArray(service.ports)) {
|
|
129
|
+
const port = service.ports[0]
|
|
130
|
+
return port.split(":")[0]
|
|
131
|
+
} else if (service.environment) {
|
|
132
|
+
return service.environment[property]
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const {
|
|
2
|
+
checkDockerConfigured,
|
|
3
|
+
checkInitComplete,
|
|
4
|
+
handleError,
|
|
5
|
+
} = require("./utils")
|
|
6
|
+
const { info, success } = require("../utils")
|
|
7
|
+
const makeFiles = require("./makeFiles")
|
|
8
|
+
const compose = require("docker-compose")
|
|
9
|
+
const fs = require("fs")
|
|
10
|
+
|
|
11
|
+
exports.start = async () => {
|
|
12
|
+
await checkDockerConfigured()
|
|
13
|
+
checkInitComplete()
|
|
14
|
+
console.log(
|
|
15
|
+
info(
|
|
16
|
+
"Starting services, this may take a moment - first time this may take a few minutes to download images."
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
let port
|
|
20
|
+
if (fs.existsSync(makeFiles.ENV_PATH)) {
|
|
21
|
+
port = makeFiles.getEnvProperty("MAIN_PORT")
|
|
22
|
+
} else {
|
|
23
|
+
port = makeFiles.getComposeProperty("port")
|
|
24
|
+
}
|
|
25
|
+
await handleError(async () => {
|
|
26
|
+
// need to log as it makes it more clear
|
|
27
|
+
await compose.upAll({ cwd: "./", log: true })
|
|
28
|
+
})
|
|
29
|
+
console.log(
|
|
30
|
+
success(
|
|
31
|
+
`Services started, please go to http://localhost:${port} for next steps.`
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const {
|
|
2
|
+
checkDockerConfigured,
|
|
3
|
+
checkInitComplete,
|
|
4
|
+
handleError,
|
|
5
|
+
} = require("./utils")
|
|
6
|
+
const { info } = require("../utils")
|
|
7
|
+
const compose = require("docker-compose")
|
|
8
|
+
|
|
9
|
+
exports.status = async () => {
|
|
10
|
+
await checkDockerConfigured()
|
|
11
|
+
checkInitComplete()
|
|
12
|
+
console.log(info("Budibase status"))
|
|
13
|
+
await handleError(async () => {
|
|
14
|
+
const response = await compose.ps()
|
|
15
|
+
console.log(response.out)
|
|
16
|
+
})
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const {
|
|
2
|
+
checkDockerConfigured,
|
|
3
|
+
checkInitComplete,
|
|
4
|
+
handleError,
|
|
5
|
+
} = require("./utils")
|
|
6
|
+
const { info, success } = require("../utils")
|
|
7
|
+
const compose = require("docker-compose")
|
|
8
|
+
|
|
9
|
+
exports.stop = async () => {
|
|
10
|
+
await checkDockerConfigured()
|
|
11
|
+
checkInitComplete()
|
|
12
|
+
console.log(info("Stopping services, this may take a moment."))
|
|
13
|
+
await handleError(async () => {
|
|
14
|
+
await compose.stop()
|
|
15
|
+
})
|
|
16
|
+
console.log(success("Services have been stopped successfully."))
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const {
|
|
2
|
+
checkDockerConfigured,
|
|
3
|
+
checkInitComplete,
|
|
4
|
+
downloadFiles,
|
|
5
|
+
handleError,
|
|
6
|
+
getServices,
|
|
7
|
+
} = require("./utils")
|
|
8
|
+
const { confirmation } = require("../questions")
|
|
9
|
+
const compose = require("docker-compose")
|
|
10
|
+
const { COMPOSE_PATH } = require("./makeFiles")
|
|
11
|
+
const { info, success } = require("../utils")
|
|
12
|
+
const { start } = require("./start")
|
|
13
|
+
|
|
14
|
+
const BB_COMPOSE_SERVICES = ["app-service", "worker-service", "proxy-service"]
|
|
15
|
+
const BB_SINGLE_SERVICE = ["budibase"]
|
|
16
|
+
|
|
17
|
+
exports.update = async () => {
|
|
18
|
+
const { services } = getServices(COMPOSE_PATH)
|
|
19
|
+
const isSingle = Object.keys(services).length === 1
|
|
20
|
+
await checkDockerConfigured()
|
|
21
|
+
checkInitComplete()
|
|
22
|
+
if (
|
|
23
|
+
!isSingle &&
|
|
24
|
+
(await confirmation("Do you wish to update you docker-compose.yaml?"))
|
|
25
|
+
) {
|
|
26
|
+
await downloadFiles()
|
|
27
|
+
}
|
|
28
|
+
await handleError(async () => {
|
|
29
|
+
const status = await compose.ps()
|
|
30
|
+
const parts = status.out.split("\n")
|
|
31
|
+
const isUp = parts[2] && parts[2].indexOf("Up") !== -1
|
|
32
|
+
if (isUp) {
|
|
33
|
+
console.log(info("Stopping services, this may take a moment."))
|
|
34
|
+
await compose.stop()
|
|
35
|
+
}
|
|
36
|
+
console.log(info("Beginning update, this may take a few minutes."))
|
|
37
|
+
let services
|
|
38
|
+
if (isSingle) {
|
|
39
|
+
services = BB_SINGLE_SERVICE
|
|
40
|
+
} else {
|
|
41
|
+
services = BB_COMPOSE_SERVICES
|
|
42
|
+
}
|
|
43
|
+
await compose.pullMany(services, { log: true })
|
|
44
|
+
if (isUp) {
|
|
45
|
+
console.log(success("Update complete, restarting services..."))
|
|
46
|
+
await start()
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { lookpath } = require("lookpath")
|
|
2
|
+
const fs = require("fs")
|
|
3
|
+
const makeFiles = require("./makeFiles")
|
|
4
|
+
const { logErrorToFile, downloadFile, error } = require("../utils")
|
|
5
|
+
const yaml = require("yaml")
|
|
6
|
+
|
|
7
|
+
const ERROR_FILE = "docker-error.log"
|
|
8
|
+
const FILE_URLS = [
|
|
9
|
+
"https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
exports.downloadFiles = async () => {
|
|
13
|
+
const promises = []
|
|
14
|
+
for (let url of FILE_URLS) {
|
|
15
|
+
const fileName = url.split("/").slice(-1)[0]
|
|
16
|
+
promises.push(downloadFile(url, `./${fileName}`))
|
|
17
|
+
}
|
|
18
|
+
await Promise.all(promises)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
exports.checkDockerConfigured = async () => {
|
|
22
|
+
const error =
|
|
23
|
+
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
|
|
24
|
+
const docker = await lookpath("docker")
|
|
25
|
+
const compose = await lookpath("docker-compose")
|
|
26
|
+
if (!docker || !compose) {
|
|
27
|
+
throw error
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
exports.checkInitComplete = () => {
|
|
32
|
+
if (
|
|
33
|
+
!fs.existsSync(makeFiles.ENV_PATH) &&
|
|
34
|
+
!fs.existsSync(makeFiles.COMPOSE_PATH)
|
|
35
|
+
) {
|
|
36
|
+
throw "Please run the hosting --init command before any other hosting command."
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
exports.handleError = async func => {
|
|
41
|
+
try {
|
|
42
|
+
await func()
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err && err.err) {
|
|
45
|
+
logErrorToFile(ERROR_FILE, err.err)
|
|
46
|
+
}
|
|
47
|
+
throw `Failed to start - logs written to file: ${ERROR_FILE}`
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
exports.getServices = path => {
|
|
52
|
+
const dockerYaml = fs.readFileSync(path, "utf8")
|
|
53
|
+
const parsedYaml = yaml.parse(dockerYaml)
|
|
54
|
+
return { yaml: parsedYaml, services: parsedYaml.services }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
exports.getAppService = path => {
|
|
58
|
+
const { yaml, services } = exports.getServices(path),
|
|
59
|
+
serviceList = Object.keys(services)
|
|
60
|
+
let service
|
|
61
|
+
if (services["app-service"]) {
|
|
62
|
+
service = services["app-service"]
|
|
63
|
+
} else if (serviceList.length === 1) {
|
|
64
|
+
service = services[serviceList[0]]
|
|
65
|
+
}
|
|
66
|
+
return { yaml, service }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
exports.updateDockerComposeService = updateFn => {
|
|
70
|
+
const opts = ["docker-compose.yaml", "docker-compose.yml"]
|
|
71
|
+
const dockerFilePath = opts.find(name => fs.existsSync(name))
|
|
72
|
+
if (!dockerFilePath) {
|
|
73
|
+
console.log(error("Unable to locate docker-compose YAML."))
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
const { yaml: parsedYaml, service } = exports.getAppService(dockerFilePath)
|
|
77
|
+
if (!service) {
|
|
78
|
+
console.log(
|
|
79
|
+
error(
|
|
80
|
+
"Unable to locate service within compose file, is it a valid Budibase configuration?"
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
updateFn(service)
|
|
86
|
+
fs.writeFileSync(dockerFilePath, yaml.stringify(parsedYaml))
|
|
87
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { resolve } = require("path")
|
|
2
|
+
const fs = require("fs")
|
|
3
|
+
const { error, success } = require("../utils")
|
|
4
|
+
const { updateDockerComposeService } = require("./utils")
|
|
5
|
+
|
|
6
|
+
exports.watchPlugins = async pluginPath => {
|
|
7
|
+
const PLUGIN_PATH = "/plugins"
|
|
8
|
+
// get absolute path
|
|
9
|
+
pluginPath = resolve(pluginPath)
|
|
10
|
+
if (!fs.existsSync(pluginPath)) {
|
|
11
|
+
console.log(
|
|
12
|
+
error(
|
|
13
|
+
`The directory "${pluginPath}" does not exist, please create and then try again.`
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
updateDockerComposeService(service => {
|
|
19
|
+
// set environment variable
|
|
20
|
+
service.environment["PLUGINS_DIR"] = PLUGIN_PATH
|
|
21
|
+
// add volumes to parsed yaml
|
|
22
|
+
if (!service.volumes) {
|
|
23
|
+
service.volumes = []
|
|
24
|
+
}
|
|
25
|
+
const found = service.volumes.find(vol => vol.includes(PLUGIN_PATH))
|
|
26
|
+
if (found) {
|
|
27
|
+
service.volumes.splice(service.volumes.indexOf(found), 1)
|
|
28
|
+
}
|
|
29
|
+
service.volumes.push(`${pluginPath}:${PLUGIN_PATH}`)
|
|
30
|
+
})
|
|
31
|
+
console.log(
|
|
32
|
+
success(`Docker compose configured to watch directory: ${pluginPath}`)
|
|
33
|
+
)
|
|
34
|
+
}
|
package/src/plugins/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const Command = require("../structures/Command")
|
|
2
|
-
const { CommandWords, AnalyticsEvents } = require("../constants")
|
|
2
|
+
const { CommandWords, AnalyticsEvents, InitTypes } = require("../constants")
|
|
3
3
|
const { getSkeleton, fleshOutSkeleton } = require("./skeleton")
|
|
4
4
|
const questions = require("../questions")
|
|
5
5
|
const fs = require("fs")
|
|
@@ -9,6 +9,9 @@ const { runPkgCommand } = require("../exec")
|
|
|
9
9
|
const { join } = require("path")
|
|
10
10
|
const { success, error, info, moveDirectory } = require("../utils")
|
|
11
11
|
const { captureEvent } = require("../events")
|
|
12
|
+
const fp = require("find-free-port")
|
|
13
|
+
const { init: hostingInit } = require("../hosting/init")
|
|
14
|
+
const { start: hostingStart } = require("../hosting/start")
|
|
12
15
|
|
|
13
16
|
function checkInPlugin() {
|
|
14
17
|
if (!fs.existsSync("package.json")) {
|
|
@@ -141,6 +144,19 @@ async function watch() {
|
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
async function dev() {
|
|
148
|
+
const pluginDir = await questions.string("Directory to watch", "./")
|
|
149
|
+
const [port] = await fp(10000)
|
|
150
|
+
await hostingInit({
|
|
151
|
+
init: InitTypes.QUICK,
|
|
152
|
+
single: true,
|
|
153
|
+
watchPluginDir: pluginDir,
|
|
154
|
+
genUser: true,
|
|
155
|
+
port,
|
|
156
|
+
})
|
|
157
|
+
await hostingStart()
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
const command = new Command(`${CommandWords.PLUGIN}`)
|
|
145
161
|
.addHelp(
|
|
146
162
|
"Custom plugins for Budibase, init, build and verify your components and datasources with this tool."
|
|
@@ -160,5 +176,10 @@ const command = new Command(`${CommandWords.PLUGIN}`)
|
|
|
160
176
|
"Automatically build any changes to your plugin.",
|
|
161
177
|
watch
|
|
162
178
|
)
|
|
179
|
+
.addSubOption(
|
|
180
|
+
"--dev",
|
|
181
|
+
"Run a development environment which automatically watches the current directory.",
|
|
182
|
+
dev
|
|
183
|
+
)
|
|
163
184
|
|
|
164
185
|
exports.command = command
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
getSubHelpDescription,
|
|
3
|
+
getHelpDescription,
|
|
4
|
+
error,
|
|
5
|
+
capitaliseFirstLetter,
|
|
6
|
+
} = require("../utils")
|
|
2
7
|
|
|
3
8
|
class Command {
|
|
4
9
|
constructor(command, func = null) {
|
|
@@ -8,6 +13,15 @@ class Command {
|
|
|
8
13
|
this.func = func
|
|
9
14
|
}
|
|
10
15
|
|
|
16
|
+
convertToCommander(lookup) {
|
|
17
|
+
const parts = lookup.toLowerCase().split("-")
|
|
18
|
+
// camel case, separate out first
|
|
19
|
+
const first = parts.shift()
|
|
20
|
+
return [first]
|
|
21
|
+
.concat(parts.map(part => capitaliseFirstLetter(part)))
|
|
22
|
+
.join("")
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
addHelp(help) {
|
|
12
26
|
this.help = help
|
|
13
27
|
return this
|
|
@@ -25,10 +39,7 @@ class Command {
|
|
|
25
39
|
command = command.description(getHelpDescription(thisCmd.help))
|
|
26
40
|
}
|
|
27
41
|
for (let opt of thisCmd.opts) {
|
|
28
|
-
command = command.option(
|
|
29
|
-
`${opt.command}`,
|
|
30
|
-
getSubHelpDescription(opt.help)
|
|
31
|
-
)
|
|
42
|
+
command = command.option(opt.command, getSubHelpDescription(opt.help))
|
|
32
43
|
}
|
|
33
44
|
command.helpOption(
|
|
34
45
|
"--help",
|
|
@@ -36,17 +47,25 @@ class Command {
|
|
|
36
47
|
)
|
|
37
48
|
command.action(async options => {
|
|
38
49
|
try {
|
|
39
|
-
let executed = false
|
|
50
|
+
let executed = false,
|
|
51
|
+
found = false
|
|
40
52
|
for (let opt of thisCmd.opts) {
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
let lookup = opt.command.split(" ")[0].replace("--", "")
|
|
54
|
+
// need to handle how commander converts watch-plugin-dir to watchPluginDir
|
|
55
|
+
lookup = this.convertToCommander(lookup)
|
|
56
|
+
found = !executed && options[lookup]
|
|
57
|
+
if (found && opt.func) {
|
|
43
58
|
const input =
|
|
44
59
|
Object.keys(options).length > 1 ? options : options[lookup]
|
|
45
60
|
await opt.func(input)
|
|
46
61
|
executed = true
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
|
-
if (!executed) {
|
|
64
|
+
if (found && !executed) {
|
|
65
|
+
console.log(
|
|
66
|
+
error(`${Object.keys(options)[0]} is an option, not an operation.`)
|
|
67
|
+
)
|
|
68
|
+
} else if (!executed) {
|
|
50
69
|
console.log(error(`Unknown ${this.command} option.`))
|
|
51
70
|
command.help()
|
|
52
71
|
}
|
package/src/utils.js
CHANGED
|
@@ -84,3 +84,15 @@ exports.moveDirectory = (oldPath, newPath) => {
|
|
|
84
84
|
}
|
|
85
85
|
fs.rmdirSync(oldPath)
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
exports.capitaliseFirstLetter = str => {
|
|
89
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
exports.stringifyToDotEnv = json => {
|
|
93
|
+
let str = ""
|
|
94
|
+
for (let [key, value] of Object.entries(json)) {
|
|
95
|
+
str += `${key}=${value}\n`
|
|
96
|
+
}
|
|
97
|
+
return str
|
|
98
|
+
}
|
package/src/hosting/makeEnv.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
const { number } = require("../questions")
|
|
2
|
-
const { success } = require("../utils")
|
|
3
|
-
const fs = require("fs")
|
|
4
|
-
const path = require("path")
|
|
5
|
-
const randomString = require("randomstring")
|
|
6
|
-
|
|
7
|
-
const FILE_PATH = path.resolve("./.env")
|
|
8
|
-
|
|
9
|
-
function getContents(port) {
|
|
10
|
-
return `
|
|
11
|
-
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
|
12
|
-
MAIN_PORT=${port}
|
|
13
|
-
|
|
14
|
-
# This section contains all secrets pertaining to the system
|
|
15
|
-
JWT_SECRET=${randomString.generate()}
|
|
16
|
-
MINIO_ACCESS_KEY=${randomString.generate()}
|
|
17
|
-
MINIO_SECRET_KEY=${randomString.generate()}
|
|
18
|
-
COUCH_DB_PASSWORD=${randomString.generate()}
|
|
19
|
-
COUCH_DB_USER=${randomString.generate()}
|
|
20
|
-
REDIS_PASSWORD=${randomString.generate()}
|
|
21
|
-
INTERNAL_API_KEY=${randomString.generate()}
|
|
22
|
-
|
|
23
|
-
# This section contains variables that do not need to be altered under normal circumstances
|
|
24
|
-
APP_PORT=4002
|
|
25
|
-
WORKER_PORT=4003
|
|
26
|
-
MINIO_PORT=4004
|
|
27
|
-
COUCH_DB_PORT=4005
|
|
28
|
-
REDIS_PORT=6379
|
|
29
|
-
WATCHTOWER_PORT=6161
|
|
30
|
-
BUDIBASE_ENVIRONMENT=PRODUCTION`
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
module.exports.filePath = FILE_PATH
|
|
34
|
-
module.exports.ConfigMap = {
|
|
35
|
-
MAIN_PORT: "port",
|
|
36
|
-
}
|
|
37
|
-
module.exports.QUICK_CONFIG = {
|
|
38
|
-
key: "budibase",
|
|
39
|
-
port: 10000,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
module.exports.make = async (inputs = {}) => {
|
|
43
|
-
const hostingPort =
|
|
44
|
-
inputs.port ||
|
|
45
|
-
(await number(
|
|
46
|
-
"Please enter the port on which you want your installation to run: ",
|
|
47
|
-
10000
|
|
48
|
-
))
|
|
49
|
-
const fileContents = getContents(hostingPort)
|
|
50
|
-
fs.writeFileSync(FILE_PATH, fileContents)
|
|
51
|
-
console.log(
|
|
52
|
-
success(
|
|
53
|
-
"Configuration has been written successfully - please check .env file for more details."
|
|
54
|
-
)
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
module.exports.get = property => {
|
|
59
|
-
const props = fs.readFileSync(FILE_PATH, "utf8").split(property)
|
|
60
|
-
if (props[0].charAt(0) === "=") {
|
|
61
|
-
property = props[0]
|
|
62
|
-
} else {
|
|
63
|
-
property = props[1]
|
|
64
|
-
}
|
|
65
|
-
return property.split("=")[1].split("\n")[0]
|
|
66
|
-
}
|