253 lines
6.6 KiB
JavaScript
253 lines
6.6 KiB
JavaScript
const CouchDB = require("pouchdb")
|
|
const PouchDB = require("../../../db")
|
|
const {
|
|
uploadAppAssets,
|
|
verifyDeployment,
|
|
updateDeploymentQuota,
|
|
isInvalidationComplete,
|
|
} = require("./aws")
|
|
const { DocumentTypes, SEPARATOR, UNICODE_MAX } = require("../../../db/utils")
|
|
const newid = require("../../../db/newid")
|
|
|
|
// the max time we can wait for an invalidation to complete before considering it failed
|
|
const MAX_PENDING_TIME_MS = 30 * 60000
|
|
|
|
const DeploymentStatus = {
|
|
SUCCESS: "SUCCESS",
|
|
PENDING: "PENDING",
|
|
FAILURE: "FAILURE",
|
|
}
|
|
|
|
// checks that deployments are in a good state, any pending will be updated
|
|
async function checkAllDeployments(deployments, user) {
|
|
let updated = false
|
|
function update(deployment, status) {
|
|
deployment.status = status
|
|
delete deployment.invalidationId
|
|
delete deployment.cfDistribution
|
|
updated = true
|
|
}
|
|
|
|
for (let deployment of Object.values(deployments.history)) {
|
|
// check that no deployments have crashed etc and are now stuck
|
|
if (
|
|
deployment.status === DeploymentStatus.PENDING &&
|
|
Date.now() - deployment.updatedAt > MAX_PENDING_TIME_MS
|
|
) {
|
|
update(deployment, DeploymentStatus.FAILURE)
|
|
}
|
|
// if pending but not past failure point need to update them
|
|
else if (deployment.status === DeploymentStatus.PENDING) {
|
|
let complete = false
|
|
try {
|
|
complete = await isInvalidationComplete(
|
|
deployment.cfDistribution,
|
|
deployment.invalidationId
|
|
)
|
|
} catch (err) {
|
|
// system may have restarted, need to re-verify
|
|
if (
|
|
err !== undefined &&
|
|
err.code === "InvalidClientTokenId" &&
|
|
deployment.quota
|
|
) {
|
|
await verifyDeployment({
|
|
...user,
|
|
quota: deployment.quota,
|
|
})
|
|
complete = await isInvalidationComplete(
|
|
deployment.cfDistribution,
|
|
deployment.invalidationId
|
|
)
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
if (complete) {
|
|
update(deployment, DeploymentStatus.SUCCESS)
|
|
}
|
|
}
|
|
}
|
|
return { updated, deployments }
|
|
}
|
|
|
|
function replicate(local, remote) {
|
|
return new Promise((resolve, reject) => {
|
|
const replication = local.sync(remote)
|
|
|
|
replication.on("complete", () => resolve())
|
|
replication.on("error", err => reject(err))
|
|
})
|
|
}
|
|
|
|
async function replicateCouch({ instanceId, clientId, session }) {
|
|
const databases = [`client_${clientId}`, "client_app_lookup", instanceId]
|
|
|
|
const replications = databases.map(localDbName => {
|
|
const localDb = new PouchDB(localDbName)
|
|
const remoteDb = new CouchDB(
|
|
`${process.env.DEPLOYMENT_DB_URL}/${localDbName}`,
|
|
{
|
|
fetch: function(url, opts) {
|
|
opts.headers.set("Cookie", `${session};`)
|
|
return CouchDB.fetch(url, opts)
|
|
},
|
|
}
|
|
)
|
|
|
|
return replicate(localDb, remoteDb)
|
|
})
|
|
|
|
await Promise.all(replications)
|
|
}
|
|
|
|
async function getCurrentInstanceQuota(instanceId) {
|
|
const db = new PouchDB(instanceId)
|
|
|
|
const rows = await db.allDocs({
|
|
startkey: DocumentTypes.ROW + SEPARATOR,
|
|
endkey: DocumentTypes.ROW + SEPARATOR + UNICODE_MAX,
|
|
})
|
|
|
|
const users = await db.allDocs({
|
|
startkey: DocumentTypes.USER + SEPARATOR,
|
|
endkey: DocumentTypes.USER + SEPARATOR + UNICODE_MAX,
|
|
})
|
|
|
|
const existingRows = rows.rows.length
|
|
const existingUsers = users.rows.length
|
|
|
|
const designDoc = await db.get("_design/database")
|
|
|
|
return {
|
|
rows: existingRows,
|
|
users: existingUsers,
|
|
views: Object.keys(designDoc.views).length,
|
|
}
|
|
}
|
|
|
|
async function storeLocalDeploymentHistory(deployment) {
|
|
const db = new PouchDB(deployment.instanceId)
|
|
|
|
let deploymentDoc
|
|
try {
|
|
deploymentDoc = await db.get("_local/deployments")
|
|
} catch (err) {
|
|
deploymentDoc = { _id: "_local/deployments", history: {} }
|
|
}
|
|
|
|
const deploymentId = deployment._id || newid()
|
|
|
|
// first time deployment
|
|
if (!deploymentDoc.history[deploymentId])
|
|
deploymentDoc.history[deploymentId] = {}
|
|
|
|
deploymentDoc.history[deploymentId] = {
|
|
...deploymentDoc.history[deploymentId],
|
|
...deployment,
|
|
updatedAt: Date.now(),
|
|
}
|
|
|
|
await db.put(deploymentDoc)
|
|
return {
|
|
_id: deploymentId,
|
|
...deploymentDoc.history[deploymentId],
|
|
}
|
|
}
|
|
|
|
async function deployApp({ instanceId, appId, clientId, deploymentId }) {
|
|
try {
|
|
const instanceQuota = await getCurrentInstanceQuota(instanceId)
|
|
const verification = await verifyDeployment({
|
|
instanceId,
|
|
appId,
|
|
quota: instanceQuota,
|
|
})
|
|
|
|
console.log(`Uploading assets for appID ${appId} assets to s3..`)
|
|
|
|
const invalidationId = await uploadAppAssets({
|
|
clientId,
|
|
appId,
|
|
instanceId,
|
|
...verification,
|
|
})
|
|
|
|
// replicate the DB to the couchDB cluster in prod
|
|
console.log("Replicating local PouchDB to remote..")
|
|
await replicateCouch({
|
|
instanceId,
|
|
clientId,
|
|
session: verification.couchDbSession,
|
|
})
|
|
|
|
await updateDeploymentQuota(verification.quota)
|
|
|
|
await storeLocalDeploymentHistory({
|
|
_id: deploymentId,
|
|
instanceId,
|
|
invalidationId,
|
|
cfDistribution: verification.cfDistribution,
|
|
quota: verification.quota,
|
|
status: DeploymentStatus.PENDING,
|
|
})
|
|
} catch (err) {
|
|
await storeLocalDeploymentHistory({
|
|
_id: deploymentId,
|
|
instanceId,
|
|
status: DeploymentStatus.FAILURE,
|
|
err: err.message,
|
|
})
|
|
throw new Error(`Deployment Failed: ${err.message}`)
|
|
}
|
|
}
|
|
|
|
exports.fetchDeployments = async function(ctx) {
|
|
try {
|
|
const db = new PouchDB(ctx.user.instanceId)
|
|
const deploymentDoc = await db.get("_local/deployments")
|
|
const { updated, deployments } = await checkAllDeployments(
|
|
deploymentDoc,
|
|
ctx.user
|
|
)
|
|
if (updated) {
|
|
await db.put(deployments)
|
|
}
|
|
ctx.body = Object.values(deployments.history).reverse()
|
|
} catch (err) {
|
|
ctx.body = []
|
|
}
|
|
}
|
|
|
|
exports.deploymentProgress = async function(ctx) {
|
|
try {
|
|
const db = new PouchDB(ctx.user.instanceId)
|
|
const deploymentDoc = await db.get("_local/deployments")
|
|
ctx.body = deploymentDoc[ctx.params.deploymentId]
|
|
} catch (err) {
|
|
ctx.throw(
|
|
500,
|
|
`Error fetching data for deployment ${ctx.params.deploymentId}`
|
|
)
|
|
}
|
|
}
|
|
|
|
exports.deployApp = async function(ctx) {
|
|
const clientAppLookupDB = new PouchDB("client_app_lookup")
|
|
const { clientId } = await clientAppLookupDB.get(ctx.user.appId)
|
|
|
|
const deployment = await storeLocalDeploymentHistory({
|
|
instanceId: ctx.user.instanceId,
|
|
appId: ctx.user.appId,
|
|
status: DeploymentStatus.PENDING,
|
|
})
|
|
|
|
await deployApp({
|
|
...ctx.user,
|
|
clientId,
|
|
deploymentId: deployment._id,
|
|
})
|
|
|
|
ctx.body = deployment
|
|
}
|