v1.0.6 (#30)
Features:
- Rust support 🦀 (Thanks to @pepoviola)
- Add a default rewrite rule to PHP apps (to index.php)
- Able to control upgrades in a straightforward way
Fixes:
- Improved upgrade scripts
- Simplified prechecks before deployment
- Fixed path deployments
- Fixed already defined apps redirections
- Better error handling - still needs a lot of improvement here!
			
			
This commit is contained in:
		@@ -5,31 +5,36 @@ const { docker } = require('../../../libs/docker')
 | 
			
		||||
 | 
			
		||||
module.exports = async function (fastify) {
 | 
			
		||||
  fastify.post('/', async (request, reply) => {
 | 
			
		||||
    if (!await verifyUserId(request.headers.authorization)) {
 | 
			
		||||
      reply.code(500).send({ error: 'Invalid request' })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const configuration = setDefaultConfiguration(request.body)
 | 
			
		||||
    try {
 | 
			
		||||
      if (!await verifyUserId(request.headers.authorization)) {
 | 
			
		||||
        reply.code(500).send({ error: 'Invalid request' })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const configuration = setDefaultConfiguration(request.body)
 | 
			
		||||
 | 
			
		||||
    const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
 | 
			
		||||
    let foundDomain = false
 | 
			
		||||
      const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
 | 
			
		||||
      let foundDomain = false
 | 
			
		||||
 | 
			
		||||
    for (const service of services) {
 | 
			
		||||
      const running = JSON.parse(service.Spec.Labels.configuration)
 | 
			
		||||
      if (running) {
 | 
			
		||||
        if (
 | 
			
		||||
          running.publish.domain === configuration.publish.domain &&
 | 
			
		||||
          running.repository.id !== configuration.repository.id
 | 
			
		||||
        ) {
 | 
			
		||||
          foundDomain = true
 | 
			
		||||
      for (const service of services) {
 | 
			
		||||
        const running = JSON.parse(service.Spec.Labels.configuration)
 | 
			
		||||
        if (running) {
 | 
			
		||||
          if (
 | 
			
		||||
            running.publish.domain === configuration.publish.domain &&
 | 
			
		||||
            running.repository.id !== configuration.repository.id &&
 | 
			
		||||
            running.publish.path === configuration.publish.path
 | 
			
		||||
          ) {
 | 
			
		||||
            foundDomain = true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (fastify.config.DOMAIN === configuration.publish.domain) foundDomain = true
 | 
			
		||||
      if (foundDomain) {
 | 
			
		||||
        reply.code(500).send({ message: 'Domain already in use.' })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      return { message: 'OK' }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw { error, type: 'server' }
 | 
			
		||||
    }
 | 
			
		||||
    if (fastify.config.DOMAIN === configuration.publish.domain) foundDomain = true
 | 
			
		||||
    if (foundDomain) {
 | 
			
		||||
      reply.code(500).send({ message: 'Domain already in use.' })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    return { message: 'OK' }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
 | 
			
		||||
const { verifyUserId, cleanupTmp, execShellAsync } = require('../../../../libs/common')
 | 
			
		||||
const { verifyUserId, cleanupTmp } = require('../../../../libs/common')
 | 
			
		||||
const Deployment = require('../../../../models/Deployment')
 | 
			
		||||
const { queueAndBuild } = require('../../../../libs/applications')
 | 
			
		||||
const { setDefaultConfiguration } = require('../../../../libs/applications/configuration')
 | 
			
		||||
const { setDefaultConfiguration, precheckDeployment } = require('../../../../libs/applications/configuration')
 | 
			
		||||
const { docker } = require('../../../../libs/docker')
 | 
			
		||||
const cloneRepository = require('../../../../libs/applications/github/cloneRepository')
 | 
			
		||||
 | 
			
		||||
@@ -32,90 +32,43 @@ module.exports = async function (fastify) {
 | 
			
		||||
  //     },
 | 
			
		||||
  // };
 | 
			
		||||
  fastify.post('/', async (request, reply) => {
 | 
			
		||||
    if (!await verifyUserId(request.headers.authorization)) {
 | 
			
		||||
    try {
 | 
			
		||||
      await verifyUserId(request.headers.authorization)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      reply.code(500).send({ error: 'Invalid request' })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
 | 
			
		||||
      const configuration = setDefaultConfiguration(request.body)
 | 
			
		||||
      await cloneRepository(configuration)
 | 
			
		||||
      const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
 | 
			
		||||
 | 
			
		||||
    const configuration = setDefaultConfiguration(request.body)
 | 
			
		||||
 | 
			
		||||
    const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
 | 
			
		||||
 | 
			
		||||
    await cloneRepository(configuration)
 | 
			
		||||
 | 
			
		||||
    let foundService = false
 | 
			
		||||
    let foundDomain = false
 | 
			
		||||
    let configChanged = false
 | 
			
		||||
    let imageChanged = false
 | 
			
		||||
 | 
			
		||||
    let forceUpdate = false
 | 
			
		||||
 | 
			
		||||
    for (const service of services) {
 | 
			
		||||
      const running = JSON.parse(service.Spec.Labels.configuration)
 | 
			
		||||
      if (running) {
 | 
			
		||||
        if (
 | 
			
		||||
          running.publish.domain === configuration.publish.domain &&
 | 
			
		||||
          running.repository.id !== configuration.repository.id
 | 
			
		||||
        ) {
 | 
			
		||||
          foundDomain = true
 | 
			
		||||
        }
 | 
			
		||||
        if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
 | 
			
		||||
          // Base service configuration changed
 | 
			
		||||
          if (!running.build.container.baseSHA || running.build.container.baseSHA !== configuration.build.container.baseSHA) {
 | 
			
		||||
            configChanged = true
 | 
			
		||||
          }
 | 
			
		||||
          const state = await execShellAsync(`docker stack ps ${running.build.container.name} --format '{{ json . }}'`)
 | 
			
		||||
          const isError = state.split('\n').filter(n => n).map(s => JSON.parse(s)).filter(n => n.DesiredState !== 'Running')
 | 
			
		||||
          if (isError.length > 0) forceUpdate = true
 | 
			
		||||
 | 
			
		||||
          foundService = true
 | 
			
		||||
          const runningWithoutContainer = JSON.parse(JSON.stringify(running))
 | 
			
		||||
          delete runningWithoutContainer.build.container
 | 
			
		||||
 | 
			
		||||
          const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration))
 | 
			
		||||
          delete configurationWithoutContainer.build.container
 | 
			
		||||
 | 
			
		||||
          // If only the configuration changed
 | 
			
		||||
          if (JSON.stringify(runningWithoutContainer.build) !== JSON.stringify(configurationWithoutContainer.build) || JSON.stringify(runningWithoutContainer.publish) !== JSON.stringify(configurationWithoutContainer.publish)) configChanged = true
 | 
			
		||||
          // If only the image changed
 | 
			
		||||
          if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true
 | 
			
		||||
          // If build pack changed, forceUpdate the service
 | 
			
		||||
          if (running.build.pack !== configuration.build.pack) forceUpdate = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (foundDomain) {
 | 
			
		||||
      cleanupTmp(configuration.general.workdir)
 | 
			
		||||
      reply.code(500).send({ message: 'Domain already in use.' })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (forceUpdate) {
 | 
			
		||||
      imageChanged = false
 | 
			
		||||
      configChanged = false
 | 
			
		||||
    } else {
 | 
			
		||||
      if (foundService && !imageChanged && !configChanged) {
 | 
			
		||||
      if (foundService && !forceUpdate && !imageChanged && !configChanged) {
 | 
			
		||||
        cleanupTmp(configuration.general.workdir)
 | 
			
		||||
        reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const alreadyQueued = await Deployment.find({
 | 
			
		||||
        repoId: configuration.repository.id,
 | 
			
		||||
        branch: configuration.repository.branch,
 | 
			
		||||
        organization: configuration.repository.organization,
 | 
			
		||||
        name: configuration.repository.name,
 | 
			
		||||
        domain: configuration.publish.domain,
 | 
			
		||||
        progress: { $in: ['queued', 'inprogress'] }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (alreadyQueued.length > 0) {
 | 
			
		||||
        reply.code(200).send({ message: 'Already in the queue.' })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      queueAndBuild(configuration, imageChanged)
 | 
			
		||||
 | 
			
		||||
      reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw { error, type: 'server' }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const alreadyQueued = await Deployment.find({
 | 
			
		||||
      repoId: configuration.repository.id,
 | 
			
		||||
      branch: configuration.repository.branch,
 | 
			
		||||
      organization: configuration.repository.organization,
 | 
			
		||||
      name: configuration.repository.name,
 | 
			
		||||
      domain: configuration.publish.domain,
 | 
			
		||||
      progress: { $in: ['queued', 'inprogress'] }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (alreadyQueued.length > 0) {
 | 
			
		||||
      reply.code(200).send({ message: 'Already in the queue.' })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    queueAndBuild(configuration, services, configChanged, imageChanged)
 | 
			
		||||
 | 
			
		||||
    reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,25 +18,29 @@ module.exports = async function (fastify) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  fastify.get('/', { schema: getLogSchema }, async (request, reply) => {
 | 
			
		||||
    const { repoId, branch, page } = request.query
 | 
			
		||||
    const onePage = 5
 | 
			
		||||
    const show = Number(page) * onePage || 5
 | 
			
		||||
    const deploy = await Deployment.find({ repoId, branch })
 | 
			
		||||
      .select('-_id -__v -repoId')
 | 
			
		||||
      .sort({ createdAt: 'desc' })
 | 
			
		||||
      .limit(show)
 | 
			
		||||
    try {
 | 
			
		||||
      const { repoId, branch, page } = request.query
 | 
			
		||||
      const onePage = 5
 | 
			
		||||
      const show = Number(page) * onePage || 5
 | 
			
		||||
      const deploy = await Deployment.find({ repoId, branch })
 | 
			
		||||
        .select('-_id -__v -repoId')
 | 
			
		||||
        .sort({ createdAt: 'desc' })
 | 
			
		||||
        .limit(show)
 | 
			
		||||
 | 
			
		||||
    const finalLogs = deploy.map(d => {
 | 
			
		||||
      const finalLogs = { ...d._doc }
 | 
			
		||||
      const finalLogs = deploy.map(d => {
 | 
			
		||||
        const finalLogs = { ...d._doc }
 | 
			
		||||
 | 
			
		||||
      const updatedAt = dayjs(d.updatedAt).utc()
 | 
			
		||||
        const updatedAt = dayjs(d.updatedAt).utc()
 | 
			
		||||
 | 
			
		||||
      finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000
 | 
			
		||||
      finalLogs.since = updatedAt.fromNow()
 | 
			
		||||
        finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000
 | 
			
		||||
        finalLogs.since = updatedAt.fromNow()
 | 
			
		||||
 | 
			
		||||
        return finalLogs
 | 
			
		||||
      })
 | 
			
		||||
      return finalLogs
 | 
			
		||||
    })
 | 
			
		||||
    return finalLogs
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw { error, type: 'server' }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  fastify.get('/:deployId', async (request, reply) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,13 @@ const { docker } = require('../../../libs/docker')
 | 
			
		||||
 | 
			
		||||
module.exports = async function (fastify) {
 | 
			
		||||
  fastify.get('/', async (request, reply) => {
 | 
			
		||||
    const { name } = request.query
 | 
			
		||||
    const service = await docker.engine.getService(`${name}_${name}`)
 | 
			
		||||
    const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a)
 | 
			
		||||
    return { logs }
 | 
			
		||||
    try {
 | 
			
		||||
      const { name } = request.query
 | 
			
		||||
      const service = await docker.engine.getService(`${name}_${name}`)
 | 
			
		||||
      const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a)
 | 
			
		||||
      return { logs }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw { error, type: 'server' }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,60 +1,6 @@
 | 
			
		||||
const { docker } = require('../../libs/docker')
 | 
			
		||||
 | 
			
		||||
module.exports = async function (fastify) {
 | 
			
		||||
  // const getConfig = {
 | 
			
		||||
  //   querystring: {
 | 
			
		||||
  //     type: 'object',
 | 
			
		||||
  //     properties: {
 | 
			
		||||
  //       repoId: { type: 'number' },
 | 
			
		||||
  //       branch: { type: 'string' }
 | 
			
		||||
  //     },
 | 
			
		||||
  //     required: ['repoId', 'branch']
 | 
			
		||||
  //   }
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  // const saveConfig = {
 | 
			
		||||
  //   body: {
 | 
			
		||||
  //     type: 'object',
 | 
			
		||||
  //     properties: {
 | 
			
		||||
  //       build: {
 | 
			
		||||
  //         type: 'object',
 | 
			
		||||
  //         properties: {
 | 
			
		||||
  //           baseDir: { type: 'string' },
 | 
			
		||||
  //           installCmd: { type: 'string' },
 | 
			
		||||
  //           buildCmd: { type: 'string' }
 | 
			
		||||
  //         },
 | 
			
		||||
  //         required: ['baseDir', 'installCmd', 'buildCmd']
 | 
			
		||||
  //       },
 | 
			
		||||
  //       publish: {
 | 
			
		||||
  //         type: 'object',
 | 
			
		||||
  //         properties: {
 | 
			
		||||
  //           publishDir: { type: 'string' },
 | 
			
		||||
  //           domain: { type: 'string' },
 | 
			
		||||
  //           pathPrefix: { type: 'string' },
 | 
			
		||||
  //           port: { type: 'number' }
 | 
			
		||||
  //         },
 | 
			
		||||
  //         required: ['publishDir', 'domain', 'pathPrefix', 'port']
 | 
			
		||||
  //       },
 | 
			
		||||
  //       previewDeploy: { type: 'boolean' },
 | 
			
		||||
  //       branch: { type: 'string' },
 | 
			
		||||
  //       repoId: { type: 'number' },
 | 
			
		||||
  //       buildPack: { type: 'string' },
 | 
			
		||||
  //       fullName: { type: 'string' },
 | 
			
		||||
  //       installationId: { type: 'number' }
 | 
			
		||||
  //     },
 | 
			
		||||
  //     required: ['build', 'publish', 'previewDeploy', 'branch', 'repoId', 'buildPack', 'fullName', 'installationId']
 | 
			
		||||
  //   }
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  // fastify.get("/all", async (request, reply) => {
 | 
			
		||||
  //   return await Config.find().select("-_id -__v");
 | 
			
		||||
  // });
 | 
			
		||||
 | 
			
		||||
  // fastify.get("/", { schema: getConfig }, async (request, reply) => {
 | 
			
		||||
  //   const { repoId, branch } = request.query;
 | 
			
		||||
  //   return await Config.findOne({ repoId, branch }).select("-_id -__v");
 | 
			
		||||
  // });
 | 
			
		||||
 | 
			
		||||
  fastify.post('/', async (request, reply) => {
 | 
			
		||||
    const { name, organization, branch } = request.body
 | 
			
		||||
    const services = await docker.engine.listServices()
 | 
			
		||||
@@ -79,25 +25,4 @@ module.exports = async function (fastify) {
 | 
			
		||||
      reply.code(500).send({ message: 'No configuration found.' })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // fastify.delete("/", async (request, reply) => {
 | 
			
		||||
  //   const { repoId, branch } = request.body;
 | 
			
		||||
 | 
			
		||||
  //   const deploys = await Deployment.find({ repoId, branch })
 | 
			
		||||
  //   const found = deploys.filter(d => d.progress !== 'done' && d.progress !== 'failed')
 | 
			
		||||
  //   if (found.length > 0) {
 | 
			
		||||
  //     throw new Error('Deployment inprogress, cannot delete now.');
 | 
			
		||||
  //   }
 | 
			
		||||
 | 
			
		||||
  //   const config = await Config.findOneAndDelete({ repoId, branch })
 | 
			
		||||
  //   for (const deploy of deploys) {
 | 
			
		||||
  //     await ApplicationLog.findOneAndRemove({ deployId: deploy.deployId });
 | 
			
		||||
  //   }
 | 
			
		||||
  //   const secrets = await Secret.find({ repoId, branch });
 | 
			
		||||
  //   for (const secret of secrets) {
 | 
			
		||||
  //     await Secret.findByIdAndRemove(secret._id);
 | 
			
		||||
  //   }
 | 
			
		||||
  //   await execShellAsync(`docker stack rm ${config.containerName}`);
 | 
			
		||||
  //   return { message: 'Deleted application and related configurations.' };
 | 
			
		||||
  // });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ module.exports = async function (fastify) {
 | 
			
		||||
        r.Spec.Labels.configuration = configuration
 | 
			
		||||
        return r
 | 
			
		||||
      })
 | 
			
		||||
      applications = [...new Map(applications.map(item => [item.Spec.Labels.configuration.publish.domain, item])).values()]
 | 
			
		||||
      applications = [...new Map(applications.map(item => [item.Spec.Labels.configuration.publish.domain + item.Spec.Labels.configuration.publish.path, item])).values()]
 | 
			
		||||
      return {
 | 
			
		||||
        serverLogs,
 | 
			
		||||
        applications: {
 | 
			
		||||
@@ -55,6 +55,8 @@ module.exports = async function (fastify) {
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      if (error.code === 'ENOENT' && error.errno === -2) {
 | 
			
		||||
        throw new Error(`Docker service unavailable at ${error.address}.`)
 | 
			
		||||
      } else {
 | 
			
		||||
        throw { error, type: 'server' }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 
 | 
			
		||||
@@ -45,124 +45,128 @@ module.exports = async function (fastify) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fastify.post('/deploy', { schema: postSchema }, async (request, reply) => {
 | 
			
		||||
    let { type, defaultDatabaseName } = request.body
 | 
			
		||||
    const passwords = generator.generateMultiple(2, {
 | 
			
		||||
      length: 24,
 | 
			
		||||
      numbers: true,
 | 
			
		||||
      strict: true
 | 
			
		||||
    })
 | 
			
		||||
    const usernames = generator.generateMultiple(2, {
 | 
			
		||||
      length: 10,
 | 
			
		||||
      numbers: true,
 | 
			
		||||
      strict: true
 | 
			
		||||
    })
 | 
			
		||||
    // TODO: Query for existing db with the same name
 | 
			
		||||
    const nickname = getUniq()
 | 
			
		||||
    try {
 | 
			
		||||
      let { type, defaultDatabaseName } = request.body
 | 
			
		||||
      const passwords = generator.generateMultiple(2, {
 | 
			
		||||
        length: 24,
 | 
			
		||||
        numbers: true,
 | 
			
		||||
        strict: true
 | 
			
		||||
      })
 | 
			
		||||
      const usernames = generator.generateMultiple(2, {
 | 
			
		||||
        length: 10,
 | 
			
		||||
        numbers: true,
 | 
			
		||||
        strict: true
 | 
			
		||||
      })
 | 
			
		||||
      // TODO: Query for existing db with the same name
 | 
			
		||||
      const nickname = getUniq()
 | 
			
		||||
 | 
			
		||||
    if (!defaultDatabaseName) defaultDatabaseName = nickname
 | 
			
		||||
      if (!defaultDatabaseName) defaultDatabaseName = nickname
 | 
			
		||||
 | 
			
		||||
    reply.code(201).send({ message: 'Deploying.' })
 | 
			
		||||
    // TODO: Persistent volume, custom inputs
 | 
			
		||||
    const deployId = cuid()
 | 
			
		||||
    const configuration = {
 | 
			
		||||
      general: {
 | 
			
		||||
        workdir: `/tmp/${deployId}`,
 | 
			
		||||
        deployId,
 | 
			
		||||
        nickname,
 | 
			
		||||
        type
 | 
			
		||||
      },
 | 
			
		||||
      database: {
 | 
			
		||||
        usernames,
 | 
			
		||||
        passwords,
 | 
			
		||||
        defaultDatabaseName
 | 
			
		||||
      },
 | 
			
		||||
      deploy: {
 | 
			
		||||
        name: nickname
 | 
			
		||||
      reply.code(201).send({ message: 'Deploying.' })
 | 
			
		||||
      // TODO: Persistent volume, custom inputs
 | 
			
		||||
      const deployId = cuid()
 | 
			
		||||
      const configuration = {
 | 
			
		||||
        general: {
 | 
			
		||||
          workdir: `/tmp/${deployId}`,
 | 
			
		||||
          deployId,
 | 
			
		||||
          nickname,
 | 
			
		||||
          type
 | 
			
		||||
        },
 | 
			
		||||
        database: {
 | 
			
		||||
          usernames,
 | 
			
		||||
          passwords,
 | 
			
		||||
          defaultDatabaseName
 | 
			
		||||
        },
 | 
			
		||||
        deploy: {
 | 
			
		||||
          name: nickname
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    let generateEnvs = {}
 | 
			
		||||
    let image = null
 | 
			
		||||
    let volume = null
 | 
			
		||||
    if (type === 'mongodb') {
 | 
			
		||||
      generateEnvs = {
 | 
			
		||||
        MONGODB_ROOT_PASSWORD: passwords[0],
 | 
			
		||||
        MONGODB_USERNAME: usernames[0],
 | 
			
		||||
        MONGODB_PASSWORD: passwords[1],
 | 
			
		||||
        MONGODB_DATABASE: defaultDatabaseName
 | 
			
		||||
      let generateEnvs = {}
 | 
			
		||||
      let image = null
 | 
			
		||||
      let volume = null
 | 
			
		||||
      if (type === 'mongodb') {
 | 
			
		||||
        generateEnvs = {
 | 
			
		||||
          MONGODB_ROOT_PASSWORD: passwords[0],
 | 
			
		||||
          MONGODB_USERNAME: usernames[0],
 | 
			
		||||
          MONGODB_PASSWORD: passwords[1],
 | 
			
		||||
          MONGODB_DATABASE: defaultDatabaseName
 | 
			
		||||
        }
 | 
			
		||||
        image = 'bitnami/mongodb:4.4'
 | 
			
		||||
        volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb`
 | 
			
		||||
      } else if (type === 'postgresql') {
 | 
			
		||||
        generateEnvs = {
 | 
			
		||||
          POSTGRESQL_PASSWORD: passwords[0],
 | 
			
		||||
          POSTGRESQL_USERNAME: usernames[0],
 | 
			
		||||
          POSTGRESQL_DATABASE: defaultDatabaseName
 | 
			
		||||
        }
 | 
			
		||||
        image = 'bitnami/postgresql:13.2.0'
 | 
			
		||||
        volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql`
 | 
			
		||||
      } else if (type === 'couchdb') {
 | 
			
		||||
        generateEnvs = {
 | 
			
		||||
          COUCHDB_PASSWORD: passwords[0],
 | 
			
		||||
          COUCHDB_USER: usernames[0]
 | 
			
		||||
        }
 | 
			
		||||
        image = 'bitnami/couchdb:3'
 | 
			
		||||
        volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb`
 | 
			
		||||
      } else if (type === 'mysql') {
 | 
			
		||||
        generateEnvs = {
 | 
			
		||||
          MYSQL_ROOT_PASSWORD: passwords[0],
 | 
			
		||||
          MYSQL_ROOT_USER: usernames[0],
 | 
			
		||||
          MYSQL_USER: usernames[1],
 | 
			
		||||
          MYSQL_PASSWORD: passwords[1],
 | 
			
		||||
          MYSQL_DATABASE: defaultDatabaseName
 | 
			
		||||
        }
 | 
			
		||||
        image = 'bitnami/mysql:8.0'
 | 
			
		||||
        volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`
 | 
			
		||||
      }
 | 
			
		||||
      image = 'bitnami/mongodb:4.4'
 | 
			
		||||
      volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb`
 | 
			
		||||
    } else if (type === 'postgresql') {
 | 
			
		||||
      generateEnvs = {
 | 
			
		||||
        POSTGRESQL_PASSWORD: passwords[0],
 | 
			
		||||
        POSTGRESQL_USERNAME: usernames[0],
 | 
			
		||||
        POSTGRESQL_DATABASE: defaultDatabaseName
 | 
			
		||||
      }
 | 
			
		||||
      image = 'bitnami/postgresql:13.2.0'
 | 
			
		||||
      volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql`
 | 
			
		||||
    } else if (type === 'couchdb') {
 | 
			
		||||
      generateEnvs = {
 | 
			
		||||
        COUCHDB_PASSWORD: passwords[0],
 | 
			
		||||
        COUCHDB_USER: usernames[0]
 | 
			
		||||
      }
 | 
			
		||||
      image = 'bitnami/couchdb:3'
 | 
			
		||||
      volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb`
 | 
			
		||||
    } else if (type === 'mysql') {
 | 
			
		||||
      generateEnvs = {
 | 
			
		||||
        MYSQL_ROOT_PASSWORD: passwords[0],
 | 
			
		||||
        MYSQL_ROOT_USER: usernames[0],
 | 
			
		||||
        MYSQL_USER: usernames[1],
 | 
			
		||||
        MYSQL_PASSWORD: passwords[1],
 | 
			
		||||
        MYSQL_DATABASE: defaultDatabaseName
 | 
			
		||||
      }
 | 
			
		||||
      image = 'bitnami/mysql:8.0'
 | 
			
		||||
      volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const stack = {
 | 
			
		||||
      version: '3.8',
 | 
			
		||||
      services: {
 | 
			
		||||
        [configuration.general.deployId]: {
 | 
			
		||||
          image,
 | 
			
		||||
          networks: [`${docker.network}`],
 | 
			
		||||
          environment: generateEnvs,
 | 
			
		||||
          volumes: [volume],
 | 
			
		||||
          deploy: {
 | 
			
		||||
            replicas: 1,
 | 
			
		||||
            update_config: {
 | 
			
		||||
              parallelism: 0,
 | 
			
		||||
              delay: '10s',
 | 
			
		||||
              order: 'start-first'
 | 
			
		||||
            },
 | 
			
		||||
            rollback_config: {
 | 
			
		||||
              parallelism: 0,
 | 
			
		||||
              delay: '10s',
 | 
			
		||||
              order: 'start-first'
 | 
			
		||||
            },
 | 
			
		||||
            labels: [
 | 
			
		||||
              'managedBy=coolify',
 | 
			
		||||
              'type=database',
 | 
			
		||||
              'configuration=' + JSON.stringify(configuration)
 | 
			
		||||
            ]
 | 
			
		||||
      const stack = {
 | 
			
		||||
        version: '3.8',
 | 
			
		||||
        services: {
 | 
			
		||||
          [configuration.general.deployId]: {
 | 
			
		||||
            image,
 | 
			
		||||
            networks: [`${docker.network}`],
 | 
			
		||||
            environment: generateEnvs,
 | 
			
		||||
            volumes: [volume],
 | 
			
		||||
            deploy: {
 | 
			
		||||
              replicas: 1,
 | 
			
		||||
              update_config: {
 | 
			
		||||
                parallelism: 0,
 | 
			
		||||
                delay: '10s',
 | 
			
		||||
                order: 'start-first'
 | 
			
		||||
              },
 | 
			
		||||
              rollback_config: {
 | 
			
		||||
                parallelism: 0,
 | 
			
		||||
                delay: '10s',
 | 
			
		||||
                order: 'start-first'
 | 
			
		||||
              },
 | 
			
		||||
              labels: [
 | 
			
		||||
                'managedBy=coolify',
 | 
			
		||||
                'type=database',
 | 
			
		||||
                'configuration=' + JSON.stringify(configuration)
 | 
			
		||||
              ]
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        networks: {
 | 
			
		||||
          [`${docker.network}`]: {
 | 
			
		||||
            external: true
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        volumes: {
 | 
			
		||||
          [`${configuration.general.deployId}-${type}-data`]: {
 | 
			
		||||
            external: true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      networks: {
 | 
			
		||||
        [`${docker.network}`]: {
 | 
			
		||||
          external: true
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      volumes: {
 | 
			
		||||
        [`${configuration.general.deployId}-${type}-data`]: {
 | 
			
		||||
          external: true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
 | 
			
		||||
      await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
 | 
			
		||||
      await execShellAsync(
 | 
			
		||||
              `cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
 | 
			
		||||
      )
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw { error, type: 'server' }
 | 
			
		||||
    }
 | 
			
		||||
    await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
 | 
			
		||||
    await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
 | 
			
		||||
    await execShellAsync(
 | 
			
		||||
            `cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  fastify.delete('/:dbName', async (request, reply) => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								api/routes/v1/server/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								api/routes/v1/server/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
const Server = require('../../../models/Logs/Server')
 | 
			
		||||
module.exports = async function (fastify) {
 | 
			
		||||
  fastify.get('/', async (request, reply) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const serverLogs = await Server.find().select('-_id -__v')
 | 
			
		||||
      // TODO: Should do better
 | 
			
		||||
      return {
 | 
			
		||||
        serverLogs
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw { error, type: 'server' }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
@@ -25,7 +25,7 @@ module.exports = async function (fastify) {
 | 
			
		||||
        settings
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new Error(error)
 | 
			
		||||
      throw { error, type: 'server' }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +38,7 @@ module.exports = async function (fastify) {
 | 
			
		||||
      ).select('-_id -__v')
 | 
			
		||||
      reply.code(201).send({ settings })
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new Error(error)
 | 
			
		||||
      throw { error, type: 'server' }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,10 @@ const { saveServerLog } = require('../../../libs/logging')
 | 
			
		||||
 | 
			
		||||
module.exports = async function (fastify) {
 | 
			
		||||
  fastify.get('/', async (request, reply) => {
 | 
			
		||||
    const upgradeP1 = await execShellAsync('bash ./install.sh upgrade-phase-1')
 | 
			
		||||
    const upgradeP1 = await execShellAsync('bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p1.sh)"')
 | 
			
		||||
    await saveServerLog({ event: upgradeP1, type: 'UPGRADE-P-1' })
 | 
			
		||||
    reply.code(200).send('I\'m trying, okay?')
 | 
			
		||||
    const upgradeP2 = await execShellAsync('bash ./install.sh upgrade-phase-2')
 | 
			
		||||
    const upgradeP2 = await execShellAsync('docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -u root coolify bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p2.sh)"')
 | 
			
		||||
    await saveServerLog({ event: upgradeP2, type: 'UPGRADE-P-2' })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,14 +3,18 @@ const jwt = require('jsonwebtoken')
 | 
			
		||||
 | 
			
		||||
module.exports = async function (fastify) {
 | 
			
		||||
  fastify.get('/', async (request, reply) => {
 | 
			
		||||
    const { authorization } = request.headers
 | 
			
		||||
    if (!authorization) {
 | 
			
		||||
    try {
 | 
			
		||||
      const { authorization } = request.headers
 | 
			
		||||
      if (!authorization) {
 | 
			
		||||
        reply.code(401).send({})
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const token = authorization.split(' ')[1]
 | 
			
		||||
      const verify = jwt.verify(token, fastify.config.JWT_SIGN_KEY)
 | 
			
		||||
      const found = await User.findOne({ uid: verify.jti })
 | 
			
		||||
      found ? reply.code(200).send({}) : reply.code(401).send({})
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      reply.code(401).send({})
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const token = authorization.split(' ')[1]
 | 
			
		||||
    const verify = jwt.verify(token, fastify.config.JWT_SIGN_KEY)
 | 
			
		||||
    const found = await User.findOne({ uid: verify.jti })
 | 
			
		||||
    found ? reply.code(200).send({}) : reply.code(401).send({})
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
const crypto = require('crypto')
 | 
			
		||||
const { cleanupTmp, execShellAsync } = require('../../../libs/common')
 | 
			
		||||
const { cleanupTmp } = require('../../../libs/common')
 | 
			
		||||
const Deployment = require('../../../models/Deployment')
 | 
			
		||||
const { queueAndBuild } = require('../../../libs/applications')
 | 
			
		||||
const { setDefaultConfiguration } = require('../../../libs/applications/configuration')
 | 
			
		||||
const { setDefaultConfiguration, precheckDeployment } = require('../../../libs/applications/configuration')
 | 
			
		||||
const { docker } = require('../../../libs/docker')
 | 
			
		||||
const cloneRepository = require('../../../libs/applications/github/cloneRepository')
 | 
			
		||||
 | 
			
		||||
@@ -45,98 +45,55 @@ module.exports = async function (fastify) {
 | 
			
		||||
      reply.code(500).send({ error: 'Not a push event.' })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
 | 
			
		||||
 | 
			
		||||
    const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
 | 
			
		||||
 | 
			
		||||
    let configuration = services.find(r => {
 | 
			
		||||
      if (request.body.ref.startsWith('refs')) {
 | 
			
		||||
        const branch = request.body.ref.split('/')[2]
 | 
			
		||||
        if (
 | 
			
		||||
          JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
 | 
			
		||||
          JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
 | 
			
		||||
        ) {
 | 
			
		||||
          return r
 | 
			
		||||
      let configuration = services.find(r => {
 | 
			
		||||
        if (request.body.ref.startsWith('refs')) {
 | 
			
		||||
          const branch = request.body.ref.split('/')[2]
 | 
			
		||||
          if (
 | 
			
		||||
            JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
 | 
			
		||||
            JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
 | 
			
		||||
          ) {
 | 
			
		||||
            return r
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null
 | 
			
		||||
      })
 | 
			
		||||
      if (!configuration) {
 | 
			
		||||
        reply.code(500).send({ error: 'No configuration found.' })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return null
 | 
			
		||||
    })
 | 
			
		||||
      configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration))
 | 
			
		||||
      await cloneRepository(configuration)
 | 
			
		||||
      const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
 | 
			
		||||
 | 
			
		||||
    if (!configuration) {
 | 
			
		||||
      reply.code(500).send({ error: 'No configuration found.' })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration))
 | 
			
		||||
 | 
			
		||||
    await cloneRepository(configuration)
 | 
			
		||||
 | 
			
		||||
    let foundService = false
 | 
			
		||||
    let foundDomain = false
 | 
			
		||||
    let configChanged = false
 | 
			
		||||
    let imageChanged = false
 | 
			
		||||
 | 
			
		||||
    let forceUpdate = false
 | 
			
		||||
 | 
			
		||||
    for (const service of services) {
 | 
			
		||||
      const running = JSON.parse(service.Spec.Labels.configuration)
 | 
			
		||||
      if (running) {
 | 
			
		||||
        if (
 | 
			
		||||
          running.publish.domain === configuration.publish.domain &&
 | 
			
		||||
          running.repository.id !== configuration.repository.id &&
 | 
			
		||||
          running.repository.branch !== configuration.repository.branch
 | 
			
		||||
        ) {
 | 
			
		||||
          foundDomain = true
 | 
			
		||||
        }
 | 
			
		||||
        if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
 | 
			
		||||
          const state = await execShellAsync(`docker stack ps ${running.build.container.name} --format '{{ json . }}'`)
 | 
			
		||||
          const isError = state.split('\n').filter(n => n).map(s => JSON.parse(s)).filter(n => n.DesiredState !== 'Running')
 | 
			
		||||
          if (isError.length > 0) forceUpdate = true
 | 
			
		||||
          foundService = true
 | 
			
		||||
 | 
			
		||||
          const runningWithoutContainer = JSON.parse(JSON.stringify(running))
 | 
			
		||||
          delete runningWithoutContainer.build.container
 | 
			
		||||
 | 
			
		||||
          const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration))
 | 
			
		||||
          delete configurationWithoutContainer.build.container
 | 
			
		||||
 | 
			
		||||
          if (JSON.stringify(runningWithoutContainer.build) !== JSON.stringify(configurationWithoutContainer.build) || JSON.stringify(runningWithoutContainer.publish) !== JSON.stringify(configurationWithoutContainer.publish)) configChanged = true
 | 
			
		||||
          if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (foundDomain) {
 | 
			
		||||
      cleanupTmp(configuration.general.workdir)
 | 
			
		||||
      reply.code(500).send({ message: 'Domain already used.' })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (forceUpdate) {
 | 
			
		||||
      imageChanged = false
 | 
			
		||||
      configChanged = false
 | 
			
		||||
    } else {
 | 
			
		||||
      if (foundService && !imageChanged && !configChanged) {
 | 
			
		||||
      if (foundService && !forceUpdate && !imageChanged && !configChanged) {
 | 
			
		||||
        cleanupTmp(configuration.general.workdir)
 | 
			
		||||
        reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const alreadyQueued = await Deployment.find({
 | 
			
		||||
        repoId: configuration.repository.id,
 | 
			
		||||
        branch: configuration.repository.branch,
 | 
			
		||||
        organization: configuration.repository.organization,
 | 
			
		||||
        name: configuration.repository.name,
 | 
			
		||||
        domain: configuration.publish.domain,
 | 
			
		||||
        progress: { $in: ['queued', 'inprogress'] }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (alreadyQueued.length > 0) {
 | 
			
		||||
        reply.code(200).send({ message: 'Already in the queue.' })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      queueAndBuild(configuration, imageChanged)
 | 
			
		||||
 | 
			
		||||
      reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw { error, type: 'server' }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const alreadyQueued = await Deployment.find({
 | 
			
		||||
      repoId: configuration.repository.id,
 | 
			
		||||
      branch: configuration.repository.branch,
 | 
			
		||||
      organization: configuration.repository.organization,
 | 
			
		||||
      name: configuration.repository.name,
 | 
			
		||||
      domain: configuration.publish.domain,
 | 
			
		||||
      progress: { $in: ['queued', 'inprogress'] }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (alreadyQueued.length > 0) {
 | 
			
		||||
      reply.code(200).send({ message: 'Already in the queue.' })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    queueAndBuild(configuration, services, configChanged, imageChanged)
 | 
			
		||||
 | 
			
		||||
    reply.code(201).send({ message: 'Deployment queued.' })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user