Update after testing, it is now possible to make a deployment to a self hosted environment. Some work still required, better authentication around MINIO deployment, currently the bucket is set to public read and there is no signing/verification to the upload process, also right now four different URLs are needed for the builder to connect correctly, ideally this shouldn't be the case.

This commit is contained in:
mike12345567 2020-12-18 12:54:20 +00:00
parent 84a16e2dfb
commit 31bc45985f
16 changed files with 89 additions and 79 deletions

View File

@ -8,14 +8,14 @@ services:
ports: ports:
- "${APP_PORT}:${APP_PORT}" - "${APP_PORT}:${APP_PORT}"
environment: environment:
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
MINIO_URL: http://nginx-service:${MINIO_PORT}
SELF_HOSTED: 1 SELF_HOSTED: 1
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984 COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT} BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
LOGO_URL: ${LOGO_URL} LOGO_URL: ${LOGO_URL}
PORT: ${APP_PORT} PORT: ${APP_PORT}
HOSTING_URL: ${HOSTING_URL}
MINIO_PORT: ${MINIO_PORT}
JWT_SECRET: ${JWT_SECRET}
depends_on: depends_on:
- worker-service - worker-service
@ -29,7 +29,7 @@ services:
PORT: ${WORKER_PORT} PORT: ${WORKER_PORT}
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
RAW_MINIO_URL: http://nginx-service:${MINIO_PORT} RAW_MINIO_URL: http://nginx-service:5001
COUCH_DB_USERNAME: ${COUCH_DB_USER} COUCH_DB_USERNAME: ${COUCH_DB_USER}
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD} COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
RAW_COUCH_DB_URL: http://couchdb-service:5984 RAW_COUCH_DB_URL: http://couchdb-service:5984

View File

@ -4,9 +4,10 @@ COUCH_DB_PASSWORD=budibase
COUCH_DB_USER=budibase COUCH_DB_USER=budibase
WORKER_API_KEY=budibase WORKER_API_KEY=budibase
BUDIBASE_ENVIRONMENT=PRODUCTION BUDIBASE_ENVIRONMENT=PRODUCTION
HOSTING_URL="http://localhost:4001" HOSTING_URL="http://localhost"
LOGO_URL=https://logoipsum.com/logo/logo-15.svg LOGO_URL=https://logoipsum.com/logo/logo-15.svg
APP_PORT=4002 APP_PORT=4002
MINIO_PORT=4003 WORKER_PORT=4003
COUCH_DB_PORT=4004 MINIO_PORT=4004
WORKER_PORT=4006 COUCH_DB_PORT=4005
JWT_SECRET=testsecret

View File

@ -17,7 +17,7 @@ export const getHostingStore = () => {
const [info, urls] = await Promise.all(responses.map(resp => resp.json())) const [info, urls] = await Promise.all(responses.map(resp => resp.json()))
store.update(state => { store.update(state => {
state.hostingInfo = info state.hostingInfo = info
state.appUrl = urls.appServer state.appUrl = urls.app
return state return state
}) })
return info return info

View File

@ -39,8 +39,10 @@
</p> </p>
<Toggle thin text="Self hosted" bind:checked={selfhosted} /> <Toggle thin text="Self hosted" bind:checked={selfhosted} />
{#if selfhosted} {#if selfhosted}
<Input bind:value={hostingInfo.appServerUrl} label="Apps URL" /> <Input bind:value={hostingInfo.appUrl} label="Apps URL" />
<Input bind:value={hostingInfo.deploymentServerUrl} label="Deployments URL" /> <Input bind:value={hostingInfo.workerUrl} label="Workers URL" />
<Input bind:value={hostingInfo.minioUrl} label="MINIO URL" />
<Input bind:value={hostingInfo.couchUrl} label="CouchDB URL" />
<Toggle thin text="HTTPS" bind:checked={hostingInfo.useHttps} /> <Toggle thin text="HTTPS" bind:checked={hostingInfo.useHttps} />
{/if} {/if}
</ModalContent> </ModalContent>

View File

@ -35,8 +35,8 @@ exports.authenticate = async ctx => {
roleId: dbUser.roleId, roleId: dbUser.roleId,
version: app.version, version: app.version,
} }
// if in cloud add the user api key // if in cloud add the user api key, unless self hosted
if (env.CLOUD) { if (env.CLOUD && !env.SELF_HOSTED) {
const { apiKey } = await getAPIKey(ctx.user.appId) const { apiKey } = await getAPIKey(ctx.user.appId)
payload.apiKey = apiKey payload.apiKey = apiKey
} }

View File

@ -37,10 +37,10 @@ class Deployment {
if (!verification) { if (!verification) {
return return
} }
this.verification = verification
if (this.verification.quota) { if (this.verification.quota) {
this.quota = this.verification.quota this.quota = this.verification.quota
} }
this.verification = verification
} }
getVerification() { getVerification() {

View File

@ -24,7 +24,7 @@ async function checkAllDeployments(deployments) {
deployment.status === DeploymentStatus.PENDING && deployment.status === DeploymentStatus.PENDING &&
Date.now() - deployment.updatedAt > MAX_PENDING_TIME_MS Date.now() - deployment.updatedAt > MAX_PENDING_TIME_MS
) { ) {
deployment.status = status deployment.status = DeploymentStatus.FAILURE
deployment.err = "Timed out" deployment.err = "Timed out"
updated = true updated = true
} }

View File

@ -5,23 +5,26 @@ const {
performReplication, performReplication,
fetchCredentials, fetchCredentials,
} = require("./utils") } = require("./utils")
const { getDeploymentUrl } = require("../../../utilities/builder/hosting") const {
const { join } = require("path") getWorkerUrl,
getCouchUrl,
getMinioUrl,
} = require("../../../utilities/builder/hosting")
exports.preDeployment = async function() { exports.preDeployment = async function() {
const url = join(await getDeploymentUrl(), "api", "deploy") const url = `${await getWorkerUrl()}/api/deploy`
const json = await fetchCredentials(url, { const json = await fetchCredentials(url, {
apiKey: env.BUDIBASE_API_KEY, apiKey: env.BUDIBASE_API_KEY,
}) })
// response contains: // response contains:
// couchDbSession, bucket, objectStoreSession, couchDbUrl, objectStoreUrl // couchDbSession, bucket, objectStoreSession
// set credentials here, means any time we're verified we're ready to go // set credentials here, means any time we're verified we're ready to go
if (json.objectStoreSession) { if (json.objectStoreSession) {
AWS.config.update({ AWS.config.update({
accessKeyId: json.objectStoreSession.AccessKeyId, accessKeyId: json.objectStoreSession.accessKeyId,
secretAccessKey: json.objectStoreSession.SecretAccessKey, secretAccessKey: json.objectStoreSession.secretAccessKey,
}) })
} }
@ -36,7 +39,7 @@ exports.deploy = async function(deployment) {
const appId = deployment.getAppId() const appId = deployment.getAppId()
const verification = deployment.getVerification() const verification = deployment.getVerification()
const objClient = new AWS.S3({ const objClient = new AWS.S3({
endpoint: verification.objectStoreUrl, endpoint: await getMinioUrl(),
s3ForcePathStyle: true, // needed with minio? s3ForcePathStyle: true, // needed with minio?
signatureVersion: "v4", signatureVersion: "v4",
params: { params: {
@ -54,6 +57,6 @@ exports.replicateDb = async function(deployment) {
return performReplication( return performReplication(
appId, appId,
verification.couchDbSession, verification.couchDbSession,
verification.couchDbUrl await getCouchUrl()
) )
} }

View File

@ -3,6 +3,7 @@ const sanitize = require("sanitize-s3-objectkey")
const { walkDir } = require("../../../utilities") const { walkDir } = require("../../../utilities")
const { join } = require("../../../utilities/centralPath") const { join } = require("../../../utilities/centralPath")
const { budibaseAppsDir } = require("../../../utilities/budibaseDir") const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
const fetch = require("node-fetch")
const PouchDB = require("../../../db") const PouchDB = require("../../../db")
const CouchDB = require("pouchdb") const CouchDB = require("pouchdb")

View File

@ -3,7 +3,7 @@ const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants")
const { const {
getHostingInfo, getHostingInfo,
HostingTypes, HostingTypes,
getAppServerUrl, getAppUrl,
} = require("../../utilities/builder/hosting") } = require("../../utilities/builder/hosting")
exports.fetchInfo = async ctx => { exports.fetchInfo = async ctx => {
@ -30,6 +30,6 @@ exports.fetch = async ctx => {
exports.fetchUrls = async ctx => { exports.fetchUrls = async ctx => {
ctx.body = { ctx.body = {
appServer: await getAppServerUrl(ctx.appId), app: await getAppUrl(ctx.appId),
} }
} }

View File

@ -17,17 +17,10 @@ const setBuilderToken = require("../../../utilities/builder/setBuilderToken")
const fileProcessor = require("../../../utilities/fileProcessor") const fileProcessor = require("../../../utilities/fileProcessor")
const env = require("../../../environment") const env = require("../../../environment")
function appServerUrl(appId) {
if (env.SELF_HOSTED) {
return env.HOSTING_URL
} else {
return `https://${appId}.app.budi.live`
}
}
function objectStoreUrl() { function objectStoreUrl() {
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
return env.MINIO_URL // TODO: need a better way to handle this, probably reverse proxy
return `${env.HOSTING_URL}:${env.MINIO_PORT}/app-assets/assets`
} else { } else {
return "https://cdn.app.budi.live/assets" return "https://cdn.app.budi.live/assets"
} }
@ -164,7 +157,7 @@ exports.serveApp = async function(ctx) {
title: appInfo.name, title: appInfo.name,
production: env.CLOUD, production: env.CLOUD,
appId: ctx.params.appId, appId: ctx.params.appId,
appServerUrl: appServerUrl(ctx.params.appId), objectStoreUrl: objectStoreUrl(ctx.params.appId),
}) })
const template = handlebars.compile( const template = handlebars.compile(
@ -232,8 +225,7 @@ exports.serveComponentLibrary = async function(ctx) {
} }
const S3_URL = encodeURI( const S3_URL = encodeURI(
join( join(
appServerUrl(appId), objectStoreUrl(appId),
"assets",
componentLib, componentLib,
ctx.query.library, ctx.query.library,
"dist", "dist",

View File

@ -4,11 +4,11 @@
export let appId export let appId
export let production export let production
export let appServerUrl export let objectStoreUrl
function publicPath(path) { function publicPath(path) {
if (production) { if (production) {
return `${appServerUrl}/assets/${appId}/${path}` return `${objectStoreUrl}/${appId}/${path}`
} }
return `/assets/${path}` return `/assets/${path}`

View File

@ -37,10 +37,8 @@ module.exports = {
DEPLOYMENT_DB_URL: process.env.DEPLOYMENT_DB_URL, DEPLOYMENT_DB_URL: process.env.DEPLOYMENT_DB_URL,
LOCAL_TEMPLATES: process.env.LOCAL_TEMPLATES, LOCAL_TEMPLATES: process.env.LOCAL_TEMPLATES,
// self hosting features // self hosting features
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, HOSTING_URL: process.env.HOSTING_URL,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_PORT: process.env.MINIO_PORT,
MINIO_URL: process.MINIO_URL,
HOSTING_URL: process.HOSTING_URL,
LOGO_URL: process.env.LOGO_URL, LOGO_URL: process.env.LOGO_URL,
_set(key, value) { _set(key, value) {
process.env[key] = value process.env[key] = value

View File

@ -1,6 +1,5 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants") const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants")
const { join } = require("path")
function getProtocol(hostingInfo) { function getProtocol(hostingInfo) {
return hostingInfo.useHttps ? "https://" : "http://" return hostingInfo.useHttps ? "https://" : "http://"
@ -23,8 +22,10 @@ exports.getHostingInfo = async () => {
doc = { doc = {
_id: HOSTING_DOC, _id: HOSTING_DOC,
type: exports.HostingTypes.CLOUD, type: exports.HostingTypes.CLOUD,
appServerUrl: "app.budi.live", appUrl: "app.budi.live",
deploymentServerUrl: "", workerUrl: "",
minioUrl: "",
couchUrl: "",
templatesUrl: "prod-budi-templates.s3-eu-west-1.amazonaws.com", templatesUrl: "prod-budi-templates.s3-eu-west-1.amazonaws.com",
useHttps: true, useHttps: true,
} }
@ -32,22 +33,34 @@ exports.getHostingInfo = async () => {
return doc return doc
} }
exports.getAppServerUrl = async appId => { exports.getAppUrl = async appId => {
const hostingInfo = await exports.getHostingInfo() const hostingInfo = await exports.getHostingInfo()
const protocol = getProtocol(hostingInfo) const protocol = getProtocol(hostingInfo)
let url let url
if (hostingInfo.type === "cloud") { if (hostingInfo.type === "cloud") {
url = `${protocol}${appId}.${hostingInfo.appServerUrl}` url = `${protocol}${appId}.${hostingInfo.appUrl}`
} else { } else {
url = `${protocol}${hostingInfo.appServerUrl}` url = `${protocol}${hostingInfo.appUrl}`
} }
return url return url
} }
exports.getDeploymentUrl = async () => { exports.getWorkerUrl = async () => {
const hostingInfo = await exports.getHostingInfo() const hostingInfo = await exports.getHostingInfo()
const protocol = getProtocol(hostingInfo) const protocol = getProtocol(hostingInfo)
return `${protocol}${hostingInfo.deploymentServerUrl}` return `${protocol}${hostingInfo.workerUrl}`
}
exports.getMinioUrl = async () => {
const hostingInfo = await exports.getHostingInfo()
const protocol = getProtocol(hostingInfo)
return `${protocol}${hostingInfo.minioUrl}`
}
exports.getCouchUrl = async () => {
const hostingInfo = await exports.getHostingInfo()
const protocol = getProtocol(hostingInfo)
return `${protocol}${hostingInfo.couchUrl}`
} }
exports.getTemplatesUrl = async (appId, type, name) => { exports.getTemplatesUrl = async (appId, type, name) => {
@ -55,9 +68,9 @@ exports.getTemplatesUrl = async (appId, type, name) => {
const protocol = getProtocol(hostingInfo) const protocol = getProtocol(hostingInfo)
let path let path
if (type && name) { if (type && name) {
path = join("templates", type, `${name}.tar.gz`) path = `templates/type/${name}.tar.gz`
} else { } else {
path = "manifest.json" path = "manifest.json"
} }
return join(`${protocol}${hostingInfo.templatesUrl}`, path) return `${protocol}${hostingInfo.templatesUrl}/${path}`
} }

View File

@ -5,11 +5,23 @@ const AWS = require("aws-sdk")
const APP_BUCKET = "app-assets" const APP_BUCKET = "app-assets"
// this doesn't matter in self host // this doesn't matter in self host
const REGION = "eu-west-1" const REGION = "eu-west-1"
const PUBLIC_READ_POLICY = {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: "*",
Action: "s3:GetObject",
Resource: `arn:aws:s3:::${APP_BUCKET}/*`,
}
]
}
async function getCouchSession() { async function getCouchSession() {
// fetch session token for the api user // fetch session token for the api user
const session = await got.post(`${env.RAW_COUCH_DB_URL}/_session`, { const session = await got.post(`${env.RAW_COUCH_DB_URL}/_session`, {
responseType: "json", responseType: "json",
credentials: "include",
json: { json: {
username: env.COUCH_DB_USERNAME, username: env.COUCH_DB_USERNAME,
password: env.COUCH_DB_PASSWORD, password: env.COUCH_DB_PASSWORD,
@ -38,37 +50,25 @@ async function getMinioSession() {
}) })
// make sure the bucket exists // make sure the bucket exists
try { try {
await objClient.headBucket({ Bucket: APP_BUCKET }).promise() await objClient.headBucket({
Bucket: APP_BUCKET
}).promise()
await objClient.putBucketPolicy({
Bucket: APP_BUCKET,
Policy: JSON.stringify(PUBLIC_READ_POLICY),
}).promise()
} catch (err) { } catch (err) {
// bucket doesn't exist create it // bucket doesn't exist create it
if (err.statusCode === 404) { if (err.statusCode === 404) {
await objClient.createBucket({ Bucket: APP_BUCKET }).promise() await objClient.createBucket({
Bucket: APP_BUCKET,
}).promise()
} else { } else {
throw err throw err
} }
} }
// TODO: this doesn't seem to work get an error
// TODO: Generating temporary credentials not allowed for this request.
// TODO: this should work based on minio documentation
// const sts = new AWS.STS({
// endpoint: env.RAW_MINIO_URL,
// region: REGION,
// s3ForcePathStyle: true,
// })
// // NOTE: In the following commands RoleArn and RoleSessionName are not meaningful for MinIO
// const params = {
// DurationSeconds: 3600,
// ExternalId: "123ABC",
// Policy: '{"Version":"2012-10-17","Statement":[{"Sid":"Stmt1","Effect":"Allow","Action":"s3:*","Resource":"arn:aws:s3:::*"}]}',
// RoleArn: 'arn:xxx:xxx:xxx:xxxx',
// RoleSessionName: 'anything',
// };
// const assumedRole = await sts.assumeRole(params).promise();
// if (!assumedRole) {
// throw "Unable to get access to object store."
// }
// return assumedRole.Credentials
// TODO: need to do something better than this // TODO: need to do something better than this
// Ideally want to send back some pre-signed URLs for files that are to be uploaded
return { return {
accessKeyId: env.MINIO_ACCESS_KEY, accessKeyId: env.MINIO_ACCESS_KEY,
secretAccessKey: env.MINIO_SECRET_KEY, secretAccessKey: env.MINIO_SECRET_KEY,
@ -80,7 +80,5 @@ exports.deploy = async ctx => {
couchDbSession: await getCouchSession(), couchDbSession: await getCouchSession(),
bucket: APP_BUCKET, bucket: APP_BUCKET,
objectStoreSession: await getMinioSession(), objectStoreSession: await getMinioSession(),
couchDbUrl: env.RAW_COUCH_DB_URL,
objectStoreUrl: env.RAW_MINIO_URL,
} }
} }

View File

@ -4,10 +4,12 @@ module.exports = {
PORT: process.env.PORT, PORT: process.env.PORT,
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
RAW_MINIO_URL: process.env.RAW_MINIO_URL,
COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME, COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME,
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
RAW_COUCH_DB_URL: process.env.RAW_COUCH_DB_URL, RAW_COUCH_DB_URL: process.env.RAW_COUCH_DB_URL,
RAW_MINIO_URL: process.env.RAW_MINIO_URL,
COUCH_DB_PORT: process.env.COUCH_DB_PORT,
MINIO_PORT: process.env.MINIO_PORT,
_set(key, value) { _set(key, value) {
process.env[key] = value process.env[key] = value
module.exports[key] = value module.exports[key] = value