budibase complete deployment
This commit is contained in:
parent
f7d65deb5e
commit
27975057c7
|
@ -32,36 +32,4 @@ jobs:
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
name: Budibase CI
|
name: Budibase CI
|
||||||
|
|
||||||
- name: Login to Amazon ECR
|
|
||||||
id: login-ecr
|
|
||||||
uses: aws-actions/amazon-ecr-login@v1
|
|
||||||
|
|
||||||
- name: Build, tag, and push image to Amazon ECR
|
|
||||||
id: build-image
|
|
||||||
env:
|
|
||||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
||||||
ECR_REPOSITORY: my-ecr-repo
|
|
||||||
IMAGE_TAG: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
|
|
||||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
|
||||||
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
|
|
||||||
|
|
||||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
|
||||||
id: task-def
|
|
||||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
|
||||||
with:
|
|
||||||
task-definition: task-definition.json
|
|
||||||
container-name: my-container
|
|
||||||
image: ${{ steps.build-image.outputs.image }}
|
|
||||||
|
|
||||||
- name: Deploy Amazon ECS task definition
|
|
||||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
|
||||||
with:
|
|
||||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
|
||||||
service: my-service
|
|
||||||
cluster: my-cluster
|
|
||||||
wait-for-service-stability: true
|
|
||||||
|
|
|
@ -27,8 +27,7 @@
|
||||||
animation: false,
|
animation: false,
|
||||||
})
|
})
|
||||||
$: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false))
|
$: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false))
|
||||||
$: noChildrenAllowed = false
|
$: noChildrenAllowed = !component ||
|
||||||
!component ||
|
|
||||||
getComponentDefinition($store, component._component).children === false
|
getComponentDefinition($store, component._component).children === false
|
||||||
$: noPaste = !$store.componentToPaste
|
$: noPaste = !$store.componentToPaste
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@ export const loadBudibase = async opts => {
|
||||||
componentLibraryModules[library] = await import(
|
componentLibraryModules[library] = await import(
|
||||||
`/componentlibrary?library=${encodeURI(library)}`
|
`/componentlibrary?library=${encodeURI(library)}`
|
||||||
)
|
)
|
||||||
// componentLibraryModules[library] = await import(`/assets/componentlibrary/${library}/dist/index.js`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentLibraryModules[builtinLibName] = builtins(_window)
|
componentLibraryModules[builtinLibName] = builtins(_window)
|
||||||
|
|
|
@ -14,6 +14,4 @@ PORT=4001
|
||||||
# error level for koa-pino
|
# error level for koa-pino
|
||||||
LOG_LEVEL=error
|
LOG_LEVEL=error
|
||||||
|
|
||||||
DEPLOYMENT_CF_DISTRIBUTION_ID=
|
|
||||||
DEPLOYMENT_APP_ASSETS_BUCKET=g
|
|
||||||
DEPLOYMENT_CREDENTIALS_URL="https://dt4mpwwap8.execute-api.eu-west-1.amazonaws.com/prod/"
|
DEPLOYMENT_CREDENTIALS_URL="https://dt4mpwwap8.execute-api.eu-west-1.amazonaws.com/prod/"
|
|
@ -1,17 +0,0 @@
|
||||||
# url of couch db, including username and password
|
|
||||||
# http://admin:password@localhost:5984
|
|
||||||
COUCH_DB_URL=
|
|
||||||
# identifies a client database - i.e. group of apps
|
|
||||||
CLIENT_ID=1
|
|
||||||
# used to create cookie hashes
|
|
||||||
JWT_SECRET=4715888e-144f-4802-b8d8-862a1b8365dd
|
|
||||||
# port to run http server on
|
|
||||||
PORT=4001
|
|
||||||
|
|
||||||
# error level for koa-pino
|
|
||||||
LOG_LEVEL=error
|
|
||||||
|
|
||||||
COUCH_DB_REMOTE=https://admin:afasdgafgF342G@couchdb.budi.live:5984
|
|
||||||
BUDIBASE_APP_ASSETS_BUCKET=prod-budi-app-assets
|
|
||||||
BUDIBASE_API_KEY=d498278c-4ab4-144b-c212-b8f9e6da5c2b
|
|
||||||
|
|
|
@ -5,11 +5,11 @@ const {
|
||||||
budibaseAppsDir,
|
budibaseAppsDir,
|
||||||
} = require("../../../utilities/budibaseDir")
|
} = require("../../../utilities/budibaseDir")
|
||||||
|
|
||||||
async function invalidateCDN(appId) {
|
async function invalidateCDN(cfDistribution, appId) {
|
||||||
const cf = new AWS.CloudFront({})
|
const cf = new AWS.CloudFront({})
|
||||||
|
|
||||||
return cf.createInvalidation({
|
return cf.createInvalidation({
|
||||||
DistributionId: process.env.DEPLOYMENT_CF_DISTRIBUTION_ID,
|
DistributionId: cfDistribution,
|
||||||
InvalidationBatch: {
|
InvalidationBatch: {
|
||||||
CallerReference: appId,
|
CallerReference: appId,
|
||||||
Paths: {
|
Paths: {
|
||||||
|
@ -47,8 +47,8 @@ const CONTENT_TYPE_MAP = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively walk a directory tree and execute a callback on all files.
|
* Recursively walk a directory tree and execute a callback on all files.
|
||||||
* @param {Re} dirPath - Directory to traverse
|
* @param {String} dirPath - Directory to traverse
|
||||||
* @param {*} callback - callback to execute on files
|
* @param {Function} callback - callback to execute on files
|
||||||
*/
|
*/
|
||||||
function walkDir(dirPath, callback) {
|
function walkDir(dirPath, callback) {
|
||||||
for (let filename of fs.readdirSync(dirPath)) {
|
for (let filename of fs.readdirSync(dirPath)) {
|
||||||
|
@ -56,10 +56,7 @@ function walkDir(dirPath, callback) {
|
||||||
const stat = fs.lstatSync(filePath)
|
const stat = fs.lstatSync(filePath)
|
||||||
|
|
||||||
if (stat.isFile()) {
|
if (stat.isFile()) {
|
||||||
callback({
|
callback(filePath)
|
||||||
bytes: fs.readFileSync(filePath),
|
|
||||||
filename
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
walkDir(filePath, callback)
|
walkDir(filePath, callback)
|
||||||
}
|
}
|
||||||
|
@ -67,7 +64,12 @@ function walkDir(dirPath, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.uploadAppAssets = async function ({ appId }) {
|
exports.uploadAppAssets = async function ({ appId }) {
|
||||||
const { credentials, accountId } = await fetchTemporaryCredentials()
|
const {
|
||||||
|
credentials,
|
||||||
|
accountId,
|
||||||
|
bucket,
|
||||||
|
cfDistribution,
|
||||||
|
} = await fetchTemporaryCredentials()
|
||||||
|
|
||||||
AWS.config.update({
|
AWS.config.update({
|
||||||
accessKeyId: credentials.AccessKeyId,
|
accessKeyId: credentials.AccessKeyId,
|
||||||
|
@ -77,7 +79,7 @@ exports.uploadAppAssets = async function ({ appId }) {
|
||||||
|
|
||||||
const s3 = new AWS.S3({
|
const s3 = new AWS.S3({
|
||||||
params: {
|
params: {
|
||||||
Bucket: process.env.DEPLOYMENT_APP_ASSETS_BUCKET
|
Bucket: bucket
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -88,12 +90,13 @@ exports.uploadAppAssets = async function ({ appId }) {
|
||||||
const uploads = []
|
const uploads = []
|
||||||
|
|
||||||
for (let page of appPages) {
|
for (let page of appPages) {
|
||||||
walkDir(`${appAssetsPath}/${page}`, function prepareUploadsForS3({ bytes, filename }) {
|
walkDir(`${appAssetsPath}/${page}`, function prepareUploadsForS3(filePath) {
|
||||||
const fileExtension = [...filename.split(".")].pop()
|
const fileExtension = [...filePath.split(".")].pop()
|
||||||
|
const fileBytes = fs.readFileSync(filePath)
|
||||||
|
|
||||||
const upload = s3.upload({
|
const upload = s3.upload({
|
||||||
Key: `assets/${appId}/${page}/${filename}`,
|
Key: filePath.replace(appAssetsPath, `assets/${appId}`),
|
||||||
Body: bytes,
|
Body: fileBytes,
|
||||||
ContentType: CONTENT_TYPE_MAP[fileExtension],
|
ContentType: CONTENT_TYPE_MAP[fileExtension],
|
||||||
Metadata: {
|
Metadata: {
|
||||||
accountId
|
accountId
|
||||||
|
@ -105,10 +108,8 @@ exports.uploadAppAssets = async function ({ appId }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const uploadAllFiles = Promise.all(uploads)
|
await Promise.all(uploads)
|
||||||
const invalidateCloudfront = invalidateCDN(appId)
|
await invalidateCDN(cfDistribution, appId)
|
||||||
await uploadAllFiles
|
|
||||||
await invalidateCloudfront
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error uploading budibase app assets to s3", err)
|
console.error("Error uploading budibase app assets to s3", err)
|
||||||
throw err
|
throw err
|
||||||
|
|
|
@ -7,7 +7,7 @@ const {
|
||||||
function replicate(local, remote) {
|
function replicate(local, remote) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
const replication = local.replicate.to(remote);
|
const replication = local.sync(remote);
|
||||||
|
|
||||||
replication.on("complete", () => resolve())
|
replication.on("complete", () => resolve())
|
||||||
replication.on("error", err => reject(err))
|
replication.on("error", err => reject(err))
|
||||||
|
@ -15,24 +15,24 @@ function replicate(local, remote) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replicateCouch(instanceId, clientId) {
|
async function replicateCouch(instanceId, clientId) {
|
||||||
const clientDb = new PouchDB(`client_${clientId}`)
|
|
||||||
const remoteClientDb = new CouchDB(`${process.env.COUCH_DB_REMOTE}/client_${clientId}`)
|
|
||||||
|
|
||||||
const clientAppLookupDb = new PouchDB("client_app_lookup")
|
const databases = [
|
||||||
const remoteClientAppLookupDb = new CouchDB(`${process.env.COUCH_DB_REMOTE}/client_app_lookup`)
|
`client_${clientId}`,
|
||||||
|
"client_app_lookup",
|
||||||
|
instanceId
|
||||||
|
];
|
||||||
|
|
||||||
const localDb = new PouchDB(instanceId)
|
const replications = databases.map(local => {
|
||||||
const remoteDb = new CouchDB(`${process.env.COUCH_DB_REMOTE}/${instanceId}`)
|
const localDb = new PouchDB(local);
|
||||||
|
const remoteDb = new CouchDB(`${process.env.DEPLOYMENT_COUCH_DB_URL}/${local}`)
|
||||||
|
|
||||||
await Promise.all([
|
return replicate(localDb, remoteDb);
|
||||||
replicate(clientDb, remoteClientDb),
|
});
|
||||||
replicate(clientAppLookupDb, remoteClientAppLookupDb),
|
|
||||||
replicate(localDb, remoteDb)
|
await Promise.all(replications)
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.deployApp = async function(ctx) {
|
exports.deployApp = async function(ctx) {
|
||||||
// TODO: This should probably be async - it could take a while
|
|
||||||
try {
|
try {
|
||||||
const clientAppLookupDB = new PouchDB("client_app_lookup")
|
const clientAppLookupDB = new PouchDB("client_app_lookup")
|
||||||
const { clientId } = await clientAppLookupDB.get(ctx.user.appId)
|
const { clientId } = await clientAppLookupDB.get(ctx.user.appId)
|
||||||
|
@ -51,8 +51,7 @@ exports.deployApp = async function(ctx) {
|
||||||
status: "SUCCESS",
|
status: "SUCCESS",
|
||||||
completed: Date.now()
|
completed: Date.now()
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
} catch (err) {
|
ctx.throw(err.status || 500, `Deployment Failed: ${err.message}`);
|
||||||
ctx.throw(err.status || 500, `Deployment Failed: ${err.message}`);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -8,7 +8,6 @@ const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||||
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
|
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { S3 } = require("aws-sdk")
|
|
||||||
|
|
||||||
exports.serveBuilder = async function(ctx) {
|
exports.serveBuilder = async function(ctx) {
|
||||||
let builderPath = resolve(__dirname, "../../../builder")
|
let builderPath = resolve(__dirname, "../../../builder")
|
||||||
|
@ -28,14 +27,20 @@ exports.serveApp = async function(ctx) {
|
||||||
"public",
|
"public",
|
||||||
mainOrAuth
|
mainOrAuth
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let appId = ctx.params.appId
|
||||||
|
if (process.env.CLOUD) {
|
||||||
|
appId = ctx.subdomains[1]
|
||||||
|
}
|
||||||
|
|
||||||
// only set the appId cookie for /appId .. we COULD check for valid appIds
|
// only set the appId cookie for /appId .. we COULD check for valid appIds
|
||||||
// but would like to avoid that DB hit
|
// but would like to avoid that DB hit
|
||||||
const looksLikeAppId = /^[0-9a-f]{32}$/.test(ctx.params.appId)
|
const looksLikeAppId = /^[0-9a-f]{32}$/.test(appId)
|
||||||
if (looksLikeAppId && !ctx.isAuthenticated) {
|
if (looksLikeAppId && !ctx.isAuthenticated) {
|
||||||
const anonUser = {
|
const anonUser = {
|
||||||
userId: "ANON",
|
userId: "ANON",
|
||||||
accessLevelId: ANON_LEVEL_ID,
|
accessLevelId: ANON_LEVEL_ID,
|
||||||
appId: ctx.params.appId,
|
appId,
|
||||||
}
|
}
|
||||||
const anonToken = jwt.sign(anonUser, ctx.config.jwtSecret)
|
const anonToken = jwt.sign(anonUser, ctx.config.jwtSecret)
|
||||||
ctx.cookies.set("budibase:token", anonToken, {
|
ctx.cookies.set("budibase:token", anonToken, {
|
||||||
|
@ -45,7 +50,7 @@ exports.serveApp = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.CLOUD) {
|
if (process.env.CLOUD) {
|
||||||
const S3_URL = `https://${ctx.params.appId}.app.budi.live/assets/${ctx.params.appId}/${mainOrAuth}/${ctx.file || "index.production.html"}`
|
const S3_URL = `https://${appId}.app.budi.live/assets/${appId}/${mainOrAuth}/${ctx.file || "index.production.html"}`
|
||||||
const response = await fetch(S3_URL)
|
const response = await fetch(S3_URL)
|
||||||
const body = await response.text()
|
const body = await response.text()
|
||||||
ctx.body = body
|
ctx.body = body
|
||||||
|
|
|
@ -16,7 +16,7 @@ app.use(
|
||||||
prettyPrint: {
|
prettyPrint: {
|
||||||
levelFirst: true,
|
levelFirst: true,
|
||||||
},
|
},
|
||||||
level: "info" || "info",
|
level: env.LOG_LEVEL || "error",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,4 +29,4 @@ module.exports = async port => {
|
||||||
const serverPort = port || env.PORT
|
const serverPort = port || env.PORT
|
||||||
const server = http.createServer(app.callback())
|
const server = http.createServer(app.callback())
|
||||||
return server.listen(serverPort || 4001)
|
return server.listen(serverPort || 4001)
|
||||||
}
|
}
|
Loading…
Reference in New Issue