mirror of
https://github.com/caprover/caprover
synced 2025-10-30 10:07:01 +00:00
skeleton
This commit is contained in:
51
src/models/IOneClickAppModels.ts
Normal file
51
src/models/IOneClickAppModels.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { IHashMapGeneric } from './ICacheGeneric'
|
||||
|
||||
export interface IOneClickAppIdentifier {
|
||||
sortScore?: number // 0-1 and dynamically calculated based on search terms
|
||||
isOfficial?: boolean
|
||||
name: string
|
||||
displayName: string
|
||||
description: string
|
||||
logoUrl: string
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
export interface IOneClickVariable {
|
||||
id: string
|
||||
label: string
|
||||
defaultValue?: string
|
||||
validRegex?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface IDockerComposeService {
|
||||
image?: string
|
||||
volumes?: string[]
|
||||
ports?: string[]
|
||||
environment?: IHashMapGeneric<string>
|
||||
depends_on?: string[]
|
||||
hostname?: string
|
||||
cap_add?: string[]
|
||||
command?: string | string[]
|
||||
|
||||
// These are CapRover property, not DockerCompose. We use this instead of image if we need to extend the image.
|
||||
caproverExtra?: {
|
||||
dockerfileLines?: string[]
|
||||
containerHttpPort: number
|
||||
notExposeAsWebApp: boolean // This is actually a string "true", make sure to double negate!
|
||||
websocketSupport: boolean // This is actually a string "true", make sure to double negate!
|
||||
}
|
||||
}
|
||||
|
||||
export interface IOneClickTemplate {
|
||||
services: IHashMapGeneric<IDockerComposeService>
|
||||
captainVersion: number
|
||||
caproverOneClickApp: {
|
||||
instructions: {
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
displayName: string
|
||||
variables: IOneClickVariable[]
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
CapRoverEventFactory,
|
||||
CapRoverEventType,
|
||||
} from '../../../user/events/ICapRoverEvent'
|
||||
import OneClickAppDeployManager from '../../../user/oneclick/OneClickAppDeployManager'
|
||||
import { OneClickDeploymentJobRegistry } from '../../../user/oneclick/OneClickDeploymentJobRegistry'
|
||||
import CaptainConstants from '../../../utils/CaptainConstants'
|
||||
import Logger from '../../../utils/Logger'
|
||||
|
||||
@@ -269,4 +271,86 @@ router.get('/template/app', function (req, res, next) {
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.post('/deploy', function (req, res, next) {
|
||||
const template = req.body.template
|
||||
const values = req.body.values
|
||||
const deploymentJobRegistry = OneClickDeploymentJobRegistry.getInstance()
|
||||
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
if (!template) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Template is required'
|
||||
)
|
||||
}
|
||||
|
||||
const jobId = deploymentJobRegistry.createJob()
|
||||
|
||||
Logger.dev(`Starting one-click deployment with jobId: ${jobId}`)
|
||||
Logger.dev(`Template: ${JSON.stringify(template, null, 2)}`)
|
||||
Logger.dev(`Values: ${JSON.stringify(values, null, 2)}`)
|
||||
|
||||
new OneClickAppDeployManager((deploymentState) => {
|
||||
deploymentJobRegistry.updateJobProgress(jobId, deploymentState)
|
||||
Logger.dev(`Deployment state updated for jobId: ${jobId}`)
|
||||
Logger.dev(
|
||||
`Deployment state: ${JSON.stringify(deploymentState, null, 2)}`
|
||||
)
|
||||
}).startDeployProcess(template, values)
|
||||
|
||||
const baseApi = new BaseApi(
|
||||
ApiStatusCodes.STATUS_OK,
|
||||
'One-click deployment started'
|
||||
)
|
||||
baseApi.data = { jobId }
|
||||
res.send(baseApi)
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.get('/deploy/progress', function (req, res, next) {
|
||||
const jobId = req.query.jobId as string
|
||||
const deploymentJobRegistry = OneClickDeploymentJobRegistry.getInstance()
|
||||
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
// Validate input
|
||||
if (!jobId) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Job ID is required'
|
||||
)
|
||||
}
|
||||
|
||||
// Check if job exists
|
||||
if (!deploymentJobRegistry.jobExists(jobId)) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Job ID not found'
|
||||
)
|
||||
}
|
||||
|
||||
Logger.d(`Getting deployment progress for jobId: ${jobId}`)
|
||||
|
||||
// Get the current job state from deployment manager
|
||||
const jobState = deploymentJobRegistry.getJobState(jobId)
|
||||
|
||||
if (!jobState) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
||||
'Unable to retrieve job state'
|
||||
)
|
||||
}
|
||||
|
||||
const baseApi = new BaseApi(
|
||||
ApiStatusCodes.STATUS_OK,
|
||||
'Deployment progress retrieved'
|
||||
)
|
||||
baseApi.data = jobState
|
||||
res.send(baseApi)
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
282
src/user/oneclick/OneClickAppDeployManager.ts
Normal file
282
src/user/oneclick/OneClickAppDeployManager.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import { IHashMapGeneric } from '../../models/ICacheGeneric'
|
||||
import {
|
||||
IDockerComposeService,
|
||||
IOneClickTemplate,
|
||||
} from '../../models/IOneClickAppModels'
|
||||
import Utils from '../../utils/Utils'
|
||||
import OneClickAppDeploymentHelper from './OneClickAppDeploymentHelper'
|
||||
export const ONE_CLICK_APP_NAME_VAR_NAME = '$$cap_appname'
|
||||
|
||||
interface IDeploymentStep {
|
||||
stepName: string
|
||||
stepPromise: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface IDeploymentState {
|
||||
steps: string[]
|
||||
error: string
|
||||
successMessage?: string
|
||||
currentStep: number
|
||||
}
|
||||
|
||||
function replaceWith(
|
||||
replaceThis: string,
|
||||
withThis: string,
|
||||
mainString: string
|
||||
) {
|
||||
return mainString.split(replaceThis).join(withThis)
|
||||
}
|
||||
|
||||
export default class OneClickAppDeployManager {
|
||||
private deploymentHelper: OneClickAppDeploymentHelper =
|
||||
new OneClickAppDeploymentHelper()
|
||||
private template: IOneClickTemplate | undefined
|
||||
constructor(
|
||||
private onDeploymentStateChanged: (
|
||||
deploymentState: IDeploymentState
|
||||
) => void
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
startDeployProcess(
|
||||
template: IOneClickTemplate,
|
||||
values: IHashMapGeneric<string>
|
||||
) {
|
||||
const self = this
|
||||
let stringified = JSON.stringify(template)
|
||||
|
||||
for (
|
||||
let index = 0;
|
||||
index < template.caproverOneClickApp.variables.length;
|
||||
index++
|
||||
) {
|
||||
const element = template.caproverOneClickApp.variables[index]
|
||||
stringified = replaceWith(
|
||||
element.id,
|
||||
values[element.id] || '',
|
||||
stringified
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
this.template = JSON.parse(stringified)
|
||||
} catch (error) {
|
||||
this.onDeploymentStateChanged({
|
||||
steps: ['Parsing the template'],
|
||||
error: `Cannot parse: ${stringified}\n\n\n\n${error}`,
|
||||
currentStep: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Dependency tree and sort apps using "createAppsArrayInOrder"
|
||||
// Call "createDeploymentStepPromises" for all apps.
|
||||
// populate steps as string[]
|
||||
// create promise chain with catch -> error. Each promise gets followed by currentStep++ promise
|
||||
// Start running promises,
|
||||
|
||||
const apps = this.createAppsArrayInOrder()
|
||||
if (!apps) {
|
||||
self.onDeploymentStateChanged({
|
||||
steps: ['Parsing the template'],
|
||||
error: 'Cannot parse the template. Dependency tree cannot be resolved. Infinite loop!!',
|
||||
currentStep: 0,
|
||||
})
|
||||
} else if (apps.length === 0) {
|
||||
self.onDeploymentStateChanged({
|
||||
steps: ['Parsing the template'],
|
||||
error: 'Cannot parse the template. No services found!!',
|
||||
currentStep: 0,
|
||||
})
|
||||
} else {
|
||||
const steps: IDeploymentStep[] = []
|
||||
const capAppName = values[ONE_CLICK_APP_NAME_VAR_NAME]
|
||||
|
||||
const projectMemoryCache = { projectId: '' }
|
||||
|
||||
if (apps.length > 1) {
|
||||
steps.push(
|
||||
self.createDeploymentStepForProjectCreation(
|
||||
capAppName,
|
||||
projectMemoryCache
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
for (let index = 0; index < apps.length; index++) {
|
||||
const appToDeploy = apps[index]
|
||||
steps.push(
|
||||
...self.createDeploymentStepPromises(
|
||||
appToDeploy.appName,
|
||||
appToDeploy.service,
|
||||
capAppName,
|
||||
projectMemoryCache
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const stepsTexts: string[] = ['Parsing the template']
|
||||
for (let index = 0; index < steps.length; index++) {
|
||||
stepsTexts.push(steps[index].stepName)
|
||||
}
|
||||
|
||||
let currentStep = 0
|
||||
const onNextStepPromiseCreator = function () {
|
||||
return new Promise<void>(function (resolve) {
|
||||
currentStep++
|
||||
self.onDeploymentStateChanged(
|
||||
Utils.copyObject({
|
||||
steps: stepsTexts,
|
||||
error: '',
|
||||
currentStep,
|
||||
successMessage:
|
||||
currentStep >= stepsTexts.length
|
||||
? self.template!.caproverOneClickApp
|
||||
.instructions.end
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
let promise = onNextStepPromiseCreator()
|
||||
|
||||
for (let index = 0; index < steps.length; index++) {
|
||||
const element = steps[index]
|
||||
promise = promise
|
||||
.then(element.stepPromise)
|
||||
.then(onNextStepPromiseCreator)
|
||||
}
|
||||
|
||||
promise.catch(function (error) {
|
||||
self.onDeploymentStateChanged(
|
||||
Utils.copyObject({
|
||||
steps: stepsTexts,
|
||||
error: `Failed: ${error}`,
|
||||
currentStep,
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs an array which includes all services in order based on their dependencies.
|
||||
* The first element is an app without any dependencies. The second element can be an app that depends on the first app.
|
||||
*/
|
||||
private createAppsArrayInOrder() {
|
||||
const apps: {
|
||||
appName: string
|
||||
service: IDockerComposeService
|
||||
}[] = []
|
||||
|
||||
let numberOfServices = 0
|
||||
const servicesMap = this.template!.services
|
||||
Object.keys(servicesMap).forEach(function (key) {
|
||||
numberOfServices++
|
||||
})
|
||||
|
||||
let itCount = 0
|
||||
while (apps.length < numberOfServices) {
|
||||
if (itCount > numberOfServices) {
|
||||
// we are stuck in an infinite loop
|
||||
return undefined
|
||||
}
|
||||
itCount++
|
||||
|
||||
Object.keys(servicesMap).forEach(function (appName) {
|
||||
for (let index = 0; index < apps.length; index++) {
|
||||
const element = apps[index]
|
||||
if (element.appName === appName) {
|
||||
// already added
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const service = servicesMap[appName]
|
||||
|
||||
const dependsOn = service.depends_on || []
|
||||
|
||||
for (let index = 0; index < dependsOn.length; index++) {
|
||||
const element = dependsOn[index]
|
||||
let dependencyAlreadyAdded = false
|
||||
for (let j = 0; j < apps.length; j++) {
|
||||
if (element === apps[j].appName) {
|
||||
dependencyAlreadyAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!dependencyAlreadyAdded) return
|
||||
}
|
||||
|
||||
apps.push({
|
||||
appName,
|
||||
service,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return apps
|
||||
}
|
||||
private createDeploymentStepForProjectCreation(
|
||||
capAppName: string,
|
||||
projectMemoryCache: { projectId: string }
|
||||
): IDeploymentStep {
|
||||
const self = this
|
||||
return {
|
||||
stepName: `Creating project ${capAppName}`,
|
||||
stepPromise: function () {
|
||||
return self.deploymentHelper.createRegisterPromiseProject(
|
||||
capAppName,
|
||||
projectMemoryCache
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private createDeploymentStepPromises(
|
||||
appName: string,
|
||||
dockerComposeService: IDockerComposeService,
|
||||
capAppName: string,
|
||||
projectMemoryCache: { projectId: string }
|
||||
): IDeploymentStep[] {
|
||||
const promises: IDeploymentStep[] = []
|
||||
const self = this
|
||||
|
||||
promises.push({
|
||||
stepName: `Registering ${appName}`,
|
||||
stepPromise: function () {
|
||||
return self.deploymentHelper.createRegisterPromise(
|
||||
appName,
|
||||
dockerComposeService,
|
||||
projectMemoryCache
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
promises.push({
|
||||
stepName: `Configuring ${appName} (volumes, ports, environmental variables)`,
|
||||
stepPromise: function () {
|
||||
return self.deploymentHelper.createConfigurationPromise(
|
||||
appName,
|
||||
dockerComposeService,
|
||||
capAppName
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
promises.push({
|
||||
stepName: `Deploying ${appName} (might take up to a minute)`,
|
||||
stepPromise: function () {
|
||||
return self.deploymentHelper.createDeploymentPromise(
|
||||
appName,
|
||||
dockerComposeService
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
return promises
|
||||
}
|
||||
}
|
||||
209
src/user/oneclick/OneClickAppDeploymentHelper.ts
Normal file
209
src/user/oneclick/OneClickAppDeploymentHelper.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { IAppDef } from '../../models/AppDefinition'
|
||||
import { ICaptainDefinition } from '../../models/ICaptainDefinition'
|
||||
import { IDockerComposeService } from '../../models/IOneClickAppModels'
|
||||
import { ProjectDefinition } from '../../models/ProjectDefinition'
|
||||
import DockerComposeToServiceOverride from '../../utils/DockerComposeToServiceOverride'
|
||||
import Utils from '../../utils/Utils'
|
||||
|
||||
// TODO - Replace with actual API implementation
|
||||
|
||||
class ApiManager {
|
||||
registerNewApp(
|
||||
appName: string,
|
||||
projectId: string,
|
||||
hasVolume: boolean,
|
||||
skipIfExists: boolean
|
||||
): Promise<any> {
|
||||
// Mock implementation
|
||||
return Promise.resolve({ success: true })
|
||||
}
|
||||
|
||||
registerProject(projectDef: ProjectDefinition): Promise<any> {
|
||||
// Mock implementation
|
||||
return Promise.resolve({ id: 'mock-project-id' })
|
||||
}
|
||||
getAllApps(): Promise<any> {
|
||||
// Mock implementation
|
||||
return Promise.resolve({ appDefinitions: [] })
|
||||
}
|
||||
updateConfigAndSave(appName: string, appDef: IAppDef): Promise<any> {
|
||||
// Mock implementation
|
||||
return Promise.resolve({ success: true })
|
||||
}
|
||||
uploadCaptainDefinitionContent(
|
||||
appName: string,
|
||||
captainDefinition: ICaptainDefinition,
|
||||
tarFileBase64: string,
|
||||
skipIfExists: boolean
|
||||
): Promise<any> {
|
||||
// Mock implementation
|
||||
return Promise.resolve({ success: true })
|
||||
}
|
||||
}
|
||||
|
||||
export default class OneClickAppDeploymentHelper {
|
||||
private apiManager: ApiManager = new ApiManager()
|
||||
|
||||
createRegisterPromise(
|
||||
appName: string,
|
||||
dockerComposeService: IDockerComposeService,
|
||||
projectMemoryCache: { projectId: string }
|
||||
) {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
return self.apiManager.registerNewApp(
|
||||
appName,
|
||||
projectMemoryCache.projectId,
|
||||
!!dockerComposeService.volumes &&
|
||||
!!dockerComposeService.volumes.length,
|
||||
false
|
||||
)
|
||||
})
|
||||
}
|
||||
createRegisterPromiseProject(
|
||||
appName: string,
|
||||
projectMemoryCache: { projectId: string }
|
||||
) {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
const projectDef: ProjectDefinition = {
|
||||
id: '',
|
||||
name: appName,
|
||||
description: ``,
|
||||
}
|
||||
// change backend to ensure this returns project ID
|
||||
return self.apiManager
|
||||
.registerProject(projectDef)
|
||||
.then(function (data) {
|
||||
projectMemoryCache.projectId = data.id
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createConfigurationPromise(
|
||||
appName: string,
|
||||
dockerComposeService: IDockerComposeService,
|
||||
capAppName: string
|
||||
) {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
return self.apiManager
|
||||
.getAllApps()
|
||||
.then(function (data) {
|
||||
const appDefs = data.appDefinitions as IAppDef[]
|
||||
for (let index = 0; index < appDefs.length; index++) {
|
||||
const element = appDefs[index]
|
||||
if (element.appName === appName) {
|
||||
return Utils.copyObject(element)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(function (appDef) {
|
||||
if (!appDef) {
|
||||
throw new Error(
|
||||
'App was not found right after registering!!'
|
||||
)
|
||||
}
|
||||
|
||||
appDef.volumes = appDef.volumes || []
|
||||
appDef.tags = [
|
||||
{
|
||||
tagName: capAppName,
|
||||
},
|
||||
]
|
||||
|
||||
const vols = dockerComposeService.volumes || []
|
||||
for (let i = 0; i < vols.length; i++) {
|
||||
const elements = vols[i].split(':')
|
||||
if (elements[0].startsWith('/')) {
|
||||
appDef.volumes.push({
|
||||
hostPath: elements[0],
|
||||
containerPath: elements[1],
|
||||
})
|
||||
} else {
|
||||
appDef.volumes.push({
|
||||
volumeName: elements[0],
|
||||
containerPath: elements[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
appDef.ports = appDef.ports || []
|
||||
const ports = dockerComposeService.ports || []
|
||||
for (let i = 0; i < ports.length; i++) {
|
||||
const elements = ports[i].split(':')
|
||||
appDef.ports.push({
|
||||
hostPort: Number(elements[0]),
|
||||
containerPort: Number(elements[1]),
|
||||
})
|
||||
}
|
||||
|
||||
appDef.envVars = appDef.envVars || []
|
||||
const environment = dockerComposeService.environment || {}
|
||||
Object.keys(environment).forEach(function (envKey) {
|
||||
appDef.envVars.push({
|
||||
key: envKey,
|
||||
value: environment[envKey],
|
||||
})
|
||||
})
|
||||
|
||||
const overrideYaml =
|
||||
DockerComposeToServiceOverride.convertUnconsumedComposeParametersToServiceOverride(
|
||||
dockerComposeService
|
||||
)
|
||||
|
||||
if (overrideYaml) {
|
||||
appDef.serviceUpdateOverride = overrideYaml
|
||||
}
|
||||
|
||||
if (dockerComposeService.caproverExtra) {
|
||||
if (
|
||||
dockerComposeService.caproverExtra.containerHttpPort
|
||||
) {
|
||||
appDef.containerHttpPort =
|
||||
dockerComposeService.caproverExtra.containerHttpPort
|
||||
}
|
||||
|
||||
if (
|
||||
dockerComposeService.caproverExtra.notExposeAsWebApp
|
||||
) {
|
||||
appDef.notExposeAsWebApp = true
|
||||
}
|
||||
if (
|
||||
dockerComposeService.caproverExtra.websocketSupport
|
||||
) {
|
||||
appDef.websocketSupport = true
|
||||
}
|
||||
}
|
||||
|
||||
return self.apiManager.updateConfigAndSave(appName, appDef)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createDeploymentPromise(
|
||||
appName: string,
|
||||
dockerComposeService: IDockerComposeService
|
||||
) {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
const captainDefinition: ICaptainDefinition = {
|
||||
schemaVersion: 2,
|
||||
}
|
||||
|
||||
if (dockerComposeService.image) {
|
||||
captainDefinition.imageName = dockerComposeService.image
|
||||
} else {
|
||||
captainDefinition.dockerfileLines =
|
||||
dockerComposeService.caproverExtra?.dockerfileLines
|
||||
}
|
||||
|
||||
return self.apiManager.uploadCaptainDefinitionContent(
|
||||
appName,
|
||||
captainDefinition,
|
||||
'',
|
||||
false
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
127
src/user/oneclick/OneClickDeploymentJobRegistry.ts
Normal file
127
src/user/oneclick/OneClickDeploymentJobRegistry.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
export interface IDeploymentState {
|
||||
steps: string[]
|
||||
error: string
|
||||
successMessage?: string
|
||||
currentStep: number
|
||||
}
|
||||
|
||||
interface IJobInfo {
|
||||
jobId: string
|
||||
state: IDeploymentState
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory registry for tracking one-click deployment job progress
|
||||
*/
|
||||
export class OneClickDeploymentJobRegistry {
|
||||
private static instance: OneClickDeploymentJobRegistry
|
||||
private jobs: Map<string, IJobInfo> = new Map()
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): OneClickDeploymentJobRegistry {
|
||||
if (!OneClickDeploymentJobRegistry.instance) {
|
||||
OneClickDeploymentJobRegistry.instance =
|
||||
new OneClickDeploymentJobRegistry()
|
||||
}
|
||||
return OneClickDeploymentJobRegistry.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique job ID
|
||||
*/
|
||||
private generateJobId(): string {
|
||||
return `deploy_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new deployment job with initial state
|
||||
* Returns the generated job ID
|
||||
*/
|
||||
public createJob(): string {
|
||||
const jobId = this.generateJobId()
|
||||
const initialState: IDeploymentState = {
|
||||
steps: ['Queuing deployment'],
|
||||
currentStep: 0,
|
||||
error: '',
|
||||
successMessage: '',
|
||||
}
|
||||
|
||||
const jobInfo: IJobInfo = {
|
||||
jobId,
|
||||
state: initialState,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
this.jobs.set(jobId, jobInfo)
|
||||
return jobId
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the progress of a deployment job
|
||||
*/
|
||||
public updateJobProgress(
|
||||
jobId: string,
|
||||
newState: IDeploymentState
|
||||
): boolean {
|
||||
const jobInfo = this.jobs.get(jobId)
|
||||
if (!jobInfo) {
|
||||
return false
|
||||
}
|
||||
jobInfo.state = newState
|
||||
jobInfo.updatedAt = new Date()
|
||||
|
||||
this.jobs.set(jobId, jobInfo)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state of a deployment job
|
||||
*/
|
||||
public getJobState(jobId: string): IDeploymentState | null {
|
||||
const jobInfo = this.jobs.get(jobId)
|
||||
return jobInfo ? jobInfo.state : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a job exists
|
||||
*/
|
||||
public jobExists(jobId: string): boolean {
|
||||
return this.jobs.has(jobId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a job from tracking (cleanup)
|
||||
*/
|
||||
public removeJob(jobId: string): boolean {
|
||||
return this.jobs.delete(jobId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all job IDs (for debugging/admin purposes)
|
||||
*/
|
||||
public getAllJobIds(): string[] {
|
||||
return Array.from(this.jobs.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old jobs (older than specified hours)
|
||||
*/
|
||||
public cleanupOldJobs(olderThanHours: number = 24): number {
|
||||
const cutoffTime = new Date()
|
||||
cutoffTime.setHours(cutoffTime.getHours() - olderThanHours)
|
||||
|
||||
let removedCount = 0
|
||||
for (const [jobId, jobInfo] of this.jobs.entries()) {
|
||||
if (jobInfo.updatedAt < cutoffTime) {
|
||||
this.jobs.delete(jobId)
|
||||
removedCount++
|
||||
}
|
||||
}
|
||||
|
||||
return removedCount
|
||||
}
|
||||
}
|
||||
102
src/utils/DockerComposeToServiceOverride.ts
Normal file
102
src/utils/DockerComposeToServiceOverride.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import * as yaml from 'yaml'
|
||||
import { IDockerComposeService } from '../models/IOneClickAppModels'
|
||||
import Utils from './Utils'
|
||||
|
||||
export default class DockerComposeToServiceOverride {
|
||||
/**
|
||||
* Converts the unsupported docker compose parameters to CapRover service override definition.
|
||||
* Port, replicas, env vars, volumes, and image are supplied through CapRover definition,
|
||||
* network will be set to captain-overlay restart_policy is not generally needed,
|
||||
* by default docker services restart automatically.
|
||||
* Only parse parameters that are not from the aforementioned list.
|
||||
* The only useful parameter that we are parsing at the moment is hostname: https://github.com/caprover/caprover/issues/404
|
||||
*
|
||||
* @param docker compose service definition
|
||||
* @returns the override service definition in yaml format
|
||||
*/
|
||||
static convertUnconsumedComposeParametersToServiceOverride(
|
||||
compose: IDockerComposeService
|
||||
) {
|
||||
const overrides = [] as any[]
|
||||
overrides.push(DockerComposeToServiceOverride.parseHostname(compose))
|
||||
overrides.push(DockerComposeToServiceOverride.parseCapAdd(compose))
|
||||
overrides.push(DockerComposeToServiceOverride.parseCommand(compose))
|
||||
// Add more overrides here if needed
|
||||
|
||||
let mergedOverride = {} as any
|
||||
overrides.forEach((o) => {
|
||||
mergedOverride = Utils.mergeObjects(mergedOverride, o)
|
||||
})
|
||||
if (Object.keys(mergedOverride).length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return yaml.stringify(mergedOverride)
|
||||
}
|
||||
|
||||
private static parseCommand(compose: IDockerComposeService) {
|
||||
const override = {} as any
|
||||
|
||||
function parseDockerCMD(cmdString: string) {
|
||||
// Matches sequences inside quotes or sequences without spaces
|
||||
const regex = /[^\s"']+|"([^"]*)"|'([^']*)'/g
|
||||
let match
|
||||
const args = []
|
||||
|
||||
while ((match = regex.exec(cmdString))) {
|
||||
// If matched quotes, add the first group that is not undefined
|
||||
if (match[1]) {
|
||||
args.push(match[1])
|
||||
} else if (match[2]) {
|
||||
args.push(match[2])
|
||||
} else {
|
||||
args.push(match[0])
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
if (compose.command) {
|
||||
override.TaskTemplate = {
|
||||
ContainerSpec: {
|
||||
Command: Array.isArray(compose.command)
|
||||
? compose.command
|
||||
: parseDockerCMD(compose.command),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return override
|
||||
}
|
||||
|
||||
private static parseHostname(compose: IDockerComposeService) {
|
||||
const override = {} as any
|
||||
const hostname = compose.hostname ? `${compose.hostname}`.trim() : ''
|
||||
if (compose.hostname) {
|
||||
override.TaskTemplate = {
|
||||
ContainerSpec: {
|
||||
Hostname: hostname,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return override
|
||||
}
|
||||
|
||||
private static parseCapAdd(compose: IDockerComposeService) {
|
||||
const override = {} as any
|
||||
if (!!compose.cap_add && Array.isArray(compose.cap_add)) {
|
||||
const capabilityAdd = compose.cap_add
|
||||
.map((cap) => cap.toUpperCase())
|
||||
.map((cap) => (cap.startsWith(`CAP`) ? cap : `CAP_${cap}`))
|
||||
override.TaskTemplate = {
|
||||
ContainerSpec: {
|
||||
CapabilityAdd: capabilityAdd,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return override
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user