mirror of
https://github.com/caprover/caprover
synced 2025-10-30 10:07:01 +00:00
Queued builds
This commit is contained in:
@@ -68,21 +68,9 @@ router.use(function(req, res, next) {
|
||||
}
|
||||
|
||||
if (threadLockNamespace[namespace]) {
|
||||
let response = new BaseApi(
|
||||
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
||||
'Another operation still in progress... please wait...'
|
||||
)
|
||||
res.send(response)
|
||||
return
|
||||
}
|
||||
|
||||
let activeBuildAppName = serviceManager.isAnyBuildRunning()
|
||||
if (activeBuildAppName) {
|
||||
let response = new BaseApi(
|
||||
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
||||
`An active build (${activeBuildAppName}) is in progress... please wait...`
|
||||
)
|
||||
res.send(response)
|
||||
// Changed to HTTP status code so that the webhook and 3rd party services can understand this.
|
||||
res.status(429)
|
||||
res.send('Another operation still in progress... please wait...')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ router.post('/:appName/', upload.single('sourceFile'), function(
|
||||
}
|
||||
|
||||
Promise.resolve().then(function() {
|
||||
const promiseToDeployNewVer = serviceManager.deployNewVersion(appName, {
|
||||
const promiseToDeployNewVer = serviceManager.scheduleDeployNewVersion(appName, {
|
||||
uploadedTarPathSource: !!tarballSourceFilePath
|
||||
? {
|
||||
uploadedTarPath: tarballSourceFilePath,
|
||||
|
||||
@@ -251,7 +251,7 @@ router.post('/register/', function(req, res, next) {
|
||||
appCreated = true
|
||||
})
|
||||
.then(function() {
|
||||
return serviceManager.deployNewVersion(appName, {
|
||||
return serviceManager.scheduleDeployNewVersion(appName, {
|
||||
captainDefinitionContentSource: {
|
||||
captainDefinitionContent: DEFAULT_APP_CAPTAIN_DEFINITION,
|
||||
gitHash: '',
|
||||
|
||||
@@ -88,7 +88,7 @@ router.post('/triggerbuild', urlencodedParser, function(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
return serviceManager.deployNewVersion(appName, {
|
||||
return serviceManager.scheduleDeployNewVersion(appName, {
|
||||
repoInfoSource: repoInfo,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,8 +57,7 @@ export default class ImageMaker {
|
||||
private dockerRegistryHelper: DockerRegistryHelper,
|
||||
private dockerApi: DockerApi,
|
||||
private namespace: string,
|
||||
private buildLogs: IHashMapGeneric<BuildLog>,
|
||||
private activeBuilds: IHashMapGeneric<boolean>
|
||||
private buildLogs: IHashMapGeneric<BuildLog>
|
||||
) {
|
||||
//
|
||||
}
|
||||
@@ -84,7 +83,6 @@ export default class ImageMaker {
|
||||
): Promise<IBuiltImage> {
|
||||
const self = this
|
||||
|
||||
this.activeBuilds[appName] = true
|
||||
this.buildLogs[appName] =
|
||||
this.buildLogs[appName] ||
|
||||
new BuildLog(CaptainConstants.configs.buildLogSize)
|
||||
@@ -207,7 +205,6 @@ export default class ImageMaker {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
.then(function() {
|
||||
self.activeBuilds[appName] = false
|
||||
self.buildLogs[appName].log(`Build has finished successfully!`)
|
||||
return {
|
||||
imageName: fullImageName,
|
||||
@@ -215,7 +212,6 @@ export default class ImageMaker {
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
self.activeBuilds[appName] = false
|
||||
self.buildLogs[appName].log(`Build has failed!`)
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
@@ -14,6 +14,18 @@ import Authenticator = require('./Authenticator')
|
||||
|
||||
const serviceMangerCache = {} as IHashMapGeneric<ServiceManager>
|
||||
|
||||
interface QueuedPromise {
|
||||
resolve: undefined | ((reason?: unknown) => void)
|
||||
reject: undefined | ((reason?: any) => void)
|
||||
promise: undefined | (Promise<unknown>)
|
||||
}
|
||||
|
||||
interface QueuedBuild {
|
||||
appName: string
|
||||
source: IImageSource
|
||||
promiseToSave: QueuedPromise
|
||||
}
|
||||
|
||||
class ServiceManager {
|
||||
static get(
|
||||
namespace: string,
|
||||
@@ -37,6 +49,7 @@ class ServiceManager {
|
||||
|
||||
private activeBuilds: IHashMapGeneric<boolean>
|
||||
private buildLogs: IHashMapGeneric<BuildLog>
|
||||
private queuedBuilds: QueuedBuild[]
|
||||
private isReady: boolean
|
||||
private imageMaker: ImageMaker
|
||||
private dockerRegistryHelper: DockerRegistryHelper
|
||||
@@ -49,6 +62,7 @@ class ServiceManager {
|
||||
private domainResolveChecker: DomainResolveChecker
|
||||
) {
|
||||
this.activeBuilds = {}
|
||||
this.queuedBuilds = []
|
||||
this.buildLogs = {}
|
||||
this.isReady = true
|
||||
this.dockerRegistryHelper = new DockerRegistryHelper(
|
||||
@@ -59,8 +73,7 @@ class ServiceManager {
|
||||
this.dockerRegistryHelper,
|
||||
this.dockerApi,
|
||||
this.dataStore.getNameSpace(),
|
||||
this.buildLogs,
|
||||
this.activeBuilds
|
||||
this.buildLogs
|
||||
)
|
||||
}
|
||||
|
||||
@@ -72,10 +85,72 @@ class ServiceManager {
|
||||
return this.isReady
|
||||
}
|
||||
|
||||
deployNewVersion(appName: string, source: IImageSource) {
|
||||
scheduleDeployNewVersion(appName: string, source: IImageSource) {
|
||||
const self = this
|
||||
|
||||
let activeBuildAppName = self.isAnyBuildRunning()
|
||||
|
||||
if (activeBuildAppName) {
|
||||
self.buildLogs[appName] =
|
||||
self.buildLogs[appName] ||
|
||||
new BuildLog(CaptainConstants.configs.buildLogSize)
|
||||
this.activeBuilds[appName] = true
|
||||
|
||||
const existingBuildForTheSameApp = self.queuedBuilds.find(
|
||||
v => v.appName === appName
|
||||
)
|
||||
|
||||
if (existingBuildForTheSameApp) {
|
||||
self.buildLogs[appName].log(
|
||||
`A build for ${appName} was queued, it's now being replaced with a new build...`
|
||||
)
|
||||
|
||||
// replacing the new source!
|
||||
existingBuildForTheSameApp.source = source
|
||||
|
||||
const existingPromise =
|
||||
existingBuildForTheSameApp.promiseToSave.promise
|
||||
|
||||
if (!existingPromise)
|
||||
throw new Error(
|
||||
'Existing promise for the queued app is NULL!!'
|
||||
)
|
||||
|
||||
return existingPromise
|
||||
}
|
||||
|
||||
self.buildLogs[appName].log(
|
||||
`An active build (${activeBuildAppName}) is in progress. This build is queued...`
|
||||
)
|
||||
|
||||
let promiseToSave: QueuedPromise = {
|
||||
resolve: undefined,
|
||||
reject: undefined,
|
||||
promise: undefined,
|
||||
}
|
||||
|
||||
let promise = new Promise(function(resolve, reject) {
|
||||
promiseToSave.resolve = resolve
|
||||
promiseToSave.reject = reject
|
||||
})
|
||||
|
||||
promiseToSave.promise = promise
|
||||
|
||||
self.queuedBuilds.push({ appName, source, promiseToSave })
|
||||
|
||||
// This should only return when the build is finished,
|
||||
// somehow we need save the promise in queue - for "attached builds"
|
||||
return promise
|
||||
}
|
||||
|
||||
return this.startDeployingNewVersion(appName, source)
|
||||
}
|
||||
|
||||
startDeployingNewVersion(appName: string, source: IImageSource) {
|
||||
const self = this
|
||||
const dataStore = this.dataStore
|
||||
let deployedVersion: number
|
||||
|
||||
return Promise.resolve() //
|
||||
.then(function() {
|
||||
return dataStore.getAppsDataStore().createNewVersion(appName)
|
||||
@@ -104,9 +179,11 @@ class ServiceManager {
|
||||
)
|
||||
})
|
||||
.then(function() {
|
||||
self.onBuildFinished(appName)
|
||||
return self.ensureServiceInitedAndUpdated(appName)
|
||||
})
|
||||
.catch(function(error) {
|
||||
self.onBuildFinished(appName)
|
||||
return new Promise<void>(function(resolve, reject) {
|
||||
self.logBuildFailed(appName, error)
|
||||
reject(error)
|
||||
@@ -114,6 +191,17 @@ class ServiceManager {
|
||||
})
|
||||
}
|
||||
|
||||
onBuildFinished(appName: string) {
|
||||
const self = this
|
||||
self.activeBuilds[appName] = false
|
||||
|
||||
Promise.resolve().then(function() {
|
||||
let newBuild = self.queuedBuilds.shift()
|
||||
if (newBuild)
|
||||
self.startDeployingNewVersion(newBuild.appName, newBuild.source)
|
||||
})
|
||||
}
|
||||
|
||||
enableCustomDomainSsl(appName: string, customDomain: string) {
|
||||
const self = this
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@ export default class Utils {
|
||||
})
|
||||
}
|
||||
|
||||
static filterInPlace<T>(arr: T[], condition: (value: T) => boolean) {
|
||||
let newArray = arr.filter(condition)
|
||||
arr.splice(0, arr.length)
|
||||
newArray.forEach(value => arr.push(value))
|
||||
}
|
||||
|
||||
static dropFirstElements(arr: any[], maxLength: number) {
|
||||
arr = arr || []
|
||||
maxLength = Number(maxLength)
|
||||
|
||||
@@ -51,3 +51,37 @@ test('Testing dropFirstElements - smaller (0)', () => {
|
||||
expect(Utils.dropFirstElements(originalArray, 3).join(',')) //
|
||||
.toBe('')
|
||||
})
|
||||
|
||||
interface TestArray {
|
||||
val1: string
|
||||
val2: string
|
||||
}
|
||||
|
||||
function createTestArray() {
|
||||
const originalArray: TestArray[] = []
|
||||
for (let index = 0; index < 2; index++) {
|
||||
originalArray.push({
|
||||
val1: 'e-1-' + (index + 1),
|
||||
val2: 'e-2-' + (index + 1),
|
||||
})
|
||||
}
|
||||
return originalArray
|
||||
}
|
||||
|
||||
test('Testing filter in place - remove 1st', () => {
|
||||
const originalArray = createTestArray()
|
||||
|
||||
Utils.filterInPlace(originalArray, v => v.val1 !== 'e-1-1')
|
||||
expect(originalArray.length).toBe(1)
|
||||
expect(originalArray[0].val1).toBe('e-1-2')
|
||||
expect(originalArray[0].val2).toBe('e-2-2')
|
||||
})
|
||||
|
||||
test('Testing filter in place - remove 2nd', () => {
|
||||
const originalArray = createTestArray()
|
||||
|
||||
Utils.filterInPlace(originalArray, v => v.val1 !== 'e-1-2')
|
||||
expect(originalArray.length).toBe(1)
|
||||
expect(originalArray[0].val1).toBe('e-1-1')
|
||||
expect(originalArray[0].val2).toBe('e-2-1')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user