budibase/packages/server/src/api/controllers/deploy/aws.js

230 lines
5.6 KiB
JavaScript
Raw Normal View History

const fs = require("fs")
const { join } = require("../../../utilities/centralPath")
const AWS = require("aws-sdk")
const fetch = require("node-fetch")
2020-10-15 17:59:57 +02:00
const uuid = require("uuid")
const sanitize = require("sanitize-s3-objectkey")
2020-07-07 22:29:20 +02:00
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
2020-09-16 13:18:47 +02:00
const PouchDB = require("../../../db")
const env = require("../../../environment")
2020-07-07 18:47:18 +02:00
async function invalidateCDN(cfDistribution, appId) {
2020-07-06 20:43:40 +02:00
const cf = new AWS.CloudFront({})
const resp = await cf
2020-07-07 22:29:20 +02:00
.createInvalidation({
DistributionId: cfDistribution,
InvalidationBatch: {
2020-10-15 17:59:57 +02:00
CallerReference: `${appId}-${uuid.v4()}`,
2020-07-07 22:29:20 +02:00
Paths: {
Quantity: 1,
Items: [`/assets/${appId}/*`],
},
},
})
.promise()
return resp.Invalidation.Id
}
exports.isInvalidationComplete = async function(
distributionId,
invalidationId
) {
if (distributionId == null || invalidationId == null) {
return false
}
const cf = new AWS.CloudFront({})
const resp = await cf
.getInvalidation({
DistributionId: distributionId,
Id: invalidationId,
})
.promise()
return resp.Invalidation.Status === "Completed"
2020-07-06 20:43:40 +02:00
}
2020-07-03 00:22:20 +02:00
2020-10-08 11:56:32 +02:00
exports.updateDeploymentQuota = async function(quota) {
2020-10-08 21:23:58 +02:00
const DEPLOYMENT_SUCCESS_URL =
env.DEPLOYMENT_CREDENTIALS_URL + "deploy/success"
2020-10-08 21:23:58 +02:00
const response = await fetch(DEPLOYMENT_SUCCESS_URL, {
method: "POST",
body: JSON.stringify({
apiKey: env.BUDIBASE_API_KEY,
2020-10-08 21:23:58 +02:00
quota,
}),
2020-10-09 18:07:46 +02:00
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
2020-10-08 21:23:58 +02:00
})
2020-10-08 11:56:32 +02:00
if (response.status !== 200) {
2020-10-08 21:23:58 +02:00
throw new Error(`Error updating deployment quota for API Key`)
2020-10-08 11:56:32 +02:00
}
const json = await response.json()
return json
}
/**
* Verifies the users API key and
* Verifies that the deployment fits within the quota of the user,
* @param {String} instanceId - instanceId being deployed
* @param {String} appId - appId being deployed
* @param {quota} quota - current quota being changed with this application
*/
exports.verifyDeployment = async function({ instanceId, appId, quota }) {
const response = await fetch(env.DEPLOYMENT_CREDENTIALS_URL, {
method: "POST",
body: JSON.stringify({
apiKey: env.BUDIBASE_API_KEY,
instanceId,
appId,
quota,
2020-07-07 22:29:20 +02:00
}),
})
if (response.status !== 200) {
2020-07-07 22:29:20 +02:00
throw new Error(
`Error fetching temporary credentials for api key: ${env.BUDIBASE_API_KEY}`
2020-07-07 22:29:20 +02:00
)
}
const json = await response.json()
if (json.errors) {
throw new Error(json.errors)
}
// set credentials here, means any time we're verified we're ready to go
if (json.credentials) {
AWS.config.update({
accessKeyId: json.credentials.AccessKeyId,
secretAccessKey: json.credentials.SecretAccessKey,
sessionToken: json.credentials.SessionToken,
})
}
return json
2020-07-07 22:29:20 +02:00
}
2020-07-03 00:22:20 +02:00
const CONTENT_TYPE_MAP = {
html: "text/html",
css: "text/css",
2020-07-07 22:29:20 +02:00
js: "application/javascript",
}
2020-07-03 00:22:20 +02:00
2020-07-06 20:43:40 +02:00
/**
* Recursively walk a directory tree and execute a callback on all files.
2020-07-07 18:47:18 +02:00
* @param {String} dirPath - Directory to traverse
* @param {Function} callback - callback to execute on files
2020-07-06 20:43:40 +02:00
*/
function walkDir(dirPath, callback) {
for (let filename of fs.readdirSync(dirPath)) {
2020-07-07 22:29:20 +02:00
const filePath = `${dirPath}/${filename}`
2020-07-06 20:43:40 +02:00
const stat = fs.lstatSync(filePath)
2020-07-07 22:29:20 +02:00
2020-07-06 20:43:40 +02:00
if (stat.isFile()) {
2020-07-07 18:47:18 +02:00
callback(filePath)
2020-07-06 20:43:40 +02:00
} else {
walkDir(filePath, callback)
}
}
}
2020-09-23 21:23:40 +02:00
async function prepareUploadForS3({ s3Key, metadata, s3, file }) {
const extension = [...file.name.split(".")].pop()
const fileBytes = fs.readFileSync(file.path)
2020-09-23 17:15:09 +02:00
const upload = await s3
2020-09-16 13:18:47 +02:00
.upload({
// windows filepaths need to be converted to forward slashes for s3
Key: sanitize(s3Key).replace(/\\/g, "/"),
2020-09-16 13:18:47 +02:00
Body: fileBytes,
2020-09-23 21:23:40 +02:00
ContentType: file.type || CONTENT_TYPE_MAP[extension.toLowerCase()],
2020-09-16 13:18:47 +02:00
Metadata: metadata,
})
.promise()
2020-09-23 17:15:09 +02:00
return {
2020-09-23 21:23:40 +02:00
size: file.size,
name: file.name,
extension,
2020-09-23 17:15:09 +02:00
url: upload.Location,
key: upload.Key,
}
2020-09-16 13:18:47 +02:00
}
2020-09-23 17:15:09 +02:00
exports.prepareUploadForS3 = prepareUploadForS3
2020-07-08 17:31:26 +02:00
exports.uploadAppAssets = async function({
appId,
2020-09-16 13:18:47 +02:00
instanceId,
2020-07-08 17:31:26 +02:00
bucket,
cfDistribution,
accountId,
}) {
const s3 = new AWS.S3({
params: {
2020-07-07 22:29:20 +02:00
Bucket: bucket,
},
})
const appAssetsPath = join(budibaseAppsDir(), appId, "public")
const appPages = fs.readdirSync(appAssetsPath)
let uploads = []
for (let page of appPages) {
// Upload HTML, CSS and JS for each page of the web app
walkDir(join(appAssetsPath, page), function(filePath) {
2020-09-16 13:18:47 +02:00
const appAssetUpload = prepareUploadForS3({
2020-09-23 21:23:40 +02:00
file: {
path: filePath,
name: [...filePath.split("/")].pop(),
},
2020-09-17 13:45:28 +02:00
s3Key: filePath.replace(appAssetsPath, `assets/${appId}`),
2020-09-16 13:18:47 +02:00
s3,
2020-09-17 17:36:39 +02:00
metadata: { accountId },
2020-09-16 13:18:47 +02:00
})
uploads.push(appAssetUpload)
})
}
// Upload file attachments
2020-09-16 13:18:47 +02:00
const db = new PouchDB(instanceId)
2020-10-08 21:23:58 +02:00
let fileUploads
try {
fileUploads = await db.get("_local/fileuploads")
} catch (err) {
fileUploads = { _id: "_local/fileuploads", uploads: [] }
}
2020-09-16 13:18:47 +02:00
2020-10-08 21:23:58 +02:00
for (let file of fileUploads.uploads) {
if (file.uploaded) continue
2020-10-08 21:23:58 +02:00
const attachmentUpload = prepareUploadForS3({
file,
s3Key: `assets/${appId}/attachments/${file.processedFileName}`,
s3,
metadata: { accountId },
})
2020-10-08 21:23:58 +02:00
uploads.push(attachmentUpload)
// mark file as uploaded
file.uploaded = true
2020-09-16 13:18:47 +02:00
}
2020-10-08 21:23:58 +02:00
db.put(fileUploads)
try {
2020-07-07 18:47:18 +02:00
await Promise.all(uploads)
return await invalidateCDN(cfDistribution, appId)
} catch (err) {
console.error("Error uploading budibase app assets to s3", err)
throw err
}
2020-07-07 22:29:20 +02:00
}