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:
- "${APP_PORT}:${APP_PORT}"
environment:
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
MINIO_URL: http://nginx-service:${MINIO_PORT}
SELF_HOSTED: 1
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
LOGO_URL: ${LOGO_URL}
PORT: ${APP_PORT}
HOSTING_URL: ${HOSTING_URL}
MINIO_PORT: ${MINIO_PORT}
JWT_SECRET: ${JWT_SECRET}
depends_on:
- worker-service
@ -29,7 +29,7 @@ services:
PORT: ${WORKER_PORT}
MINIO_ACCESS_KEY: ${MINIO_ACCESS_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_PASSWORD: ${COUCH_DB_PASSWORD}
RAW_COUCH_DB_URL: http://couchdb-service:5984

View File

@ -4,9 +4,10 @@ COUCH_DB_PASSWORD=budibase
COUCH_DB_USER=budibase
WORKER_API_KEY=budibase
BUDIBASE_ENVIRONMENT=PRODUCTION
HOSTING_URL="http://localhost:4001"
HOSTING_URL="http://localhost"
LOGO_URL=https://logoipsum.com/logo/logo-15.svg
APP_PORT=4002
MINIO_PORT=4003
COUCH_DB_PORT=4004
WORKER_PORT=4006
WORKER_PORT=4003
MINIO_PORT=4004
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()))
store.update(state => {
state.hostingInfo = info
state.appUrl = urls.appServer
state.appUrl = urls.app
return state
})
return info

View File

@ -39,8 +39,10 @@
</p>
<Toggle thin text="Self hosted" bind:checked={selfhosted} />
{#if selfhosted}
<Input bind:value={hostingInfo.appServerUrl} label="Apps URL" />
<Input bind:value={hostingInfo.deploymentServerUrl} label="Deployments URL" />
<Input bind:value={hostingInfo.appUrl} label="Apps 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} />
{/if}
</ModalContent>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants")
const {
getHostingInfo,
HostingTypes,
getAppServerUrl,
getAppUrl,
} = require("../../utilities/builder/hosting")
exports.fetchInfo = async ctx => {
@ -30,6 +30,6 @@ exports.fetch = async ctx => {
exports.fetchUrls = async ctx => {
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 env = require("../../../environment")
function appServerUrl(appId) {
if (env.SELF_HOSTED) {
return env.HOSTING_URL
} else {
return `https://${appId}.app.budi.live`
}
}
function objectStoreUrl() {
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 {
return "https://cdn.app.budi.live/assets"
}
@ -164,7 +157,7 @@ exports.serveApp = async function(ctx) {
title: appInfo.name,
production: env.CLOUD,
appId: ctx.params.appId,
appServerUrl: appServerUrl(ctx.params.appId),
objectStoreUrl: objectStoreUrl(ctx.params.appId),
})
const template = handlebars.compile(
@ -232,8 +225,7 @@ exports.serveComponentLibrary = async function(ctx) {
}
const S3_URL = encodeURI(
join(
appServerUrl(appId),
"assets",
objectStoreUrl(appId),
componentLib,
ctx.query.library,
"dist",

View File

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

View File

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

View File

@ -1,6 +1,5 @@
const CouchDB = require("../../db")
const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants")
const { join } = require("path")
function getProtocol(hostingInfo) {
return hostingInfo.useHttps ? "https://" : "http://"
@ -23,8 +22,10 @@ exports.getHostingInfo = async () => {
doc = {
_id: HOSTING_DOC,
type: exports.HostingTypes.CLOUD,
appServerUrl: "app.budi.live",
deploymentServerUrl: "",
appUrl: "app.budi.live",
workerUrl: "",
minioUrl: "",
couchUrl: "",
templatesUrl: "prod-budi-templates.s3-eu-west-1.amazonaws.com",
useHttps: true,
}
@ -32,22 +33,34 @@ exports.getHostingInfo = async () => {
return doc
}
exports.getAppServerUrl = async appId => {
exports.getAppUrl = async appId => {
const hostingInfo = await exports.getHostingInfo()
const protocol = getProtocol(hostingInfo)
let url
if (hostingInfo.type === "cloud") {
url = `${protocol}${appId}.${hostingInfo.appServerUrl}`
url = `${protocol}${appId}.${hostingInfo.appUrl}`
} else {
url = `${protocol}${hostingInfo.appServerUrl}`
url = `${protocol}${hostingInfo.appUrl}`
}
return url
}
exports.getDeploymentUrl = async () => {
exports.getWorkerUrl = async () => {
const hostingInfo = await exports.getHostingInfo()
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) => {
@ -55,9 +68,9 @@ exports.getTemplatesUrl = async (appId, type, name) => {
const protocol = getProtocol(hostingInfo)
let path
if (type && name) {
path = join("templates", type, `${name}.tar.gz`)
path = `templates/type/${name}.tar.gz`
} else {
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"
// this doesn't matter in self host
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() {
// fetch session token for the api user
const session = await got.post(`${env.RAW_COUCH_DB_URL}/_session`, {
responseType: "json",
credentials: "include",
json: {
username: env.COUCH_DB_USERNAME,
password: env.COUCH_DB_PASSWORD,
@ -38,37 +50,25 @@ async function getMinioSession() {
})
// make sure the bucket exists
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) {
// bucket doesn't exist create it
if (err.statusCode === 404) {
await objClient.createBucket({ Bucket: APP_BUCKET }).promise()
await objClient.createBucket({
Bucket: APP_BUCKET,
}).promise()
} else {
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
// Ideally want to send back some pre-signed URLs for files that are to be uploaded
return {
accessKeyId: env.MINIO_ACCESS_KEY,
secretAccessKey: env.MINIO_SECRET_KEY,
@ -80,7 +80,5 @@ exports.deploy = async ctx => {
couchDbSession: await getCouchSession(),
bucket: APP_BUCKET,
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,
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_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_PASSWORD: process.env.COUCH_DB_PASSWORD,
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) {
process.env[key] = value
module.exports[key] = value