diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 21d735fcbc..fde56b153a 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -32,4 +32,36 @@ jobs: - run: yarn test env: CI: true - name: Budibase CI \ No newline at end of file + 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 + \ No newline at end of file diff --git a/packages/builder/src/builderStore/loadComponentLibraries.js b/packages/builder/src/builderStore/loadComponentLibraries.js index 93fc5b3aac..9d534f86fe 100644 --- a/packages/builder/src/builderStore/loadComponentLibraries.js +++ b/packages/builder/src/builderStore/loadComponentLibraries.js @@ -23,8 +23,7 @@ export const fetchComponentLibModules = async application => { const allLibraries = {} for (let libraryName of application.componentLibraries) { - // const LIBRARY_URL = `/${application._id}/componentlibrary?library=${libraryName}` - const LIBRARY_URL = `/assets/componentlibrary/${libraryName}/dist/index.js` + const LIBRARY_URL = `/${application._id}/componentlibrary?library=${libraryName}` const libraryModule = await import(LIBRARY_URL) allLibraries[libraryName] = libraryModule } diff --git a/packages/builder/src/pages/[application]/deploy/index.svelte b/packages/builder/src/pages/[application]/deploy/index.svelte index 675980e7f2..56e32ac295 100644 --- a/packages/builder/src/pages/[application]/deploy/index.svelte +++ b/packages/builder/src/pages/[application]/deploy/index.svelte @@ -3,10 +3,15 @@ import { store } from "builderStore" import { notifier } from "builderStore/store/notifications" import api from "builderStore/api" + import Spinner from "components/common/Spinner.svelte" + + let deployed = false + let loading = false $: appId = $store.appId async function deployApp() { + loading = true const DEPLOY_URL = `/deploy` try { @@ -14,12 +19,15 @@ const response = await api.post(DEPLOY_URL) const json = await response.json() if (response.status !== 200) { - throw new Error + throw new Error() } - notifier.success(`Deployment Complete. View your app at blah URL https://${appId}.app.budi.live/${appId}`) + notifier.success(`Your Deployment is Complete.`) + deployed = true + loading = false } catch (err) { notifier.danger("Deployment unsuccessful. Please try again later.") + loading = false } } @@ -27,9 +35,18 @@

It's time to shine!

- + {#if deployed} + + View App + + {:else} + + {/if}
@@ -53,11 +70,11 @@ flex-direction: column; align-items: center; justify-content: center; - left: 0; - right: 0; + left: 0; + right: 0; top: 20%; - margin-left: auto; - margin-right: auto; + margin-left: auto; + margin-right: auto; width: 50%; } diff --git a/packages/cli/src/commands/run/runHandler.js b/packages/cli/src/commands/run/runHandler.js index e3f669cf53..b7c58e6e03 100644 --- a/packages/cli/src/commands/run/runHandler.js +++ b/packages/cli/src/commands/run/runHandler.js @@ -8,7 +8,7 @@ module.exports = ({ dir }) => { // dont make this a variable or top level require // ti will cause environment module to be loaded prematurely - require("@budibase/server/src/app")().then(server => { + return require("@budibase/server/src/app")().then(server => { server.on("close", () => console.log("Server Closed")) console.log(`Budibase running on ${JSON.stringify(server.address())}`) }) diff --git a/packages/client/src/index.js b/packages/client/src/index.js index a1f14f667e..f73370ab77 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -20,10 +20,10 @@ export const loadBudibase = async opts => { for (let library of libraries) { // fetch the JavaScript for the component libraries from the server - // componentLibraryModules[library] = await import( - // `/componentlibrary?library=${encodeURI(library)}` - // ) - componentLibraryModules[library] = await import(`/assets/componentlibrary/${library}/dist/index.js`) + componentLibraryModules[library] = await import( + `/componentlibrary?library=${encodeURI(library)}` + ) + // componentLibraryModules[library] = await import(`/assets/componentlibrary/${library}/dist/index.js`) } componentLibraryModules[builtinLibName] = builtins(_window) diff --git a/packages/server/.env.template b/packages/server/.env.template index f282d9c67f..30c28b1b2e 100644 --- a/packages/server/.env.template +++ b/packages/server/.env.template @@ -12,4 +12,8 @@ JWT_SECRET={{cookieKey1}} PORT=4001 # error level for koa-pino -LOG_LEVEL=error \ No newline at end of file +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/" \ No newline at end of file diff --git a/packages/server/Dockerfile b/packages/server/Dockerfile index 7ae24dfd62..bfdc52c6d3 100644 --- a/packages/server/Dockerfile +++ b/packages/server/Dockerfile @@ -2,6 +2,8 @@ FROM node:12-alpine WORKDIR /app +ENV CLOUD=1 + # copy files and install dependencies COPY . ./ RUN yarn diff --git a/packages/server/envfile b/packages/server/envfile new file mode 100644 index 0000000000..d38eaf1d56 --- /dev/null +++ b/packages/server/envfile @@ -0,0 +1,17 @@ +# 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 + diff --git a/packages/server/src/api/controllers/deploy/aws.js b/packages/server/src/api/controllers/deploy/aws.js index d9ea62769a..ae64c835b8 100644 --- a/packages/server/src/api/controllers/deploy/aws.js +++ b/packages/server/src/api/controllers/deploy/aws.js @@ -5,15 +5,28 @@ const { budibaseAppsDir, } = require("../../../utilities/budibaseDir") +async function invalidateCDN(appId) { + const cf = new AWS.CloudFront({}) + + return cf.createInvalidation({ + DistributionId: process.env.DEPLOYMENT_CF_DISTRIBUTION_ID, + InvalidationBatch: { + CallerReference: appId, + Paths: { + Quantity: 1, + Items: [ + `/assets/${appId}/*` + ] + } + } + }).promise() +} + async function fetchTemporaryCredentials() { - const CREDENTIALS_URL = "https://dt4mpwwap8.execute-api.eu-west-1.amazonaws.com/prod/" - - const BUDIBASE_API_KEY = process.env.BUDIBASE_API_KEY - - const response = await fetch(CREDENTIALS_URL, { + const response = await fetch(process.env.DEPLOYMENT_CREDENTIALS_URL, { method: "POST", body: JSON.stringify({ - apiKey: BUDIBASE_API_KEY + apiKey: process.env.BUDIBASE_API_KEY }) }) @@ -32,6 +45,27 @@ const CONTENT_TYPE_MAP = { js: "application/javascript" }; +/** + * Recursively walk a directory tree and execute a callback on all files. + * @param {Re} dirPath - Directory to traverse + * @param {*} callback - callback to execute on files + */ +function walkDir(dirPath, callback) { + for (let filename of fs.readdirSync(dirPath)) { + const filePath = `${dirPath}/${filename}` + const stat = fs.lstatSync(filePath) + + if (stat.isFile()) { + callback({ + bytes: fs.readFileSync(filePath), + filename + }) + } else { + walkDir(filePath, callback) + } + } +} + exports.uploadAppAssets = async function ({ appId }) { const { credentials, accountId } = await fetchTemporaryCredentials() @@ -43,7 +77,7 @@ exports.uploadAppAssets = async function ({ appId }) { const s3 = new AWS.S3({ params: { - Bucket: process.env.BUDIBASE_APP_ASSETS_BUCKET + Bucket: process.env.DEPLOYMENT_APP_ASSETS_BUCKET } }) @@ -54,34 +88,27 @@ exports.uploadAppAssets = async function ({ appId }) { const uploads = [] for (let page of appPages) { - for (let filename of fs.readdirSync(`${appAssetsPath}/${page}`)) { - const filePath = `${appAssetsPath}/${page}/${filename}` - const stat = await fs.lstatSync(filePath) - - // TODO: need to account for recursively traversing dirs - if (stat.isFile()) { - const fileBytes = fs.readFileSync(`${appAssetsPath}/${page}/${filename}`) + walkDir(`${appAssetsPath}/${page}`, function prepareUploadsForS3({ bytes, filename }) { + const fileExtension = [...filename.split(".")].pop() - console.log(`${appId}/${page}/${filename}`) + const upload = s3.upload({ + Key: `assets/${appId}/${page}/${filename}`, + Body: bytes, + ContentType: CONTENT_TYPE_MAP[fileExtension], + Metadata: { + accountId + } + }).promise() - const fileExtension = [...filename.split(".")].pop() - - const upload = s3.upload({ - Key: `assets/${appId}/${page}/${filename}`, - Body: fileBytes, - ContentType: CONTENT_TYPE_MAP[fileExtension], - Metadata: { - accountId - } - }).promise() - - uploads.push(upload) - } - } + uploads.push(upload) + }) } try { - return Promise.all(uploads) + const uploadAllFiles = Promise.all(uploads) + const invalidateCloudfront = invalidateCDN(appId) + await uploadAllFiles + await invalidateCloudfront } catch (err) { console.error("Error uploading budibase app assets to s3", err) throw err diff --git a/packages/server/src/api/controllers/static.js b/packages/server/src/api/controllers/static.js index 31d91672a3..6ce945dde4 100644 --- a/packages/server/src/api/controllers/static.js +++ b/packages/server/src/api/controllers/static.js @@ -44,19 +44,15 @@ exports.serveApp = async function(ctx) { }) } - const { file = "index.html" } = ctx - - - if (ctx.isCloud) { - const S3_URL = `https://${ctx.params.appId}.app.budi.live/assets/${ctx.params.appId}/${mainOrAuth}/${file}` - console.log("Serving: " + S3_URL) + 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 response = await fetch(S3_URL) const body = await response.text() ctx.body = body return } - await send(ctx, file, { root: ctx.devPath || appPath }) + await send(ctx, ctx.file || "index.html", { root: ctx.devPath || appPath }) } exports.serveAppAsset = async function(ctx) { @@ -70,15 +66,7 @@ exports.serveAppAsset = async function(ctx) { mainOrAuth ) - // if (ctx.isCloud) { - // const requestUrl = `${S3_URL_PREFIX}/${appId}/public/${mainOrAuth}/${ctx.file || "index.html"}` - // console.log('request url:' , requestUrl) - // const response = await fetch(requestUrl) - // const body = await response.text() - // ctx.body = body - // } else { - await send(ctx, ctx.file, { root: ctx.devPath || appPath }) - // } + await send(ctx, ctx.file, { root: ctx.devPath || appPath }) } exports.serveComponentLibrary = async function(ctx) { @@ -99,16 +87,15 @@ exports.serveComponentLibrary = async function(ctx) { ) } - // if (ctx.isCloud) { - // const appId = ctx.user.appId - // const requestUrl = encodeURI(`${S3_URL_PREFIX}/${appId}/node_modules/${ctx.query.library}/dist/index.js`) - // console.log('request url components: ', requestUrl) - // const response = await fetch(requestUrl) - // const body = await response.text() - // ctx.type = 'application/javascript' - // ctx.body = body; - // return - // } + if (process.env.CLOUD) { + const appId = ctx.user.appId + const S3_URL = encodeURI(`https://${appId}.app.budi.live/assets/componentlibrary/${ctx.query.library}/dist/index.js`) + const response = await fetch(S3_URL) + const body = await response.text() + ctx.type = 'application/javascript' + ctx.body = body; + return + } await send(ctx, "/index.js", { root: componentLibraryPath }) } diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index a3d7fbffc9..165a479a38 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -43,7 +43,6 @@ router useAppRootPath: true, } ctx.isDev = env.NODE_ENV !== "production" && env.NODE_ENV !== "jest" - // ctx.isCloud = true await next() }) .use(authenticated) diff --git a/packages/server/src/utilities/builder/buildPage.js b/packages/server/src/utilities/builder/buildPage.js index 96db612bf7..2c5ada76b3 100644 --- a/packages/server/src/utilities/builder/buildPage.js +++ b/packages/server/src/utilities/builder/buildPage.js @@ -57,8 +57,6 @@ const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => { pageStyle: pkg.page._css, appId, pageName, - // TODO: don't hardcode - production: true } const indexHtmlTemplate = await readFile( @@ -67,10 +65,16 @@ const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => { ) const indexHtmlPath = join(appPublicPath, "index.html") + const deployableHtmlPath = join(appPublicPath, "index.production.html") const indexHtml = sqrl.Render(indexHtmlTemplate, templateObj) + const deployableHtml = sqrl.Render(indexHtmlTemplate, { + ...templateObj, + production: true + }) await writeFile(indexHtmlPath, indexHtml, { flag: "w+" }) + await writeFile(deployableHtmlPath, deployableHtml, { flag: "w+" }) } const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => { diff --git a/packages/server/src/utilities/builder/index.template.html b/packages/server/src/utilities/builder/index.template.html index 0f90b34002..e7cf633277 100644 --- a/packages/server/src/utilities/builder/index.template.html +++ b/packages/server/src/utilities/builder/index.template.html @@ -27,29 +27,34 @@ {{ /each }} {{ each(options.screenStyles) }} + {{ if(options.production) }} + + {{#else}} + {{ /if }} {{ /each }} {{ if(options.pageStyle) }} + {{ if(options.production) }} + + {{#else}} {{ /if }} + {{ /if }} {{ if(options.production) }} - - - {{ else }} - - - {{ /if }} + {{#else}} + + + {{ /if }} -