Skip to content

Commit

Permalink
v1.0.6 (#30)
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
andrasbacsai authored Apr 15, 2021
1 parent 166a573 commit bad8428
Show file tree
Hide file tree
Showing 56 changed files with 895 additions and 657 deletions.
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
.routify
.routify
.pnpm-store
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ dist-ssr
yarn-error.log
api/development/console.log
.pnpm-debug.log
yarn.lock
yarn.lock
.pnpm-store
1 change: 1 addition & 0 deletions api/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = async function (fastify, opts) {
server.register(require('./routes/v1/application/deploy'), { prefix: '/application/deploy' })
server.register(require('./routes/v1/application/deploy/logs'), { prefix: '/application/deploy/logs' })
server.register(require('./routes/v1/databases'), { prefix: '/databases' })
server.register(require('./routes/v1/server'), { prefix: '/server' })
})
// Public routes
fastify.register(require('./routes/v1/verify'), { prefix: '/verify' })
Expand Down
19 changes: 19 additions & 0 deletions api/buildPacks/custom/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const fs = require('fs').promises
const { streamEvents, docker } = require('../../libs/docker')

module.exports = async function (configuration) {
try {
const path = `${configuration.general.workdir}/${configuration.build.directory ? configuration.build.directory : ''}`
if (fs.stat(`${path}/Dockerfile`)) {
const stream = await docker.engine.buildImage(
{ src: ['.'], context: path },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} else {
throw { error: 'No custom dockerfile found.', type: 'app' }
}
} catch (error) {
throw { error, type: 'server' }
}
}
16 changes: 10 additions & 6 deletions api/packs/helpers.js → api/buildPacks/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ const buildImageNodeDocker = (configuration) => {
].join('\n')
}
async function buildImage (configuration) {
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, buildImageNodeDocker(configuration))
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
try {
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, buildImageNodeDocker(configuration))
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
}
}

module.exports = {
Expand Down
3 changes: 2 additions & 1 deletion api/packs/index.js → api/buildPacks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ const static = require('./static')
const nodejs = require('./nodejs')
const php = require('./php')
const custom = require('./custom')
const rust = require('./rust')

module.exports = { static, nodejs, php, custom }
module.exports = { static, nodejs, php, custom, rust }
20 changes: 12 additions & 8 deletions api/packs/nodejs/index.js → api/buildPacks/nodejs/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const fs = require('fs').promises
const { buildImage } = require('../helpers')
const { streamEvents, docker } = require('../../libs/docker')

// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
const publishNodejsDocker = (configuration) => {
return [
'FROM node:lts',
Expand All @@ -16,11 +16,15 @@ const publishNodejsDocker = (configuration) => {
}

module.exports = async function (configuration) {
if (configuration.build.command.build) await buildImage(configuration)
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishNodejsDocker(configuration))
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
try {
if (configuration.build.command.build) await buildImage(configuration)
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishNodejsDocker(configuration))
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
}
}
26 changes: 26 additions & 0 deletions api/buildPacks/php/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const fs = require('fs').promises
const { streamEvents, docker } = require('../../libs/docker')
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishPHPDocker = (configuration) => {
return [
'FROM php:apache',
'RUN a2enmod rewrite',
'WORKDIR /usr/src/app',
`COPY .${configuration.build.directory} /var/www/html`,
'EXPOSE 80',
' CMD ["apache2-foreground"]'
].join('\n')
}

module.exports = async function (configuration) {
try {
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishPHPDocker(configuration))
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
}
}
64 changes: 64 additions & 0 deletions api/buildPacks/rust/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const fs = require('fs').promises
const { streamEvents, docker } = require('../../libs/docker')
const { execShellAsync } = require('../../libs/common')
const TOML = require('@iarna/toml')

const publishRustDocker = (configuration, custom) => {
return [
'FROM rust:latest',
'WORKDIR /app',
`COPY --from=${configuration.build.container.name}:cache /app/target target`,
`COPY --from=${configuration.build.container.name}:cache /usr/local/cargo /usr/local/cargo`,
'COPY . .',
`RUN cargo build --release --bin ${custom.name}`,
'FROM debian:buster-slim',
'WORKDIR /app',
'RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*',
'RUN update-ca-certificates',
`COPY --from=${configuration.build.container.name}:cache /app/target/release/${custom.name} ${custom.name}`,
`EXPOSE ${configuration.publish.port}`,
`CMD ["/app/${custom.name}"]`
].join('\n')
}

const cacheRustDocker = (configuration, custom) => {
return [
`FROM rust:latest AS planner-${configuration.build.container.name}`,
'WORKDIR /app',
'RUN cargo install cargo-chef',
'COPY . .',
'RUN cargo chef prepare --recipe-path recipe.json',
'FROM rust:latest',
'WORKDIR /app',
'RUN cargo install cargo-chef',
`COPY --from=planner-${configuration.build.container.name} /app/recipe.json recipe.json`,
'RUN cargo chef cook --release --recipe-path recipe.json'
].join('\n')
}

module.exports = async function (configuration) {
try {
const cargoToml = await execShellAsync(`cat ${configuration.general.workdir}/Cargo.toml`)
const parsedToml = TOML.parse(cargoToml)
const custom = {
name: parsedToml.package.name
}
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, cacheRustDocker(configuration, custom))

let stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:cache` }
)
await streamEvents(stream, configuration)

await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishRustDocker(configuration, custom))

stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
}
}
19 changes: 12 additions & 7 deletions api/packs/static/index.js → api/buildPacks/static/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const fs = require('fs').promises
const { buildImage } = require('../helpers')
const { streamEvents, docker } = require('../../libs/docker')

// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
Expand All @@ -16,12 +17,16 @@ const publishStaticDocker = (configuration) => {
}

module.exports = async function (configuration) {
if (configuration.build.command.build) await buildImage(configuration)
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration))
try {
if (configuration.build.command.build) await buildImage(configuration)
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration))

const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
}
}
13 changes: 9 additions & 4 deletions api/libs/applications/build/container.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const packs = require('../../../packs')
const packs = require('../../../buildPacks')
const { saveAppLog } = require('../../logging')
const Deployment = require('../../../models/Deployment')

Expand Down Expand Up @@ -26,9 +26,14 @@ module.exports = async function (configuration) {
throw { error, type: 'app' }
}
} else {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
try {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
} catch (error) {
// Hmm.
}

throw { error: 'No buildpack found.', type: 'app' }
}
}
11 changes: 5 additions & 6 deletions api/libs/applications/cleanup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ const { docker } = require('../../docker')
const { execShellAsync } = require('../../common')
const Deployment = require('../../../models/Deployment')

async function purgeOldThings () {
async function purgeImagesContainers () {
try {
// TODO: Tweak this, because it deletes coolify-base, so the upgrade will be slow
await docker.engine.pruneImages()
await docker.engine.pruneContainers()
await execShellAsync('docker container prune -f')
await execShellAsync('docker image prune -f --filter=label!=coolify-reserve=true')
} catch (error) {
throw { error, type: 'server' }
}
}

async function cleanup (configuration) {
async function cleanupStuckedDeploymentsInDB (configuration) {
const { id } = configuration.repository
const deployId = configuration.general.deployId
try {
Expand All @@ -39,4 +38,4 @@ async function deleteSameDeployments (configuration) {
}
}

module.exports = { cleanup, deleteSameDeployments, purgeOldThings }
module.exports = { cleanupStuckedDeploymentsInDB, deleteSameDeployments, purgeImagesContainers }
67 changes: 62 additions & 5 deletions api/libs/applications/configuration.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
const cuid = require('cuid')
const crypto = require('crypto')

const { docker } = require('../docker')
const { execShellAsync } = require('../common')

function getUniq () {
Expand Down Expand Up @@ -30,7 +30,8 @@ function setDefaultConfiguration (configuration) {
rollback_config: {
parallelism: 1,
delay: '10s',
order: 'start-first'
order: 'start-first',
failure_action: 'rollback'
}
}

Expand All @@ -48,11 +49,18 @@ function setDefaultConfiguration (configuration) {
configuration.publish.port = 80
} else if (configuration.build.pack === 'nodejs') {
configuration.publish.port = 3000
} else if (configuration.build.pack === 'rust') {
configuration.publish.port = 3000
}
}

if (!configuration.build.directory) {
configuration.build.directory = '/'
}
if (!configuration.publish.directory) {
configuration.publish.directory = '/'
}

if (configuration.build.pack === 'static' || configuration.build.pack === 'nodejs') {
if (!configuration.build.command.installation) configuration.build.command.installation = 'yarn install'
}
Expand All @@ -66,8 +74,9 @@ function setDefaultConfiguration (configuration) {
}
}

async function updateServiceLabels (configuration, services) {
async function updateServiceLabels (configuration) {
// In case of any failure during deployment, still update the current configuration.
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
const found = services.find(s => {
const config = JSON.parse(s.Spec.Labels.configuration)
if (config.repository.id === configuration.repository.id && config.repository.branch === configuration.repository.branch) {
Expand All @@ -79,10 +88,58 @@ async function updateServiceLabels (configuration, services) {
const { ID } = found
try {
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration }
execShellAsync(`docker service update --label-add configuration='${JSON.stringify(Labels)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${configuration.build.container.tag}' ${ID}`)
await execShellAsync(`docker service update --label-add configuration='${JSON.stringify(Labels)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${configuration.build.container.tag}' ${ID}`)
} catch (error) {
console.log(error)
}
}
}
module.exports = { setDefaultConfiguration, updateServiceLabels }

async function precheckDeployment ({ services, configuration }) {
let foundService = 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.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) {
forceUpdate = true
}
// If the deployment is in error state, forceUpdate
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' && n.Image.split(':')[1] === running.build.container.tag)
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 (forceUpdate) {
imageChanged = false
configChanged = false
}
return {
foundService,
imageChanged,
configChanged,
forceUpdate
}
}
module.exports = { setDefaultConfiguration, updateServiceLabels, precheckDeployment }
Loading

0 comments on commit bad8428

Please sign in to comment.