Compare commits

...

2 Commits

Author SHA1 Message Date
Andras Bacsai
adcd68c1ab v1.0.13 (#46) 2021-05-16 21:54:44 +02:00
Andras Bacsai
23a4ebb74a v1.0.12 - Sveltekit migration (#44)
Changed the whole tech stack to SvelteKit which means:
- Typescript 
- SSR
- No fastify :(
- Beta, but it's fine!

Other changes:
- Tailwind -> Tailwind JIT
- A lot more
2021-05-14 21:51:14 +02:00
232 changed files with 9561 additions and 11841 deletions

View File

@@ -1,4 +1,7 @@
.DS_Store
node_modules node_modules
dist /.svelte
.routify /build
/functions
.pnpm-store .pnpm-store
.pnpm-debug.log

20
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,20 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2019
},
env: {
browser: true,
es2017: true,
node: true
}
};

16
.gitignore vendored
View File

@@ -1,11 +1,9 @@
.vscode /node_modules
.idea /.svelte
node_modules /.svelte-kit
dist /.pnpm-store
dist-ssr /build
.routify /functions
.env .env
yarn-error.log .DS_Store
api/development/console.log
.pnpm-debug.log .pnpm-debug.log
.pnpm-store

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

5
.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
.svelte/**
static/**
build/**
node_modules/**
.svelte-kit/**

View File

@@ -1,14 +1,6 @@
{ {
"arrowParens": "avoid", "useTabs": true,
"bracketSpacing": true, "singleQuote": true,
"printWidth": 80, "trailingComma": "none",
"semi": true, "printWidth": 100
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"svelteSortOrder" : "styles-scripts-markup",
"svelteStrictMode": true,
"svelteBracketNewLine": true,
"svelteAllowShorthand": true,
"plugins": ["prettier-plugin-svelte"]
} }

View File

@@ -1,4 +1,3 @@
# Coolify # Coolify
An open-source, hassle-free, self-hostable Heroku & Netlify alternative. An open-source, hassle-free, self-hostable Heroku & Netlify alternative.
@@ -7,7 +6,6 @@ An open-source, hassle-free, self-hostable Heroku & Netlify alternative.
[Small video](https://cdn.coollabs.io/assets/coolify/video/coolify.webm) [Small video](https://cdn.coollabs.io/assets/coolify/video/coolify.webm)
## Installation ## Installation
Installation is automated with the following command: Installation is automated with the following command:
@@ -16,19 +14,21 @@ Installation is automated with the following command:
/bin/bash -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)" /bin/bash -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"
``` ```
## Features ## Features
You can deploy any of the following applications, databases and services easily. You can deploy any of the following applications, databases and services easily.
(constantly growing lists) (constantly growing lists)
### Applications ### Applications
With Github integration With Github integration
- Static sites - Static sites
- NodeJS - NodeJS
- VueJS - VueJS
- NuxtJS - NuxtJS
- NextJS
- React/Preact - React/Preact
- NextJS - NextJS
- Gatsby - Gatsby
@@ -38,14 +38,16 @@ With Github integration
- or any custom dockerfile - or any custom dockerfile
### Databases ### Databases
- MongoDB - MongoDB
- MySQL - MySQL
- PostgreSQL - PostgreSQL
- CouchDB - CouchDB
- Redis
### Services ### Services
- [Plausible Analytics](https://plausible.io)
- [Plausible Analytics](https://plausible.io)
## Support ## Support
@@ -55,10 +57,9 @@ With Github integration
- Discord: [Invitation](https://discord.com/invite/bvS3WhR) - Discord: [Invitation](https://discord.com/invite/bvS3WhR)
## Roadmap ## Roadmap
[See the Roadmap here](https://github.com/coollabsio/coolify/projects/1) [See the Roadmap here](https://github.com/coollabsio/coolify/projects/1)
## License ## License
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Please see the [LICENSE](/LICENSE) file in our repository for the full text. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Please see the [LICENSE](/LICENSE) file in our repository for the full text.

View File

@@ -1,30 +0,0 @@
module.exports = async function (fastify, opts) {
// Private routes
fastify.register(async function (server) {
server.register(require('./plugins/authentication'))
server.register(require('./routes/v1/upgrade'), { prefix: '/upgrade' })
server.register(require('./routes/v1/settings'), { prefix: '/settings' })
server.register(require('./routes/v1/dashboard'), { prefix: '/dashboard' })
server.register(require('./routes/v1/config'), { prefix: '/config' })
server.register(require('./routes/v1/application/remove'), { prefix: '/application/remove' })
server.register(require('./routes/v1/application/logs'), { prefix: '/application/logs' })
server.register(require('./routes/v1/application/check'), { prefix: '/application/check' })
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/services'), { prefix: '/services' })
server.register(require('./routes/v1/services/deploy'), { prefix: '/services/deploy' })
server.register(require('./routes/v1/server'), { prefix: '/server' })
})
// Public routes
fastify.register(require('./routes/v1/verify'), { prefix: '/verify' })
fastify.register(require('./routes/v1/login/github'), {
prefix: '/login/github'
})
fastify.register(require('./routes/v1/webhooks/deploy'), {
prefix: '/webhooks/deploy'
})
fastify.register(require('./routes/v1/undead'), {
prefix: '/undead'
})
}

View File

@@ -1,15 +0,0 @@
const fs = require('fs').promises
const { streamEvents, docker } = require('../../libs/docker')
module.exports = async function (configuration) {
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 new Error('No custom dockerfile found.')
}
}

View File

@@ -1,25 +0,0 @@
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',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n')
}
module.exports = async function (configuration) {
await buildImage(configuration, true)
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)
}

View File

@@ -1,24 +0,0 @@
const fs = require('fs').promises
const { streamEvents, docker } = require('../libs/docker')
const buildImageNodeDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
`COPY ${configuration.build.directory}/package*.json ./`,
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
`COPY ./${configuration.build.directory} ./`,
`RUN ${configuration.build.command.build}`
].join('\n')
}
async function buildImage (configuration, cacheBuild) {
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}:${cacheBuild ? `${configuration.build.container.tag}-cache` : configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
}
module.exports = {
buildImage
}

View File

@@ -1,13 +0,0 @@
const Static = require('./static')
const react = require('./react')
const nextjs = require('./nextjs')
const nuxtjs = require('./nuxtjs')
const gatsby = require('./gatsby')
const vuejs = require('./vuejs')
const svelte = require('./svelte')
const nodejs = require('./nodejs')
const php = require('./php')
const docker = require('./docker')
const rust = require('./rust')
module.exports = { static: Static, nodejs, php, docker, rust, react, vuejs, nextjs, nuxtjs, svelte, gatsby }

View File

@@ -1,28 +0,0 @@
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',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
].join('\n')
}
module.exports = async function (configuration) {
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)
}

View File

@@ -1,28 +0,0 @@
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',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
].join('\n')
}
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)
}

View File

@@ -1,28 +0,0 @@
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',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
].join('\n')
}
module.exports = async function (configuration) {
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)
}

View File

@@ -1,22 +0,0 @@
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) {
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)
}

View File

@@ -1,25 +0,0 @@
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',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n')
}
module.exports = async function (configuration) {
await buildImage(configuration, true)
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)
}

View File

@@ -1,60 +0,0 @@
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) {
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)
}

View File

@@ -1,27 +0,0 @@
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',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`
: `COPY ./${configuration.build.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n')
}
module.exports = async function (configuration) {
if (configuration.build.command.build) await buildImage(configuration, true)
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)
}

View File

@@ -1,25 +0,0 @@
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',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n')
}
module.exports = async function (configuration) {
await buildImage(configuration, true)
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)
}

View File

@@ -1,25 +0,0 @@
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',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n')
}
module.exports = async function (configuration) {
await buildImage(configuration, true)
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)
}

View File

@@ -1,36 +0,0 @@
const mongoose = require('mongoose')
const { MongoMemoryServer } = require('mongodb-memory-server-core')
const mongoServer = new MongoMemoryServer({
instance: {
port: 27017,
dbName: 'coolify',
storageEngine: 'wiredTiger'
},
binary: {
version: '4.4.3'
}
})
mongoose.Promise = Promise
mongoServer.getUri().then((mongoUri) => {
const mongooseOpts = {
useNewUrlParser: true,
useUnifiedTopology: true
}
mongoose.connect(mongoUri, mongooseOpts)
mongoose.connection.on('error', (e) => {
if (e.message.code === 'ETIMEDOUT') {
console.log(e)
mongoose.connect(mongoUri, mongooseOpts)
}
console.log(e)
})
mongoose.connection.once('open', () => {
console.log(`Started in-memory mongodb ${mongoUri}`)
})
})

View File

@@ -1,28 +0,0 @@
const packs = require('../../../buildPacks')
const { saveAppLog } = require('../../logging')
const Deployment = require('../../../models/Deployment')
module.exports = async function (configuration) {
const { id, organization, name, branch } = configuration.repository
const { domain } = configuration.publish
const deployId = configuration.general.deployId
const execute = packs[configuration.build.pack]
if (execute) {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'inprogress' })
await saveAppLog('### Building application.', configuration)
await execute(configuration)
await saveAppLog('### Building done.', configuration)
} else {
try {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
} catch (error) {
// Hmm.
}
throw new Error('No buildpack found.')
}
}

View File

@@ -1,35 +0,0 @@
const { docker } = require('../../docker')
const { execShellAsync } = require('../../common')
const Deployment = require('../../../models/Deployment')
async function purgeImagesContainers (configuration, deleteAll = false) {
const { name, tag } = configuration.build.container
await execShellAsync('docker container prune -f')
if (deleteAll) {
const IDsToDelete = (await execShellAsync(`docker images ls --filter=reference='${name}' --format '{{json .ID }}'`)).trim().replace(/"/g, '').split('\n')
if (IDsToDelete.length > 0) await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`)
} else {
const IDsToDelete = (await execShellAsync(`docker images ls --filter=reference='${name}' --filter=before='${name}:${tag}' --format '{{json .ID }}'`)).trim().replace(/"/g, '').split('\n')
if (IDsToDelete.length > 1) await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`)
}
await execShellAsync('docker image prune -f')
}
async function cleanupStuckedDeploymentsInDB () {
// Cleanup stucked deployments.
await Deployment.updateMany(
{ progress: { $in: ['queued', 'inprogress'] } },
{ progress: 'failed' }
)
}
async function deleteSameDeployments (configuration) {
await (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application').map(async s => {
const running = JSON.parse(s.Spec.Labels.configuration)
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`)
}
})
}
module.exports = { cleanupStuckedDeploymentsInDB, deleteSameDeployments, purgeImagesContainers }

View File

@@ -1,113 +0,0 @@
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
const cuid = require('cuid')
const crypto = require('crypto')
const { docker } = require('../docker')
const { execShellAsync, baseServiceConfiguration } = require('../common')
function getUniq () {
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 })
}
function setDefaultConfiguration (configuration) {
const nickname = getUniq()
const deployId = cuid()
const shaBase = JSON.stringify({ repository: configuration.repository })
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex')
configuration.build.container.name = sha256.slice(0, 15)
configuration.general.nickname = nickname
configuration.general.deployId = deployId
configuration.general.workdir = `/tmp/${deployId}`
if (!configuration.publish.path) configuration.publish.path = '/'
if (!configuration.publish.port) {
if (configuration.build.pack === 'nodejs' && configuration.build.pack === 'vuejs' && configuration.build.pack === 'nuxtjs' && configuration.build.pack === 'rust' && configuration.build.pack === 'nextjs') {
configuration.publish.port = 3000
} else {
configuration.publish.port = 80
}
}
if (!configuration.build.directory) configuration.build.directory = ''
if (configuration.build.directory.startsWith('/')) configuration.build.directory = configuration.build.directory.replace('/', '')
if (!configuration.publish.directory) configuration.publish.directory = ''
if (configuration.publish.directory.startsWith('/')) configuration.publish.directory = configuration.publish.directory.replace('/', '')
if (configuration.build.pack === 'static' || configuration.build.pack === 'nodejs') {
if (!configuration.build.command.installation) configuration.build.command.installation = 'yarn install'
}
configuration.build.container.baseSHA = crypto.createHash('sha256').update(JSON.stringify(baseServiceConfiguration)).digest('hex')
configuration.baseServiceConfiguration = baseServiceConfiguration
return configuration
}
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) {
return config
}
return null
})
if (found) {
const { ID } = found
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration }
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}`)
}
}
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, baseServiceConfiguration }

View File

@@ -1,65 +0,0 @@
const fs = require('fs').promises
module.exports = async function (configuration) {
const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby']
try {
// TODO: Write full .dockerignore for all deployments!!
if (configuration.build.pack === 'php') {
await fs.writeFile(`${configuration.general.workdir}/.htaccess`, `
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]
`)
}
// await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules')
if (staticDeployments.includes(configuration.build.pack)) {
await fs.writeFile(
`${configuration.general.workdir}/nginx.conf`,
`user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
access_log off;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/index.html $uri/ /index.html =404;
}
error_page 404 /50x.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
`
)
}
} catch (error) {
throw new Error(error)
}
}

View File

@@ -1,76 +0,0 @@
const yaml = require('js-yaml')
const fs = require('fs').promises
const { execShellAsync } = require('../../common')
const { docker } = require('../../docker')
const { saveAppLog } = require('../../logging')
const { deleteSameDeployments } = require('../cleanup')
module.exports = async function (configuration, imageChanged) {
const generateEnvs = {}
for (const secret of configuration.publish.secrets) {
generateEnvs[secret.name] = secret.value
}
const containerName = configuration.build.container.name
// Only save SHA256 of it in the configuration label
const baseServiceConfiguration = configuration.baseServiceConfiguration
delete configuration.baseServiceConfiguration
const stack = {
version: '3.8',
services: {
[containerName]: {
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
networks: [`${docker.network}`],
environment: generateEnvs,
deploy: {
...baseServiceConfiguration,
labels: [
'managedBy=coolify',
'type=application',
'configuration=' + JSON.stringify(configuration),
'traefik.enable=true',
'traefik.http.services.' +
configuration.build.container.name +
`.loadbalancer.server.port=${configuration.publish.port}`,
'traefik.http.routers.' +
configuration.build.container.name +
'.entrypoints=websecure',
'traefik.http.routers.' +
configuration.build.container.name +
'.rule=Host(`' +
configuration.publish.domain +
'`) && PathPrefix(`' +
configuration.publish.path +
'`)',
'traefik.http.routers.' +
configuration.build.container.name +
'.tls.certresolver=letsencrypt',
'traefik.http.routers.' +
configuration.build.container.name +
'.middlewares=global-compress'
]
}
}
},
networks: {
[`${docker.network}`]: {
external: true
}
}
}
await saveAppLog('### Publishing.', configuration)
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
if (imageChanged) {
// console.log('image changed')
await execShellAsync(`docker service update --image ${configuration.build.container.name}:${configuration.build.container.tag} ${configuration.build.container.name}_${configuration.build.container.name}`)
} else {
// console.log('new deployment or force deployment or config changed')
await deleteSameDeployments(configuration)
await execShellAsync(
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}`
)
}
await saveAppLog('### Published done!', configuration)
}

View File

@@ -1,44 +0,0 @@
const jwt = require('jsonwebtoken')
const axios = require('axios')
const { execShellAsync } = require('../../common')
module.exports = async function (configuration) {
try {
const { workdir } = configuration.general
const { organization, name, branch } = configuration.repository
const github = configuration.github
if (!github.installation.id || !github.app.id) {
throw new Error('Github installation ID is invalid.')
}
const githubPrivateKey = process.env.GITHUB_APP_PRIVATE_KEY.replace(/\\n/g, '\n').replace(/"/g, '')
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: parseInt(github.app.id)
}
const jwtToken = jwt.sign(payload, githubPrivateKey, {
algorithm: 'RS256'
})
const accessToken = await axios({
method: 'POST',
url: `https://api.github.com/app/installations/${github.installation.id}/access_tokens`,
data: {},
headers: {
Authorization: 'Bearer ' + jwtToken,
Accept: 'application/vnd.github.machine-man-preview+json'
}
})
await execShellAsync(
`mkdir -p ${workdir} && git clone -q -b ${branch} https://x-access-token:${accessToken.data.token}@github.com/${organization}/${name}.git ${workdir}/`
)
configuration.build.container.tag = (
await execShellAsync(`cd ${configuration.general.workdir}/ && git rev-parse HEAD`)
)
.replace('\n', '')
.slice(0, 7)
} catch (error) {
throw new Error(error)
}
}

View File

@@ -1,27 +0,0 @@
const dayjs = require('dayjs')
const { saveAppLog } = require('../logging')
const copyFiles = require('./deploy/copyFiles')
const buildContainer = require('./build/container')
const deploy = require('./deploy/deploy')
const Deployment = require('../../models/Deployment')
const { updateServiceLabels } = require('./configuration')
async function queueAndBuild (configuration, imageChanged) {
const { id, organization, name, branch } = configuration.repository
const { domain } = configuration.publish
const { deployId, nickname } = configuration.general
await new Deployment({
repoId: id, branch, deployId, domain, organization, name, nickname
}).save()
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration)
await copyFiles(configuration)
await buildContainer(configuration)
await deploy(configuration, imageChanged)
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' })
await updateServiceLabels(configuration)
}
module.exports = { queueAndBuild }

View File

@@ -1,117 +0,0 @@
const crypto = require('crypto')
const shell = require('shelljs')
const jsonwebtoken = require('jsonwebtoken')
const { docker } = require('./docker')
const User = require('../models/User')
const algorithm = 'aes-256-cbc'
const key = process.env.SECRETS_ENCRYPTION_KEY
const baseServiceConfiguration = {
replicas: 1,
restart_policy: {
condition: 'any',
max_attempts: 6
},
update_config: {
parallelism: 1,
delay: '10s',
order: 'start-first'
},
rollback_config: {
parallelism: 1,
delay: '10s',
order: 'start-first',
failure_action: 'rollback'
}
}
function delay (t) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve('OK')
}, t)
})
}
async function verifyUserId (authorization) {
try {
const token = authorization.split(' ')[1]
const verify = jsonwebtoken.verify(token, process.env.JWT_SIGN_KEY)
const found = await User.findOne({ uid: verify.jti })
if (found) {
return true
} else {
return false
}
} catch (error) {
return false
}
}
function execShellAsync (cmd, opts = {}) {
try {
return new Promise(function (resolve, reject) {
shell.config.silent = true
shell.exec(cmd, opts, function (code, stdout, stderr) {
if (code !== 0) return reject(new Error(stderr))
return resolve(stdout)
})
})
} catch (error) {
return new Error('Oops')
}
}
function cleanupTmp (dir) {
if (dir !== '/') shell.rm('-fr', dir)
}
async function checkImageAvailable (name) {
let cacheAvailable = false
try {
await docker.engine.getImage(name).get()
cacheAvailable = true
} catch (e) {
// Cache image not found
}
return cacheAvailable
}
function encryptData (text) {
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv)
let encrypted = cipher.update(text)
encrypted = Buffer.concat([encrypted, cipher.final()])
return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') }
}
function decryptData (text) {
const iv = Buffer.from(text.iv, 'hex')
const encryptedText = Buffer.from(text.encryptedData, 'hex')
const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv)
let decrypted = decipher.update(encryptedText)
decrypted = Buffer.concat([decrypted, decipher.final()])
return decrypted.toString()
}
function createToken (payload) {
const { uuid } = payload
return jsonwebtoken.sign({}, process.env.JWT_SIGN_KEY, {
expiresIn: 15778800,
algorithm: 'HS256',
audience: 'coolify',
issuer: 'coolify',
jwtid: uuid,
subject: `User:${uuid}`,
notBefore: -1000
})
}
module.exports = {
delay,
createToken,
execShellAsync,
cleanupTmp,
checkImageAvailable,
encryptData,
decryptData,
verifyUserId,
baseServiceConfiguration
}

View File

@@ -1,28 +0,0 @@
const Dockerode = require('dockerode')
const { saveAppLog } = require('./logging')
const docker = {
engine: new Dockerode({
socketPath: process.env.DOCKER_ENGINE
}),
network: process.env.DOCKER_NETWORK
}
async function streamEvents (stream, configuration) {
await new Promise((resolve, reject) => {
docker.engine.modem.followProgress(stream, onFinished, onProgress)
function onFinished (err, res) {
if (err) reject(err)
resolve(res)
}
function onProgress (event) {
if (event.error) {
saveAppLog(event.error, configuration, true)
reject(event.error)
} else if (event.stream) {
saveAppLog(event.stream, configuration)
}
}
})
}
module.exports = { streamEvents, docker }

View File

@@ -1,75 +0,0 @@
/* eslint-disable */
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleErrors = exports.handleValidationError = exports.handleNotFoundError = void 0;
const http_errors_enhanced_1 = require("http-errors-enhanced");
const interfaces_1 = require("./interfaces");
const utils_1 = require("./utils");
const validation_1 = require("./validation");
function handleNotFoundError(request, reply) {
handleErrors(new http_errors_enhanced_1.NotFoundError('Not found.'), request, reply);
}
exports.handleNotFoundError = handleNotFoundError;
function handleValidationError(error, request) {
/*
As seen in https://github.com/fastify/fastify/blob/master/lib/validation.js
the error.message will always start with the relative section (params, querystring, headers, body)
and fastify throws on first failing section.
*/
const section = error.message.match(/^\w+/)[0];
return new http_errors_enhanced_1.BadRequestError('One or more validations failed trying to process your request.', {
failedValidations: validation_1.convertValidationErrors(section, Reflect.get(request, section), error.validation)
});
}
exports.handleValidationError = handleValidationError;
function handleErrors(error, request, reply) {
var _a, _b;
// It is a generic error, handle it
const code = error.code;
if (!('statusCode' in error)) {
if ('validation' in error && ((_a = request[interfaces_1.kHttpErrorsEnhancedConfiguration]) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
// If it is a validation error, convert errors to human friendly format
error = handleValidationError(error, request);
}
else if ((_b = request[interfaces_1.kHttpErrorsEnhancedConfiguration]) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
// It is requested to hide the error, just log it and then create a generic one
request.log.error({ error: http_errors_enhanced_1.serializeError(error) });
error = new http_errors_enhanced_1.InternalServerError('An error occurred trying to process your request.');
}
else {
// Wrap in a HttpError, making the stack explicitily available
error = new http_errors_enhanced_1.InternalServerError(http_errors_enhanced_1.serializeError(error));
Object.defineProperty(error, 'stack', { enumerable: true });
}
}
else if (code === 'INVALID_CONTENT_TYPE' || code === 'FST_ERR_CTP_INVALID_MEDIA_TYPE') {
error = new http_errors_enhanced_1.UnsupportedMediaTypeError(utils_1.upperFirst(validation_1.validationMessagesFormatters.contentType()));
}
else if (code === 'FST_ERR_CTP_EMPTY_JSON_BODY') {
error = new http_errors_enhanced_1.BadRequestError(utils_1.upperFirst(validation_1.validationMessagesFormatters.jsonEmpty()));
}
else if (code === 'MALFORMED_JSON' || error.message === 'Invalid JSON' || error.stack.includes('at JSON.parse')) {
error = new http_errors_enhanced_1.BadRequestError(utils_1.upperFirst(validation_1.validationMessagesFormatters.json()));
}
// Get the status code
let { statusCode, headers } = error;
// Code outside HTTP range
if (statusCode < 100 || statusCode > 599) {
statusCode = http_errors_enhanced_1.INTERNAL_SERVER_ERROR;
}
// Create the body
const body = {
statusCode,
error: http_errors_enhanced_1.messagesByCodes[statusCode],
message: error.message
};
http_errors_enhanced_1.addAdditionalProperties(body, error);
// Send the error back
// eslint-disable-next-line @typescript-eslint/no-floating-promises
reply
.code(statusCode)
.headers(headers !== null && headers !== void 0 ? headers : {})
.type('application/json')
.send(body);
}
exports.handleErrors = handleErrors;

View File

@@ -1,58 +0,0 @@
/* eslint-disable */
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugin = exports.validationMessagesFormatters = exports.niceJoin = exports.convertValidationErrors = void 0;
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
const handlers_1 = require("./handlers");
const interfaces_1 = require("./interfaces");
const validation_1 = require("./validation");
__exportStar(require("./handlers"), exports);
__exportStar(require("./interfaces"), exports);
var validation_2 = require("./validation");
Object.defineProperty(exports, "convertValidationErrors", { enumerable: true, get: function () { return validation_2.convertValidationErrors; } });
Object.defineProperty(exports, "niceJoin", { enumerable: true, get: function () { return validation_2.niceJoin; } });
Object.defineProperty(exports, "validationMessagesFormatters", { enumerable: true, get: function () { return validation_2.validationMessagesFormatters; } });
exports.plugin = fastify_plugin_1.default(function (instance, options, done) {
var _a, _b, _c, _d;
const isProduction = process.env.NODE_ENV === 'production';
const convertResponsesValidationErrors = (_a = options.convertResponsesValidationErrors) !== null && _a !== void 0 ? _a : !isProduction;
const configuration = {
hideUnhandledErrors: (_b = options.hideUnhandledErrors) !== null && _b !== void 0 ? _b : isProduction,
convertValidationErrors: (_c = options.convertValidationErrors) !== null && _c !== void 0 ? _c : true,
responseValidatorCustomizer: options.responseValidatorCustomizer,
allowUndeclaredResponses: (_d = options.allowUndeclaredResponses) !== null && _d !== void 0 ? _d : false
};
instance.decorate(interfaces_1.kHttpErrorsEnhancedConfiguration, null);
instance.decorateRequest(interfaces_1.kHttpErrorsEnhancedConfiguration, null);
instance.addHook('onRequest', async (request) => {
request[interfaces_1.kHttpErrorsEnhancedConfiguration] = configuration;
});
instance.setErrorHandler(handlers_1.handleErrors);
// instance.setNotFoundHandler(handlers_1.handleNotFoundError);
if (convertResponsesValidationErrors) {
instance.decorate(interfaces_1.kHttpErrorsEnhancedResponseValidations, []);
instance.addHook('onRoute', validation_1.addResponseValidation);
instance.addHook('onReady', validation_1.compileResponseValidationSchema.bind(instance, configuration));
}
done();
}, { name: 'fastify-http-errors-enhanced' });
exports.default = exports.plugin;
// Fix CommonJS exporting
/* istanbul ignore else */
if (typeof module !== 'undefined') {
module.exports = exports.plugin;
Object.assign(module.exports, exports);
}

View File

@@ -1,6 +0,0 @@
/* eslint-disable */
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.kHttpErrorsEnhancedResponseValidations = exports.kHttpErrorsEnhancedConfiguration = void 0;
exports.kHttpErrorsEnhancedConfiguration = Symbol('fastify-http-errors-enhanced-configuration');
exports.kHttpErrorsEnhancedResponseValidations = Symbol('fastify-http-errors-enhanced-response-validation');

View File

@@ -1,31 +0,0 @@
/* eslint-disable */
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.get = exports.upperFirst = void 0;
function upperFirst(source) {
if (typeof source !== 'string' || !source.length) {
return source;
}
return source[0].toUpperCase() + source.substring(1);
}
exports.upperFirst = upperFirst;
function get(target, path) {
var _a;
const tokens = path.split('.').map((t) => t.trim());
for (const token of tokens) {
if (typeof target === 'undefined' || target === null) {
// We're supposed to be still iterating, but the chain is over - Return undefined
target = undefined;
break;
}
const index = token.match(/^(\d+)|(?:\[(\d+)\])$/);
if (index) {
target = target[parseInt((_a = index[1]) !== null && _a !== void 0 ? _a : index[2], 10)];
}
else {
target = target[token];
}
}
return target;
}
exports.get = get;

View File

@@ -1,239 +0,0 @@
/* eslint-disable */
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileResponseValidationSchema = exports.addResponseValidation = exports.convertValidationErrors = exports.validationMessagesFormatters = exports.niceJoin = void 0;
const ajv_1 = __importDefault(require("ajv"));
const http_errors_enhanced_1 = require("http-errors-enhanced");
const interfaces_1 = require("./interfaces");
const utils_1 = require("./utils");
function niceJoin(array, lastSeparator = ' and ', separator = ', ') {
switch (array.length) {
case 0:
return '';
case 1:
return array[0];
case 2:
return array.join(lastSeparator);
default:
return array.slice(0, array.length - 1).join(separator) + lastSeparator + array[array.length - 1];
}
}
exports.niceJoin = niceJoin;
exports.validationMessagesFormatters = {
contentType: () => 'only JSON payloads are accepted. Please set the "Content-Type" header to start with "application/json"',
json: () => 'the body payload is not a valid JSON',
jsonEmpty: () => 'the JSON body payload cannot be empty if the "Content-Type" header is set',
missing: () => 'must be present',
unknown: () => 'is not a valid property',
uuid: () => 'must be a valid GUID (UUID v4)',
timestamp: () => 'must be a valid ISO 8601 / RFC 3339 timestamp (example: 2018-07-06T12:34:56Z)',
date: () => 'must be a valid ISO 8601 / RFC 3339 date (example: 2018-07-06)',
time: () => 'must be a valid ISO 8601 / RFC 3339 time (example: 12:34:56)',
uri: () => 'must be a valid URI',
hostname: () => 'must be a valid hostname',
ipv4: () => 'must be a valid IPv4',
ipv6: () => 'must be a valid IPv6',
paramType: (type) => {
switch (type) {
case 'integer':
return 'must be a valid integer number';
case 'number':
return 'must be a valid number';
case 'boolean':
return 'must be a valid boolean (true or false)';
case 'object':
return 'must be a object';
case 'array':
return 'must be an array';
default:
return 'must be a string';
}
},
presentString: () => 'must be a non empty string',
minimum: (min) => `must be a number greater than or equal to ${min}`,
maximum: (max) => `must be a number less than or equal to ${max}`,
minimumProperties(min) {
return min === 1 ? 'cannot be a empty object' : `must be a object with at least ${min} properties`;
},
maximumProperties(max) {
return max === 0 ? 'must be a empty object' : `must be a object with at most ${max} properties`;
},
minimumItems(min) {
return min === 1 ? 'cannot be a empty array' : `must be an array with at least ${min} items`;
},
maximumItems(max) {
return max === 0 ? 'must be a empty array' : `must be an array with at most ${max} items`;
},
enum: (values) => `must be one of the following values: ${niceJoin(values.map((f) => `"${f}"`), ' or ')}`,
pattern: (pattern) => `must match pattern "${pattern.replace(/\(\?:/g, '(')}"`,
invalidResponseCode: (code) => `This endpoint cannot respond with HTTP status ${code}.`,
invalidResponse: (code) => `The response returned from the endpoint violates its specification for the HTTP status ${code}.`,
invalidFormat: (format) => `must match format "${format}" (format)`
};
function convertValidationErrors(section, data, validationErrors) {
const errors = {};
if (section === 'querystring') {
section = 'query';
}
// For each error
for (const e of validationErrors) {
let message = '';
let pattern;
let value;
let reason;
// Normalize the key
let key = e.dataPath;
if (key.startsWith('.')) {
key = key.substring(1);
}
// Remove useless quotes
/* istanbul ignore next */
if (key.startsWith('[') && key.endsWith(']')) {
key = key.substring(1, key.length - 1);
}
// Depending on the type
switch (e.keyword) {
case 'required':
case 'dependencies':
key = e.params.missingProperty;
message = exports.validationMessagesFormatters.missing();
break;
case 'additionalProperties':
key = e.params.additionalProperty;
message = exports.validationMessagesFormatters.unknown();
break;
case 'type':
message = exports.validationMessagesFormatters.paramType(e.params.type);
break;
case 'minProperties':
message = exports.validationMessagesFormatters.minimumProperties(e.params.limit);
break;
case 'maxProperties':
message = exports.validationMessagesFormatters.maximumProperties(e.params.limit);
break;
case 'minItems':
message = exports.validationMessagesFormatters.minimumItems(e.params.limit);
break;
case 'maxItems':
message = exports.validationMessagesFormatters.maximumItems(e.params.limit);
break;
case 'minimum':
message = exports.validationMessagesFormatters.minimum(e.params.limit);
break;
case 'maximum':
message = exports.validationMessagesFormatters.maximum(e.params.limit);
break;
case 'enum':
message = exports.validationMessagesFormatters.enum(e.params.allowedValues);
break;
case 'pattern':
pattern = e.params.pattern;
value = utils_1.get(data, key);
if (pattern === '.+' && !value) {
message = exports.validationMessagesFormatters.presentString();
}
else {
message = exports.validationMessagesFormatters.pattern(e.params.pattern);
}
break;
case 'format':
reason = e.params.format;
// Normalize the key
if (reason === 'date-time') {
reason = 'timestamp';
}
message = (exports.validationMessagesFormatters[reason] || exports.validationMessagesFormatters.invalidFormat)(reason);
break;
}
// No custom message was found, default to input one replacing the starting verb and adding some path info
if (!message.length) {
message = `${e.message.replace(/^should/, 'must')} (${e.keyword})`;
}
// Remove useless quotes
/* istanbul ignore next */
if (key.match(/(?:^['"])(?:[^.]+)(?:['"]$)/)) {
key = key.substring(1, key.length - 1);
}
// Fix empty properties
if (!key) {
key = '$root';
}
key = key.replace(/^\//, '');
errors[key] = message;
}
return { [section]: errors };
}
exports.convertValidationErrors = convertValidationErrors;
function addResponseValidation(route) {
var _a;
if (!((_a = route.schema) === null || _a === void 0 ? void 0 : _a.response)) {
return;
}
const validators = {};
/*
Add these validators to the list of the one to compile once the server is started.
This makes possible to handle shared schemas.
*/
this[interfaces_1.kHttpErrorsEnhancedResponseValidations].push([
this,
validators,
Object.entries(route.schema.response)
]);
// Note that this hook is not called for non JSON payloads therefore validation is not possible in such cases
route.preSerialization = async function (request, reply, payload) {
const statusCode = reply.raw.statusCode;
// Never validate error 500
if (statusCode === http_errors_enhanced_1.INTERNAL_SERVER_ERROR) {
return payload;
}
// No validator, it means the HTTP status is not allowed
const validator = validators[statusCode];
if (!validator) {
if (request[interfaces_1.kHttpErrorsEnhancedConfiguration].allowUndeclaredResponses) {
return payload;
}
throw new http_errors_enhanced_1.InternalServerError(exports.validationMessagesFormatters.invalidResponseCode(statusCode));
}
// Now validate the payload
const valid = validator(payload);
if (!valid) {
throw new http_errors_enhanced_1.InternalServerError(exports.validationMessagesFormatters.invalidResponse(statusCode), {
failedValidations: convertValidationErrors('response', payload, validator.errors)
});
}
return payload;
};
}
exports.addResponseValidation = addResponseValidation;
function compileResponseValidationSchema(configuration) {
// Fix CJS/ESM interoperability
// @ts-expect-error
let AjvConstructor = ajv_1.default;
/* istanbul ignore next */
if (AjvConstructor.default) {
AjvConstructor = AjvConstructor.default;
}
const hasCustomizer = typeof configuration.responseValidatorCustomizer === 'function';
for (const [instance, validators, schemas] of this[interfaces_1.kHttpErrorsEnhancedResponseValidations]) {
// @ts-expect-error
const compiler = new AjvConstructor({
// The fastify defaults, with the exception of removeAdditional and coerceTypes, which have been reversed
removeAdditional: false,
useDefaults: true,
coerceTypes: false,
allErrors: true
});
compiler.addSchema(Object.values(instance.getSchemas()));
compiler.addKeyword('example');
if (hasCustomizer) {
configuration.responseValidatorCustomizer(compiler);
}
for (const [code, schema] of schemas) {
validators[code] = compiler.compile(schema);
}
}
}
exports.compileResponseValidationSchema = compileResponseValidationSchema;

View File

@@ -1,48 +0,0 @@
const dayjs = require('dayjs')
const axios = require('axios')
const ApplicationLog = require('../models/Logs/Application')
const ServerLog = require('../models/Logs/Server')
const Settings = require('../models/Settings')
const { version } = require('../../package.json')
function generateTimestamp () {
return `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} `
}
const patterns = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
].join('|')
async function saveAppLog (event, configuration, isError) {
try {
const deployId = configuration.general.deployId
const repoId = configuration.repository.id
const branch = configuration.repository.branch
if (isError) {
const clearedEvent = '[ERROR 😱] ' + generateTimestamp() + event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '')
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
} else {
if (event && event !== '\n') {
const clearedEvent = '[INFO] ' + generateTimestamp() + event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '')
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
}
}
} catch (error) {
console.log(error)
return error
}
}
async function saveServerLog (error) {
const settings = await Settings.findOne({ applicationName: 'coolify' })
const payload = { message: error.message, stack: error.stack, type: error.type || 'spaghetticode', version }
const found = await ServerLog.find(payload)
if (found.length === 0 && error.message) await new ServerLog(payload).save()
if (settings && settings.sendErrors && process.env.NODE_ENV === 'production') await axios.post('https://errors.coollabs.io/api/error', payload)
}
module.exports = {
saveAppLog,
saveServerLog
}

View File

@@ -1,185 +0,0 @@
const { execShellAsync, cleanupTmp, baseServiceConfiguration } = require('../../common')
const yaml = require('js-yaml')
const fs = require('fs').promises
const generator = require('generate-password')
const { docker } = require('../../docker')
async function plausible ({ email, userName, userPassword, baseURL, traefikURL }) {
const deployId = 'plausible'
const workdir = '/tmp/plausible'
const secretKey = generator.generate({ length: 64, numbers: true, strict: true })
const generateEnvsPostgres = {
POSTGRESQL_PASSWORD: generator.generate({ length: 24, numbers: true, strict: true }),
POSTGRESQL_USERNAME: generator.generate({ length: 10, numbers: true, strict: true }),
POSTGRESQL_DATABASE: 'plausible'
}
const secrets = [
{ name: 'ADMIN_USER_EMAIL', value: email },
{ name: 'ADMIN_USER_NAME', value: userName },
{ name: 'ADMIN_USER_PWD', value: userPassword },
{ name: 'BASE_URL', value: baseURL },
{ name: 'SECRET_KEY_BASE', value: secretKey },
{ name: 'DISABLE_AUTH', value: 'false' },
{ name: 'DISABLE_REGISTRATION', value: 'true' },
{ name: 'DATABASE_URL', value: `postgresql://${generateEnvsPostgres.POSTGRESQL_USERNAME}:${generateEnvsPostgres.POSTGRESQL_PASSWORD}@plausible_db:5432/${generateEnvsPostgres.POSTGRESQL_DATABASE}` },
{ name: 'CLICKHOUSE_DATABASE_URL', value: 'http://plausible_events_db:8123/plausible' }
]
const generateEnvsClickhouse = {}
for (const secret of secrets) generateEnvsClickhouse[secret.name] = secret.value
const clickhouseConfigXml = `
<yandex>
<logger>
<level>warning</level>
<console>true</console>
</logger>
<!-- Stop all the unnecessary logging -->
<query_thread_log remove="remove"/>
<query_log remove="remove"/>
<text_log remove="remove"/>
<trace_log remove="remove"/>
<metric_log remove="remove"/>
<asynchronous_metric_log remove="remove"/>
</yandex>`
const clickhouseUserConfigXml = `
<yandex>
<profiles>
<default>
<log_queries>0</log_queries>
<log_query_threads>0</log_query_threads>
</default>
</profiles>
</yandex>`
const clickhouseConfigs = [
{ source: 'plausible-clickhouse-user-config.xml', target: '/etc/clickhouse-server/users.d/logging.xml' },
{ source: 'plausible-clickhouse-config.xml', target: '/etc/clickhouse-server/config.d/logging.xml' },
{ source: 'plausible-init.query', target: '/docker-entrypoint-initdb.d/init.query' },
{ source: 'plausible-init-db.sh', target: '/docker-entrypoint-initdb.d/init-db.sh' }
]
const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;'
const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query'
await execShellAsync(`mkdir -p ${workdir}`)
await fs.writeFile(`${workdir}/clickhouse-config.xml`, clickhouseConfigXml)
await fs.writeFile(`${workdir}/clickhouse-user-config.xml`, clickhouseUserConfigXml)
await fs.writeFile(`${workdir}/init.query`, initQuery)
await fs.writeFile(`${workdir}/init-db.sh`, initScript)
const stack = {
version: '3.8',
services: {
[deployId]: {
image: 'plausible/analytics:latest',
command: 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"',
networks: [`${docker.network}`],
volumes: [`${deployId}-postgres-data:/var/lib/postgresql/data`],
environment: generateEnvsClickhouse,
deploy: {
...baseServiceConfiguration,
labels: [
'managedBy=coolify',
'type=service',
'serviceName=plausible',
'configuration=' + JSON.stringify({ email, userName, userPassword, baseURL, secretKey, generateEnvsPostgres, generateEnvsClickhouse }),
'traefik.enable=true',
'traefik.http.services.' +
deployId +
'.loadbalancer.server.port=8000',
'traefik.http.routers.' +
deployId +
'.entrypoints=websecure',
'traefik.http.routers.' +
deployId +
'.rule=Host(`' +
traefikURL +
'`) && PathPrefix(`/`)',
'traefik.http.routers.' +
deployId +
'.tls.certresolver=letsencrypt',
'traefik.http.routers.' +
deployId +
'.middlewares=global-compress'
]
}
},
plausible_db: {
image: 'bitnami/postgresql:13.2.0',
networks: [`${docker.network}`],
environment: generateEnvsPostgres,
deploy: {
...baseServiceConfiguration,
labels: [
'managedBy=coolify',
'type=service',
'serviceName=plausible'
]
}
},
plausible_events_db: {
image: 'yandex/clickhouse-server:21.3.2.5',
networks: [`${docker.network}`],
volumes: [`${deployId}-clickhouse-data:/var/lib/clickhouse`],
ulimits: {
nofile: {
soft: 262144,
hard: 262144
}
},
configs: [...clickhouseConfigs],
deploy: {
...baseServiceConfiguration,
labels: [
'managedBy=coolify',
'type=service',
'serviceName=plausible'
]
}
}
},
networks: {
[`${docker.network}`]: {
external: true
}
},
volumes: {
[`${deployId}-clickhouse-data`]: {
external: true
},
[`${deployId}-postgres-data`]: {
external: true
}
},
configs: {
'plausible-clickhouse-user-config.xml': {
file: `${workdir}/clickhouse-user-config.xml`
},
'plausible-clickhouse-config.xml': {
file: `${workdir}/clickhouse-config.xml`
},
'plausible-init.query': {
file: `${workdir}/init.query`
},
'plausible-init-db.sh': {
file: `${workdir}/init-db.sh`
}
}
}
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack))
await execShellAsync('docker stack rm plausible')
await execShellAsync(
`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`
)
cleanupTmp(workdir)
}
async function activateAdminUser () {
const { POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD, POSTGRESQL_DATABASE } = JSON.parse(JSON.parse((await execShellAsync('docker service inspect plausible_plausible --format=\'{{json .Spec.Labels.configuration}}\'')))).generateEnvsPostgres
const containers = (await execShellAsync('docker ps -a --format=\'{{json .Names}}\'')).replace(/"/g, '').trim().split('\n')
const postgresDB = containers.find(container => container.startsWith('plausible_plausible_db'))
await execShellAsync(`docker exec ${postgresDB} psql -H postgresql://${POSTGRESQL_USERNAME}:${POSTGRESQL_PASSWORD}@localhost:5432/${POSTGRESQL_DATABASE} -c "UPDATE users SET email_verified = true;"`)
}
module.exports = { plausible, activateAdminUser }

View File

@@ -1,16 +0,0 @@
const mongoose = require('mongoose')
const deploymentSchema = mongoose.Schema(
{
deployId: { type: String, required: true },
nickname: { type: String, required: true },
repoId: { type: Number, required: true },
organization: { type: String, required: true },
name: { type: String, required: true },
branch: { type: String, required: true },
domain: { type: String, required: true },
progress: { type: String, require: true, default: 'queued' }
},
{ timestamps: true }
)
module.exports = mongoose.model('deployment', deploymentSchema)

View File

@@ -1,10 +0,0 @@
const mongoose = require('mongoose')
const logSchema = mongoose.Schema(
{
deployId: { type: String, required: true },
event: { type: String, required: true }
},
{ timestamps: { createdAt: 'createdAt', updatedAt: false } }
)
module.exports = mongoose.model('logs-application', logSchema)

View File

@@ -1,14 +0,0 @@
const mongoose = require('mongoose')
const { version } = require('../../../package.json')
const logSchema = mongoose.Schema(
{
version: { type: String, default: version },
type: { type: String, required: true },
message: { type: String, required: true },
stack: { type: String },
seen: { type: Boolean, default: false }
},
{ timestamps: { createdAt: 'createdAt', updatedAt: false } }
)
module.exports = mongoose.model('logs-server', logSchema)

View File

@@ -1,12 +0,0 @@
const mongoose = require('mongoose')
const settingsSchema = mongoose.Schema(
{
applicationName: { type: String, required: true, default: 'coolify' },
allowRegistration: { type: Boolean, required: true, default: false },
sendErrors: { type: Boolean, required: true, default: true }
},
{ timestamps: true }
)
module.exports = mongoose.model('settings', settingsSchema)

View File

@@ -1,12 +0,0 @@
const mongoose = require('mongoose')
const userSchema = mongoose.Schema(
{
email: { type: String, required: true },
avatar: { type: String },
uid: { type: String, required: true }
},
{ timestamps: true }
)
module.exports = mongoose.model('user', userSchema)

View File

@@ -1,21 +0,0 @@
const fp = require('fastify-plugin')
const User = require('../models/User')
module.exports = fp(async function (fastify, options, next) {
fastify.register(require('fastify-jwt'), {
secret: fastify.config.JWT_SIGN_KEY
})
fastify.addHook('onRequest', async (request, reply) => {
try {
const { jti } = await request.jwtVerify()
const found = await User.findOne({ uid: jti })
if (found) {
return true
} else {
reply.code(401).send('Unauthorized')
}
} catch (err) {
reply.code(401).send('Unauthorized')
}
})
next()
})

View File

@@ -1,37 +0,0 @@
const { setDefaultConfiguration } = require('../../../libs/applications/configuration')
const { docker } = require('../../../libs/docker')
const { saveServerLog } = require('../../../libs/logging')
module.exports = async function (fastify) {
fastify.post('/', async (request, reply) => {
try {
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
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) {
await saveServerLog(error)
throw new Error(error)
}
})
}

View File

@@ -1,69 +0,0 @@
const Deployment = require('../../../../models/Deployment')
const ApplicationLog = require('../../../../models/Logs/Application')
const { verifyUserId, cleanupTmp } = require('../../../../libs/common')
const { purgeImagesContainers } = require('../../../../libs/applications/cleanup')
const { queueAndBuild } = require('../../../../libs/applications')
const { setDefaultConfiguration, precheckDeployment } = require('../../../../libs/applications/configuration')
const { docker } = require('../../../../libs/docker')
const { saveServerLog } = require('../../../../libs/logging')
const cloneRepository = require('../../../../libs/applications/github/cloneRepository')
module.exports = async function (fastify) {
fastify.post('/', async (request, reply) => {
let configuration
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')
configuration = setDefaultConfiguration(request.body)
if (!configuration) {
throw new Error('Whaat?')
}
await cloneRepository(configuration)
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
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
}
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name, deployId: configuration.general.deployId })
await queueAndBuild(configuration, imageChanged)
} catch (error) {
const { id, organization, name, branch } = configuration.repository
const { domain } = configuration.publish
const { deployId } = configuration.general
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
if (error.name) {
if (error.message && error.stack) await saveServerLog(error)
if (reply.sent) await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
}
throw new Error(error)
} finally {
cleanupTmp(configuration.general.workdir)
await purgeImagesContainers(configuration)
}
})
}

View File

@@ -1,66 +0,0 @@
const ApplicationLog = require('../../../../models/Logs/Application')
const Deployment = require('../../../../models/Deployment')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(utc)
dayjs.extend(relativeTime)
module.exports = async function (fastify) {
const getLogSchema = {
querystring: {
type: 'object',
properties: {
repoId: { type: 'string' },
branch: { type: 'string' }
},
required: ['repoId', 'branch']
}
}
fastify.get('/', { schema: getLogSchema }, async (request, reply) => {
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 updatedAt = dayjs(d.updatedAt).utc()
finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000
finalLogs.since = updatedAt.fromNow()
return finalLogs
})
return finalLogs
} catch (error) {
throw new Error(error)
}
})
fastify.get('/:deployId', async (request, reply) => {
const { deployId } = request.params
try {
const logs = await ApplicationLog.find({ deployId })
.select('-_id -__v')
.sort({ createdAt: 'asc' })
const deploy = await Deployment.findOne({ deployId })
.select('-_id -__v')
.sort({ createdAt: 'desc' })
const finalLogs = {}
finalLogs.progress = deploy.progress
finalLogs.events = logs.map(log => log.event)
finalLogs.human = dayjs(deploy.updatedAt).from(dayjs(deploy.updatedAt))
return finalLogs
} catch (e) {
throw new Error('No logs found')
}
})
}

View File

@@ -1,16 +0,0 @@
const { docker } = require('../../../libs/docker')
const { saveServerLog } = require('../../../libs/logging')
module.exports = async function (fastify) {
fastify.get('/', async (request, reply) => {
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) {
await saveServerLog(error)
throw new Error(error)
}
})
}

View File

@@ -1,38 +0,0 @@
const { docker } = require('../../../libs/docker')
const { execShellAsync, delay } = require('../../../libs/common')
const ApplicationLog = require('../../../models/Logs/Application')
const Deployment = require('../../../models/Deployment')
const { purgeImagesContainers } = require('../../../libs/applications/cleanup')
module.exports = async function (fastify) {
fastify.post('/', async (request, reply) => {
const { organization, name, branch } = request.body
let found = false
try {
(await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application').map(s => {
const running = JSON.parse(s.Spec.Labels.configuration)
if (running.repository.organization === organization &&
running.repository.name === name &&
running.repository.branch === branch) {
found = running
}
return null
})
if (found) {
const deploys = await Deployment.find({ organization, branch, name })
for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId })
await Deployment.deleteMany({ deployId: deploy.deployId })
}
await execShellAsync(`docker stack rm ${found.build.container.name}`)
reply.code(200).send({ organization, name, branch })
await delay(10000)
await purgeImagesContainers(found, true)
} else {
reply.code(500).send({ message: 'Nothing to do.' })
}
} catch (error) {
reply.code(500).send({ message: 'Nothing to do.' })
}
})
}

View File

@@ -1,28 +0,0 @@
const { docker } = require('../../libs/docker')
module.exports = async function (fastify) {
fastify.post('/', async (request, reply) => {
const { name, organization, branch } = request.body
const services = await docker.engine.listServices()
const applications = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
const found = applications.find(r => {
const configuration = r.Spec.Labels.configuration ? JSON.parse(r.Spec.Labels.configuration) : null
if (branch) {
if (configuration.repository.name === name && configuration.repository.organization === organization && configuration.repository.branch === branch) {
return r
}
} else {
if (configuration.repository.name === name && configuration.repository.organization === organization) {
return r
}
}
return null
})
if (found) {
return JSON.parse(found.Spec.Labels.configuration)
} else {
reply.code(500).send({ message: 'No configuration found.' })
}
})
}

View File

@@ -1,61 +0,0 @@
const { docker } = require('../../../libs/docker')
const ServerLog = require('../../../models/Logs/Server')
const { saveServerLog } = require('../../../libs/logging')
module.exports = async function (fastify) {
fastify.get('/', async (request, reply) => {
try {
const serverLogs = await ServerLog.find()
const dockerServices = await docker.engine.listServices()
let applications = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' && r.Spec.Labels.configuration)
let databases = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && r.Spec.Labels.configuration)
let services = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'service' && r.Spec.Labels.configuration)
applications = applications.map(r => {
if (JSON.parse(r.Spec.Labels.configuration)) {
return {
configuration: JSON.parse(r.Spec.Labels.configuration),
UpdatedAt: r.UpdatedAt
}
}
return {}
})
databases = databases.map(r => {
if (JSON.parse(r.Spec.Labels.configuration)) {
return {
configuration: JSON.parse(r.Spec.Labels.configuration)
}
}
return {}
})
services = services.map(r => {
if (JSON.parse(r.Spec.Labels.configuration)) {
return {
serviceName: r.Spec.Labels.serviceName,
configuration: JSON.parse(r.Spec.Labels.configuration)
}
}
return {}
})
applications = [...new Map(applications.map(item => [item.configuration.publish.domain + item.configuration.publish.path, item])).values()]
return {
serverLogs,
applications: {
deployed: applications
},
databases: {
deployed: databases
},
services: {
deployed: services
}
}
} catch (error) {
if (error.code === 'ENOENT' && error.errno === -2) {
throw new Error(`Docker service unavailable at ${error.address}.`)
} else {
await saveServerLog(error)
throw new Error(error)
}
}
})
}

View File

@@ -1,192 +0,0 @@
const yaml = require('js-yaml')
const fs = require('fs').promises
const cuid = require('cuid')
const { docker } = require('../../../libs/docker')
const { execShellAsync } = require('../../../libs/common')
const { saveServerLog } = require('../../../libs/logging')
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
const generator = require('generate-password')
function getUniq () {
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 })
}
module.exports = async function (fastify) {
fastify.get('/:deployId', async (request, reply) => {
const { deployId } = request.params
try {
const database = (await docker.engine.listServices()).find(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && JSON.parse(r.Spec.Labels.configuration).general.deployId === deployId)
if (database) {
const jsonEnvs = {}
if (database.Spec.TaskTemplate.ContainerSpec.Env) {
for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
const s = d.split('=')
jsonEnvs[s[0]] = s[1]
}
}
const payload = {
config: JSON.parse(database.Spec.Labels.configuration),
envs: jsonEnvs || null
}
reply.code(200).send(payload)
} else {
throw new Error()
}
} catch (error) {
throw new Error('No database found?')
}
})
const postSchema = {
body: {
type: 'object',
properties: {
type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb', 'clickhouse'] }
},
required: ['type']
}
}
fastify.post('/deploy', { schema: postSchema }, async (request, reply) => {
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
reply.code(201).send({ message: 'Deploying.' })
const deployId = cuid()
const configuration = {
general: {
workdir: `/tmp/${deployId}`,
deployId,
nickname,
type
},
database: {
usernames,
passwords,
defaultDatabaseName
},
deploy: {
name: nickname
}
}
await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
let generateEnvs = {}
let image = null
let volume = null
let ulimits = {}
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`
} else if (type === 'clickhouse') {
image = 'yandex/clickhouse-server'
volume = `${configuration.general.deployId}-${type}-data:/var/lib/clickhouse`
ulimits = {
nofile: {
soft: 262144,
hard: 262144
}
}
}
const stack = {
version: '3.8',
services: {
[configuration.general.deployId]: {
image,
networks: [`${docker.network}`],
environment: generateEnvs,
volumes: [volume],
ulimits,
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
}
}
}
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) {
console.log(error)
await saveServerLog(error)
throw new Error(error)
}
})
fastify.delete('/:dbName', async (request, reply) => {
const { dbName } = request.params
await execShellAsync(`docker stack rm ${dbName}`)
reply.code(200).send({})
})
}

View File

@@ -1,127 +0,0 @@
const axios = require('axios')
const User = require('../../../models/User')
const Settings = require('../../../models/Settings')
const cuid = require('cuid')
const mongoose = require('mongoose')
const jwt = require('jsonwebtoken')
const { saveServerLog } = require('../../../libs/logging')
module.exports = async function (fastify) {
const githubCodeSchema = {
schema: {
querystring: {
type: 'object',
properties: {
code: { type: 'string' }
},
required: ['code']
}
}
}
fastify.get('/app', { schema: githubCodeSchema }, async (request, reply) => {
const { code } = request.query
try {
const { data } = await axios({
method: 'post',
url: `https://github.com/login/oauth/access_token?client_id=${fastify.config.VITE_GITHUB_APP_CLIENTID}&client_secret=${fastify.config.GITHUB_APP_CLIENT_SECRET}&code=${code}`,
headers: {
accept: 'application/json'
}
})
const token = data.access_token
const githubAxios = axios.create({
baseURL: 'https://api.github.com'
})
githubAxios.defaults.headers.common.Accept = 'Application/json'
githubAxios.defaults.headers.common.Authorization = `token ${token}`
try {
let uid = cuid()
const { avatar_url } = (await githubAxios.get('/user')).data // eslint-disable-line
const email = (await githubAxios.get('/user/emails')).data.filter(
(e) => e.primary
)[0].email
const settings = await Settings.findOne({ applicationName: 'coolify' })
const registeredUsers = await User.find().countDocuments()
const foundUser = await User.findOne({ email })
if (foundUser) {
await User.findOneAndUpdate(
{ email },
{ avatar: avatar_url },
{ upsert: true, new: true }
)
uid = foundUser.uid
} else {
if (registeredUsers === 0) {
const newUser = new User({
_id: new mongoose.Types.ObjectId(),
email,
avatar: avatar_url,
uid
})
const defaultSettings = new Settings({
_id: new mongoose.Types.ObjectId()
})
try {
await newUser.save()
await defaultSettings.save()
} catch (e) {
console.log(e)
reply.code(500).send({ success: false, error: e })
return
}
} else {
if (!settings && registeredUsers > 0) {
reply.code(500).send('Registration disabled, enable it in settings.')
} else {
if (!settings.allowRegistration) {
reply.code(500).send('You are not allowed here!')
} else {
const newUser = new User({
_id: new mongoose.Types.ObjectId(),
email,
avatar: avatar_url,
uid
})
try {
await newUser.save()
} catch (e) {
console.log(e)
reply.code(500).send({ success: false, error: e })
return
}
}
}
}
}
const jwtToken = jwt.sign({}, fastify.config.JWT_SIGN_KEY, {
expiresIn: 15778800,
algorithm: 'HS256',
audience: 'coolLabs',
issuer: 'coolLabs',
jwtid: uid,
subject: `User:${uid}`,
notBefore: -1000
})
reply
.code(200)
.redirect(
302,
`/api/v1/login/github/success?jwtToken=${jwtToken}&ghToken=${token}`
)
} catch (e) {
console.log(e)
reply.code(500).send({ success: false, error: e })
return
}
} catch (error) {
await saveServerLog(error)
throw new Error(error)
}
})
fastify.get('/success', async (request, reply) => {
return reply.sendFile('bye.html')
})
}

View File

@@ -1,14 +0,0 @@
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 new Error(error)
}
})
}

View File

@@ -1,15 +0,0 @@
const { plausible, activateAdminUser } = require('../../../libs/services/plausible')
module.exports = async function (fastify) {
fastify.post('/plausible', async (request, reply) => {
let { email, userName, userPassword, baseURL } = request.body
const traefikURL = baseURL
baseURL = `https://${baseURL}`
await plausible({ email, userName, userPassword, baseURL, traefikURL })
return {}
})
fastify.patch('/plausible/activate', async (request, reply) => {
await activateAdminUser()
return 'OK'
})
}

View File

@@ -1,27 +0,0 @@
const { execShellAsync } = require('../../../libs/common')
const { docker } = require('../../../libs/docker')
module.exports = async function (fastify) {
fastify.get('/:serviceName', async (request, reply) => {
const { serviceName } = request.params
try {
const service = (await docker.engine.listServices()).find(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'service' && r.Spec.Labels.serviceName === serviceName && r.Spec.Name === `${serviceName}_${serviceName}`)
if (service) {
const payload = {
config: JSON.parse(service.Spec.Labels.configuration)
}
reply.code(200).send(payload)
} else {
throw new Error()
}
} catch (error) {
console.log(error)
throw new Error('No service found?')
}
})
fastify.delete('/:serviceName', async (request, reply) => {
const { serviceName } = request.params
await execShellAsync(`docker stack rm ${serviceName}`)
reply.code(200).send({})
})
}

View File

@@ -1,49 +0,0 @@
const Settings = require('../../../models/Settings')
const { saveServerLog } = require('../../../libs/logging')
module.exports = async function (fastify) {
const applicationName = 'coolify'
const postSchema = {
body: {
type: 'object',
properties: {
allowRegistration: { type: 'boolean' },
sendErrors: { type: 'boolean' }
},
required: []
}
}
fastify.get('/', async (request, reply) => {
try {
let settings = await Settings.findOne({ applicationName }).select('-_id -__v')
// TODO: Should do better
if (!settings) {
settings = {
applicationName,
allowRegistration: false
}
}
return {
settings
}
} catch (error) {
await saveServerLog(error)
throw new Error(error)
}
})
fastify.post('/', { schema: postSchema }, async (request, reply) => {
try {
const settings = await Settings.findOneAndUpdate(
{ applicationName },
{ applicationName, ...request.body },
{ upsert: true, new: true }
).select('-_id -__v')
reply.code(201).send({ settings })
} catch (error) {
await saveServerLog(error)
throw new Error(error)
}
})
}

View File

@@ -1,5 +0,0 @@
module.exports = async function (fastify) {
fastify.get('/', async (request, reply) => {
reply.code(200).send('NO')
})
}

View File

@@ -1,12 +0,0 @@
const { execShellAsync } = require('../../../libs/common')
const { saveServerLog } = require('../../../libs/logging')
module.exports = async function (fastify) {
fastify.get('/', async (request, reply) => {
const upgradeP1 = await execShellAsync('bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p1.sh)"')
await saveServerLog({ message: upgradeP1, type: 'UPGRADE-P-1' })
reply.code(200).send('I\'m trying, okay?')
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({ message: upgradeP2, type: 'UPGRADE-P-2' })
})
}

View File

@@ -1,20 +0,0 @@
const User = require('../../models/User')
const jwt = require('jsonwebtoken')
module.exports = async function (fastify) {
fastify.get('/', async (request, reply) => {
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({})
}
})
}

View File

@@ -1,121 +0,0 @@
const crypto = require('crypto')
const { cleanupTmp } = require('../../../libs/common')
const Deployment = require('../../../models/Deployment')
const ApplicationLog = require('../../../models/Logs/Application')
const ServerLog = require('../../../models/Logs/Server')
const { queueAndBuild } = require('../../../libs/applications')
const { setDefaultConfiguration, precheckDeployment } = require('../../../libs/applications/configuration')
const { docker } = require('../../../libs/docker')
const cloneRepository = require('../../../libs/applications/github/cloneRepository')
const { purgeImagesContainers } = require('../../../libs/applications/cleanup')
module.exports = async function (fastify) {
const postSchema = {
body: {
type: 'object',
properties: {
ref: { type: 'string' },
repository: {
type: 'object',
properties: {
id: { type: 'number' },
full_name: { type: 'string' }
},
required: ['id', 'full_name']
},
installation: {
type: 'object',
properties: {
id: { type: 'number' }
},
required: ['id']
}
},
required: ['ref', 'repository', 'installation']
}
}
fastify.post('/', { schema: postSchema }, async (request, reply) => {
let configuration
const hmac = crypto.createHmac('sha256', fastify.config.GITHUP_APP_WEBHOOK_SECRET)
const digest = Buffer.from('sha256=' + hmac.update(JSON.stringify(request.body)).digest('hex'), 'utf8')
const checksum = Buffer.from(request.headers['x-hub-signature-256'], 'utf8')
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
reply.code(500).send({ error: 'Invalid request' })
return
}
if (request.headers['x-github-event'] !== 'push') {
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')
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
}
configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration))
await cloneRepository(configuration)
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
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
}
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
await queueAndBuild(configuration, imageChanged)
} catch (error) {
const { id, organization, name, branch } = configuration.repository
const { domain } = configuration.publish
const { deployId } = configuration.general
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
if (error.name === 'Error') {
// Error during runtime
await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
} else {
// Error in my code
const payload = { message: error.message, stack: error.stack, type: 'spaghetticode' }
if (error.message && error.stack) await new ServerLog(payload).save()
if (reply.sent) await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
}
throw new Error(error)
} finally {
cleanupTmp(configuration.general.workdir)
await purgeImagesContainers(configuration)
}
})
}

View File

@@ -1,49 +0,0 @@
const schema = {
type: 'object',
required: [
'DOMAIN',
'EMAIL',
'VITE_GITHUB_APP_CLIENTID',
'GITHUB_APP_CLIENT_SECRET',
'GITHUB_APP_PRIVATE_KEY',
'GITHUP_APP_WEBHOOK_SECRET',
'JWT_SIGN_KEY',
'SECRETS_ENCRYPTION_KEY'
],
properties: {
DOMAIN: {
type: 'string'
},
EMAIL: {
type: 'string'
},
VITE_GITHUB_APP_CLIENTID: {
type: 'string'
},
GITHUB_APP_CLIENT_SECRET: {
type: 'string'
},
GITHUB_APP_PRIVATE_KEY: {
type: 'string'
},
GITHUP_APP_WEBHOOK_SECRET: {
type: 'string'
},
JWT_SIGN_KEY: {
type: 'string'
},
DOCKER_ENGINE: {
type: 'string',
default: '/var/run/docker.sock'
},
DOCKER_NETWORK: {
type: 'string',
default: 'coollabs'
},
SECRETS_ENCRYPTION_KEY: {
type: 'string'
}
}
}
module.exports = { schema }

View File

@@ -1,109 +0,0 @@
require('dotenv').config()
const fs = require('fs')
const util = require('util')
const mongoose = require('mongoose')
const path = require('path')
const { saveServerLog } = require('./libs/logging')
const { execShellAsync } = require('./libs/common')
const { cleanupStuckedDeploymentsInDB } = require('./libs/applications/cleanup')
const fastify = require('fastify')({
trustProxy: true,
logger: {
level: 'error'
}
})
fastify.register(require('../api/libs/http-error'))
const { schema } = require('./schema')
process.on('unhandledRejection', async (reason, p) => {
await saveServerLog({ message: reason.message, type: 'unhandledRejection' })
})
fastify.register(require('fastify-env'), {
schema,
dotenv: true
})
if (process.env.NODE_ENV === 'production') {
fastify.register(require('fastify-static'), {
root: path.join(__dirname, '../dist/')
})
fastify.setNotFoundHandler(function (request, reply) {
reply.sendFile('index.html')
})
} else {
fastify.register(require('fastify-static'), {
root: path.join(__dirname, '../public/')
})
}
fastify.register(require('./app'), { prefix: '/api/v1' })
if (process.env.NODE_ENV === 'production') {
mongoose.connect(
`mongodb://${process.env.MONGODB_USER}:${process.env.MONGODB_PASSWORD}@${process.env.MONGODB_HOST}:${process.env.MONGODB_PORT}/${process.env.MONGODB_DB}?authSource=${process.env.MONGODB_DB}&readPreference=primary&ssl=false`,
{ useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
)
} else {
mongoose.connect(
'mongodb://localhost:27017/coolify?&readPreference=primary&ssl=false',
{ useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
)
}
mongoose.connection.on(
'error',
console.error.bind(console, 'connection error:')
)
mongoose.connection.once('open', async function () {
if (process.env.NODE_ENV === 'production') {
fastify.listen(3000, '0.0.0.0')
console.log('Coolify API is up and running in production.')
} else {
const logFile = fs.createWriteStream('api/development/console.log', { flags: 'w' })
const logStdout = process.stdout
console.log = function (d) {
logFile.write(`[INFO]: ${util.format(d)}\n`)
logStdout.write(util.format(d) + '\n')
}
console.error = function (d) {
logFile.write(`[ERROR]: ${util.format(d)}\n`)
logStdout.write(util.format(d) + '\n')
}
console.warn = function (d) {
logFile.write(`[WARN]: ${util.format(d)}\n`)
logStdout.write(util.format(d) + '\n')
}
fastify.listen(3001)
console.log('Coolify API is up and running in development.')
}
try {
// Always cleanup server logs
await mongoose.connection.db.dropCollection('logs-servers')
} catch (error) {
// Could not cleanup logs-servers collection
}
// On start cleanup inprogress/queued deployments.
try {
await cleanupStuckedDeploymentsInDB()
} catch (error) {
// Could not cleanup DB 🤔
}
try {
// Doing because I do not want to prune these images. Prune skips coolify-reserve labeled images.
const basicImages = ['nginx:stable-alpine', 'node:lts', 'ubuntu:20.04', 'php:apache', 'rust:latest']
for (const image of basicImages) {
// await execShellAsync(`echo "FROM ${image}" | docker build --label coolify-reserve=true -t ${image} -`)
await execShellAsync(`docker pull ${image}`)
}
} catch (error) {
console.log('Could not pull some basic images from Docker Hub.')
console.log(error)
}
})

84
docker-compose-dev.yml Normal file
View File

@@ -0,0 +1,84 @@
version: '3.8'
services:
proxy:
image: traefik:v2.4
hostname: coollabs-proxy
ports:
- target: 80
published: 80
protocol: tcp
mode: host
- target: 443
published: 443
protocol: tcp
mode: host
- target: 8080
published: 8080
protocol: tcp
mode: host
command:
- --api.insecure=true
- --api.dashboard=true
- --api.debug=true
- --log.level=ERROR
- --providers.docker=true
- --providers.docker.swarmMode=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=coollabs
- --providers.docker.swarmModeRefreshSeconds=1s
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- coollabs
deploy:
update_config:
parallelism: 1
delay: 10s
order: start-first
replicas: 1
placement:
constraints:
- node.role == manager
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.api.entrypoints=websecure'
- 'traefik.http.routers.api.service=api@internal'
- 'traefik.http.routers.api.middlewares=auth'
- 'traefik.http.services.traefik.loadbalancer.server.port=80'
# Global redirect www to non-www
- 'traefik.http.routers.www-catchall.rule=hostregexp(`{host:www.(.+)}`)'
- 'traefik.http.routers.www-catchall.entrypoints=web'
- 'traefik.http.routers.www-catchall.middlewares=redirect-www-to-nonwww'
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.regex=^http://(?:www\\.)?(.+)"
- 'traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.replacement=http://$${1}'
mongodb:
image: bitnami/mongodb:4.4
hostname: coollabs-mongodb
ports:
- target: 27017
published: 27017
protocol: tcp
mode: host
environment:
- MONGODB_DISABLE_SYSTEM_LOG=true
- MONGODB_ROOT_PASSWORD=developmentPassword4db
- MONGODB_USERNAME=supercooldbuser
- MONGODB_PASSWORD=developmentPassword4db
- MONGODB_DATABASE=coolify
volumes:
- coollabs-mongodb-data:/bitnami/mongodb
networks:
- coollabs
volumes:
coollabs-mongodb-data: {}
networks:
coollabs:
driver: overlay
name: coollabs
external: true

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<link rel="preload" as="image" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Coolify</title>
<link rel="dns-prefetch" href="https://cdn.coollabs.io/" />
<link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" />
<link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" />
<link rel="stylesheet" href="https://cdn.coollabs.io/css/microtip-0.2.2.min.css" />
</head>
<body>
<script type="module" src="/src/index.js"></script>
</body>
</html>

10
install/Dockerfile-dev Normal file
View File

@@ -0,0 +1,10 @@
FROM node:latest
LABEL coolify-preserve=true
WORKDIR /usr/src/app
RUN curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-20.10.6.tgz | tar -xzvf - docker/docker -C . --strip-components 1
RUN mv /usr/src/app/docker /usr/bin/docker
RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o /usr/bin/envsubst
RUN curl -L https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o /usr/bin/jq
RUN chmod +x /usr/bin/envsubst /usr/bin/jq /usr/bin/docker
ADD "https://www.random.org/cgi-bin/randbyte?nbytes=10&format=h" skipcache
RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm

View File

@@ -1,24 +0,0 @@
require('dotenv').config()
const fastify = require('fastify')()
const { schema } = require('../api/schema')
checkConfig().then(() => {
console.log('Config: OK')
}).catch((err) => {
console.log('Config: NOT OK')
console.error(err)
process.exit(1)
})
function checkConfig () {
return new Promise((resolve, reject) => {
fastify.register(require('fastify-env'), {
schema,
dotenv: true
})
.ready((err) => {
if (err) reject(err)
resolve()
})
})
}

View File

@@ -1,73 +0,0 @@
version: '3.8'
services:
proxy:
image: traefik:v2.4
hostname: coollabs-proxy
ports:
- target: 80
published: 80
protocol: tcp
mode: host
- target: 443
published: 443
protocol: tcp
mode: host
- target: 8080
published: 8080
protocol: tcp
mode: host
command:
- --api.insecure=true
- --api.dashboard=true
- --api.debug=true
- --log.level=ERROR
- --providers.docker=true
- --providers.docker.swarmMode=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=${DOCKER_NETWORK}
- --providers.docker.swarmModeRefreshSeconds=1s
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- ${DOCKER_NETWORK}
deploy:
update_config:
parallelism: 1
delay: 10s
order: start-first
replicas: 1
placement:
constraints:
- node.role == manager
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.middlewares=auth"
- "traefik.http.services.traefik.loadbalancer.server.port=80"
- "traefik.http.services.traefik.loadbalancer.server.port=443"
# Global redirect www to non-www
- "traefik.http.routers.www-catchall.rule=hostregexp(`{host:www.(.+)}`)"
- "traefik.http.routers.www-catchall.entrypoints=web"
- "traefik.http.routers.www-catchall.middlewares=redirect-www-to-nonwww"
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.regex=^http://(?:www\\.)?(.+)"
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.replacement=http://$$$${1}"
# Global redirect http to https
- "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
- "traefik.http.routers.http-catchall.entrypoints=web"
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
- "traefik.http.middlewares.global-compress.compress=true"
networks:
${DOCKER_NETWORK}:
driver: overlay
name: ${DOCKER_NETWORK}
external: true

View File

@@ -44,27 +44,27 @@ services:
constraints: constraints:
- node.role == manager - node.role == manager
labels: labels:
- "traefik.enable=true" - 'traefik.enable=true'
- "traefik.http.routers.api.entrypoints=websecure" - 'traefik.http.routers.api.entrypoints=websecure'
- "traefik.http.routers.api.service=api@internal" - 'traefik.http.routers.api.service=api@internal'
- "traefik.http.routers.api.middlewares=auth" - 'traefik.http.routers.api.middlewares=auth'
- "traefik.http.services.traefik.loadbalancer.server.port=80" - 'traefik.http.services.traefik.loadbalancer.server.port=80'
- "traefik.http.services.traefik.loadbalancer.server.port=443" - 'traefik.http.services.traefik.loadbalancer.server.port=443'
# Global redirect www to non-www # Global redirect www to non-www
- "traefik.http.routers.www-catchall.rule=hostregexp(`{host:www.(.+)}`)" - 'traefik.http.routers.www-catchall.rule=hostregexp(`{host:www.(.+)}`)'
- "traefik.http.routers.www-catchall.entrypoints=web" - 'traefik.http.routers.www-catchall.entrypoints=web'
- "traefik.http.routers.www-catchall.middlewares=redirect-www-to-nonwww" - 'traefik.http.routers.www-catchall.middlewares=redirect-www-to-nonwww'
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.regex=^http://(?:www\\.)?(.+)" - "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.regex=^http://(?:www\\.)?(.+)"
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.replacement=http://$$$${1}" - 'traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.replacement=http://$$$${1}'
# Global redirect http to https # Global redirect http to https
- "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)" - 'traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)'
- "traefik.http.routers.http-catchall.entrypoints=web" - 'traefik.http.routers.http-catchall.entrypoints=web'
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https" - 'traefik.http.routers.http-catchall.middlewares=redirect-to-https'
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" - 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'
- "traefik.http.middlewares.global-compress.compress=true" - 'traefik.http.middlewares.global-compress.compress=true'
coolify: coolify:
image: coolify image: coolify
@@ -73,7 +73,7 @@ services:
- .env - .env
networks: networks:
- ${DOCKER_NETWORK} - ${DOCKER_NETWORK}
command: "yarn start" command: 'yarn start'
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
deploy: deploy:
@@ -83,16 +83,15 @@ services:
order: start-first order: start-first
replicas: 1 replicas: 1
labels: labels:
- "traefik.enable=true" - 'traefik.enable=true'
- "traefik.http.routers.coolify.entrypoints=websecure" - 'traefik.http.routers.coolify.entrypoints=websecure'
- "traefik.http.routers.coolify.tls.certresolver=letsencrypt" - 'traefik.http.routers.coolify.tls.certresolver=letsencrypt'
- "traefik.http.routers.coolify.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)" - 'traefik.http.routers.coolify.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)'
- "traefik.http.services.coolify.loadbalancer.server.port=3000" - 'traefik.http.services.coolify.loadbalancer.server.port=3000'
- "traefik.http.routers.coolify.middlewares=global-compress" - 'traefik.http.routers.coolify.middlewares=global-compress'
networks: networks:
${DOCKER_NETWORK}: ${DOCKER_NETWORK}:
driver: overlay driver: overlay
name: ${DOCKER_NETWORK} name: ${DOCKER_NETWORK}
external: true external: true

View File

@@ -1,55 +1,36 @@
require('dotenv').config() require('dotenv').config();
const { program } = require('commander') const { program } = require('commander');
const fastify = require('fastify')() const shell = require('shelljs');
const { schema } = require('../api/schema') const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '');
const shell = require('shelljs')
const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '')
program.version('0.0.1') program.version('0.0.1');
program program
.option('-d, --debug', 'Debug outputs.') .option('-d, --debug', 'Debug outputs.')
.option('-c, --check', 'Only checks configuration.') .option('-c, --check', 'Only checks configuration.')
.option('-t, --type <type>', 'Deploy type.') .option('-t, --type <type>', 'Deploy type.');
program.parse(process.argv) program.parse(process.argv);
const options = program.opts();
const options = program.opts()
if (options.check) {
checkConfig().then(() => {
console.log('Config: OK')
}).catch((err) => {
console.log('Config: NOT OK')
console.error(err)
process.exit(1)
})
} else {
if (user !== 'root') { if (user !== 'root') {
console.error(`Please run as root! Current user: ${user}`) console.error(`Please run as root! Current user: ${user}`);
process.exit(1) process.exit(1);
} }
shell.exec(`docker network create ${process.env.DOCKER_NETWORK} --driver overlay`, { silent: !options.debug }) shell.exec(`docker network create ${process.env.DOCKER_NETWORK} --driver overlay`, {
shell.exec('docker build -t coolify -f install/Dockerfile .') silent: !options.debug
});
shell.exec('docker build -t coolify -f install/Dockerfile .');
if (options.type === 'all') { if (options.type === 'all') {
shell.exec('docker stack rm coollabs-coolify', { silent: !options.debug }) shell.exec('docker stack rm coollabs-coolify', { silent: !options.debug });
} else if (options.type === 'coolify') { } else if (options.type === 'coolify') {
shell.exec('docker service rm coollabs-coolify_coolify') shell.exec('docker service rm coollabs-coolify_coolify');
} else if (options.type === 'proxy') { } else if (options.type === 'proxy') {
shell.exec('docker service rm coollabs-coolify_proxy') shell.exec('docker service rm coollabs-coolify_proxy');
} }
if (options.type !== 'upgrade') { if (options.type !== 'upgrade') {
shell.exec('set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify', { silent: !options.debug, shell: '/bin/bash' }) shell.exec(
} 'set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify',
} { silent: !options.debug, shell: '/bin/bash' }
);
function checkConfig () {
return new Promise((resolve, reject) => {
fastify.register(require('fastify-env'), {
schema,
dotenv: true
})
.ready((err) => {
if (err) reject(err)
resolve()
})
})
} }

View File

@@ -1,4 +0,0 @@
FROM coolify-base-nodejs
WORKDIR /usr/src/app
COPY . .
RUN pnpm install

View File

@@ -1,6 +0,0 @@
FROM node:lts
LABEL coolify-preserve=true
COPY --from=coolify-binaries /usr/bin/docker /usr/bin/docker
COPY --from=coolify-binaries /usr/bin/envsubst /usr/bin/envsubst
COPY --from=coolify-binaries /usr/bin/jq /usr/bin/jq
RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm@6

View File

@@ -1,9 +0,0 @@
FROM ubuntu:20.04
LABEL coolify-preserve=true
RUN apt update && apt install -y curl gnupg2 ca-certificates
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
RUN echo 'deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable' >> /etc/apt/sources.list
RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o /usr/bin/envsubst
RUN curl -L https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o /usr/bin/jq
RUN chmod +x /usr/bin/envsubst /usr/bin/jq
RUN apt update && apt install -y docker-ce-cli && apt clean all

View File

@@ -1,21 +1,24 @@
require('dotenv').config() require('dotenv').config();
const { program } = require('commander') const { program } = require('commander');
const shell = require('shelljs') const shell = require('shelljs');
const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '') const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '');
program.version('0.0.1') program.version('0.0.1');
program program
.option('-d, --debug', 'Debug outputs.') .option('-d, --debug', 'Debug outputs.')
.option('-c, --check', 'Only checks configuration.') .option('-c, --check', 'Only checks configuration.')
.option('-t, --type <type>', 'Deploy type.') .option('-t, --type <type>', 'Deploy type.');
program.parse(process.argv) program.parse(process.argv);
const options = program.opts() const options = program.opts();
if (user !== 'root') { if (user !== 'root') {
console.error(`Please run as root! Current user: ${user}`) console.error(`Please run as root! Current user: ${user}`);
process.exit(1) process.exit(1);
} }
if (options.type === 'upgrade') { if (options.type === 'upgrade') {
shell.exec('docker service rm coollabs-coolify_coolify') shell.exec('docker service rm coollabs-coolify_coolify');
shell.exec('set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify', { silent: !options.debug, shell: '/bin/bash' }) shell.exec(
'set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify',
{ silent: !options.debug, shell: '/bin/bash' }
);
} }

View File

@@ -1,68 +1,58 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.", "description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
"version": "1.0.11", "version": "1.0.13",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"lint": "standard", "dev:docker:start": "docker-compose -f docker-compose-dev.yml up -d",
"start": "NODE_ENV=production node api/server", "dev:docker:stop": "docker-compose -f docker-compose-dev.yml down",
"dev": "run-p dev:db dev:routify dev:svite dev:server", "dev": "NODE_ENV=development svelte-kit dev --host 0.0.0.0",
"dev:db": "NODE_ENV=development node api/development/mongodb.js", "build": "NODE_ENV=production svelte-kit build",
"dev:server": "nodemon -w api api/server", "preview": "svelte-kit preview",
"dev:routify": "routify run", "start": "node build",
"dev:svite": "svite", "lint": "prettier --check . && eslint --ignore-path .gitignore .",
"build": "run-s build:routify build:svite", "format": "prettier --write ."
"build:routify": "routify run -b",
"build:svite": "svite build"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@roxi/routify": "^2.15.1",
"@zerodevx/svelte-toast": "^0.2.2",
"ajv": "^8.1.0",
"axios": "^0.21.1",
"commander": "^7.2.0",
"compare-versions": "^3.6.0",
"cuid": "^2.1.8",
"dayjs": "^1.10.4",
"deepmerge": "^4.2.2",
"dockerode": "^3.2.1",
"dotenv": "^8.2.0",
"fastify": "^3.14.2",
"fastify-env": "^2.1.0",
"fastify-jwt": "^2.4.0",
"fastify-plugin": "^3.0.0",
"fastify-static": "^4.0.1",
"generate-password": "^1.6.0",
"http-errors-enhanced": "^0.7.0",
"js-yaml": "^4.0.0",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.12.3",
"shelljs": "^0.8.4",
"svelte-select": "^3.17.0",
"unique-names-generator": "^4.4.0"
}, },
"devDependencies": { "devDependencies": {
"mongodb-memory-server-core": "^6.9.6", "@sveltejs/adapter-node": "^1.0.0-next.20",
"nodemon": "^2.0.7", "@sveltejs/kit": "1.0.0-next.107",
"npm-run-all": "^4.1.5", "@types/dockerode": "^3.2.3",
"postcss": "^8.2.9", "@typescript-eslint/eslint-plugin": "^4.23.0",
"postcss-import": "^14.0.1", "@typescript-eslint/parser": "^4.23.0",
"autoprefixer": "^10.2.5",
"cssnano": "^5.0.2",
"dotenv-extended": "^2.9.0",
"eslint": "^7.26.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^3.2.0",
"postcss": "^8.2.15",
"postcss-load-config": "^3.0.1", "postcss-load-config": "^3.0.1",
"postcss-preset-env": "^6.7.0", "prettier": "~2.3.0",
"prettier": "2.2.1", "prettier-plugin-svelte": "^2.3.0",
"prettier-plugin-svelte": "^2.2.0", "svelte": "^3.38.2",
"standard": "^16.0.3", "svelte-preprocess": "^4.7.3",
"svelte": "^3.37.0", "tailwindcss": "2.2.0-canary.8",
"svelte-hmr": "^0.14.0", "tslib": "^2.2.0",
"svelte-preprocess": "^4.7.0", "typescript": "^4.2.4",
"svite": "0.8.1", "vite": "^2.3.2"
"tailwindcss": "2.1.1"
}, },
"keywords": [ "type": "module",
"svelte", "dependencies": {
"routify", "@iarna/toml": "^2.2.5",
"fastify", "@zerodevx/svelte-toast": "^0.3.0",
"tailwind" "commander": "^7.2.0",
] "compare-versions": "^3.6.0",
"cookie": "^0.4.1",
"cuid": "^2.1.8",
"dayjs": "^1.10.4",
"dockerode": "^3.3.0",
"generate-password": "^1.6.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.12.9",
"shelljs": "^0.8.4",
"svelte-kit-cookie-session": "^0.4.3",
"svelte-select": "^3.17.0",
"unique-names-generator": "^4.5.0"
}
} }

6818
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

18
postcss.config.cjs Normal file
View File

@@ -0,0 +1,18 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
module.exports = {
plugins: [
tailwindcss,
autoprefixer,
!dev &&
cssnano({
preset: 'default'
})
]
};

View File

@@ -1,7 +0,0 @@
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss'),
require('postcss-preset-env')({ stage: 1 })
]
}

View File

@@ -1,3 +0,0 @@
<script>
window.close();
</script>

View File

@@ -1,5 +0,0 @@
module.exports = {
routifyDir: '.routify',
dynamicImports: true,
extensions: ['svelte']
}

View File

@@ -1,82 +0,0 @@
<script>
import { SvelteToast } from "@zerodevx/svelte-toast";
import { Router } from "@roxi/routify";
import { routes } from "../.routify/routes";
const options = {
duration: 2000
};
</script>
<style lang="postcss">
:global(.main) {
width: calc(100% - 4rem);
margin-left: 4rem;
}
:global(._toastMsg) {
@apply text-sm font-bold !important;
}
:global(._toastItem) {
@apply w-full border-l-2 border-green-600 !important;
}
:global(._toastBtn) {
@apply text-xs !important;
}
:global(._toastBtn:hover) {
@apply bg-gray-500 !important;
}
:global(.icon) {
@apply text-white rounded p-2 transition duration-100 !important;
}
:global(.icon:hover) {
@apply bg-warmGray-700 !important;
}
:global(input) {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
}
:global(input:hover) {
@apply bg-warmGray-700 !important;
}
:global(textarea) {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none resize-none !important;
}
:global(textarea:hover) {
@apply bg-warmGray-700 !important;
}
:global(select) {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none !important;
}
:global(select:hover) {
@apply bg-warmGray-700 !important;
}
:global(label) {
@apply text-left text-base font-bold text-warmGray-400 !important;
}
:global(button) {
@apply outline-none !important;
}
:global(.button) {
@apply rounded text-sm font-bold transition-all duration-100 !important;
}
:global(.h-271) {
min-height: 271px !important;
}
:global(.repository-select-search .listItem .item),
:global(.repository-select-search .empty) {
@apply text-sm py-3 font-bold bg-warmGray-800 text-white cursor-pointer border-none hover:bg-warmGray-700 !important;
}
:global(.repository-select-search .listContainer) {
@apply bg-transparent !important;
}
:global(.repository-select-search .clearSelect) {
@apply text-white cursor-pointer !important;
}
:global(.repository-select-search .selectedItem) {
@apply text-white relative cursor-pointer font-bold text-sm flex items-center !important;
}
</style>
<SvelteToast options="{options}" />
<Router routes="{routes}" />

17
src/app.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coolify</title>
<link rel="dns-prefetch" href="https://cdn.coollabs.io/" />
<link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" />
<link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" />
<link rel="stylesheet" href="https://cdn.coollabs.io/css/microtip-0.2.2.min.css" />
%svelte.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>

141
src/app.postcss Normal file
View File

@@ -0,0 +1,141 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
height: 100%;
}
body {
background-color: rgb(22, 22, 22);
min-height: 100vh;
overflow-x: hidden;
@apply text-white;
}
:root {
--toastBackground: rgba(41, 37, 36, 0.8);
--toastProgressBackground: transparent;
--toastFont: 'Inter';
}
.border-gradient {
border-bottom: 2px solid transparent;
border-image: linear-gradient(
0.25turn,
rgba(255, 249, 34),
rgba(255, 0, 128),
rgba(56, 2, 155, 0)
);
border-image-slice: 1;
}
.border-gradient-full {
border: 4px solid transparent;
border-image: linear-gradient(
0.25turn,
rgba(255, 249, 34),
rgba(255, 0, 128),
rgba(56, 2, 155, 0)
);
border-image-slice: 1;
}
[aria-label][role~='tooltip']::after {
background: rgba(41, 37, 36, 0.9);
color: white;
font-family: 'Inter';
font-size: 16px;
font-weight: 600;
white-space: normal;
}
[role~='tooltip'][data-microtip-position|='bottom']::before {
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
no-repeat;
}
[role~='tooltip'][data-microtip-position|='top']::before {
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
no-repeat;
}
[role~='tooltip'][data-microtip-position='right']::before {
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
no-repeat;
}
[role~='tooltip'][data-microtip-position='left']::before {
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
no-repeat;
}
.main {
width: calc(100% - 4rem);
margin-left: 4rem;
}
._toastMsg {
@apply text-sm font-bold !important;
}
._toastItem {
@apply w-full border-l-2 border-green-600 !important;
}
._toastBtn {
@apply text-xs !important;
}
._toastBtn:hover {
@apply bg-gray-500 !important;
}
.icon {
@apply text-white rounded p-2 transition duration-100;
}
.icon:hover {
@apply bg-warmGray-700;
}
input {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
}
input:hover {
@apply bg-warmGray-700 !important;
}
textarea {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none resize-none !important;
}
textarea:hover {
@apply bg-warmGray-700 !important;
}
select {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none !important;
}
select:hover {
@apply bg-warmGray-700 !important;
}
label {
@apply text-left text-base font-bold text-warmGray-400 !important;
}
button {
@apply outline-none !important;
}
.button {
@apply rounded text-sm font-bold transition-all duration-100 !important;
}
.h-271 {
min-height: 271px !important;
}
.repository-select-search .listItem .item,
.repository-select-search .empty {
@apply text-sm py-3 font-bold bg-warmGray-800 text-white cursor-pointer border-none hover:bg-warmGray-700 !important;
}
.repository-select-search .listContainer {
@apply bg-transparent !important;
}
.repository-select-search .clearSelect {
@apply text-white cursor-pointer !important;
}
.repository-select-search .selectedItem {
@apply text-white relative cursor-pointer font-bold text-sm flex items-center !important;
}
.selectContainer {
background: transparent !important;
@apply border-0 !important;
}

View File

@@ -0,0 +1,357 @@
<script>
import { application } from '$store';
import { onMount } from 'svelte';
import TooltipInfo from '$components/TooltipInfo.svelte';
let domainInput;
const buildpacks = {
static: {
port: {
active: false,
number: 80
},
build: true,
start: false
},
nodejs: {
port: {
active: true,
number: 3000
},
build: true,
start: true
},
nestjs: {
port: {
active: true,
number: 3000
},
build: true,
start: true
},
vuejs: {
port: {
active: false,
number: 80
},
build: true,
start: false
},
nuxtjs: {
port: {
active: true,
number: 3000
},
build: true,
start: true
},
react: {
port: {
active: false,
number: 80
},
build: true,
start: false
},
nextjs: {
port: {
active: true,
number: 3000
},
build: true,
start: true
},
gatsby: {
port: {
active: true,
number: 3000
},
build: true,
start: false
},
svelte: {
port: {
active: false,
number: 80
},
build: true,
start: false
},
php: {
port: {
active: false,
number: 80
},
build: false,
start: false
},
rust: {
port: {
active: true,
number: 3000
},
build: false,
start: false
},
docker: {
port: {
active: true,
number: 3000
},
build: false,
start: false
}
};
function selectBuildPack(event) {
if (event.target.innerText === 'React/Preact') {
$application.build.pack = 'react';
} else {
$application.build.pack = event.target.innerText.replace(/\./g, '').toLowerCase();
}
}
onMount(() => {
if (!$application.publish.domain) domainInput.focus();
});
</script>
<div>
<div class="grid grid-cols-1 text-sm max-w-4xl md:mx-auto mx-6 pb-16 auto-cols-max ">
<div class="text-2xl font-bold border-gradient w-40">Build Packs</div>
<div class="flex font-bold flex-wrap justify-center pt-10">
<div
class={$application.build.pack === 'static'
? 'buildpack bg-red-500'
: 'buildpack hover:border-red-500'}
on:click={selectBuildPack}
>
Static
</div>
<div
class={$application.build.pack === 'nodejs'
? 'buildpack bg-emerald-600'
: 'buildpack hover:border-emerald-600'}
on:click={selectBuildPack}
>
NodeJS
</div>
<div
class={$application.build.pack === 'vuejs'
? 'buildpack bg-green-500'
: 'buildpack hover:border-green-500'}
on:click={selectBuildPack}
>
VueJS
</div>
<div
class={$application.build.pack === 'nuxtjs'
? 'buildpack bg-green-500'
: 'buildpack hover:border-green-500'}
on:click={selectBuildPack}
>
NuxtJS
</div>
<div
class={$application.build.pack === 'react'
? 'buildpack bg-gradient-to-r from-blue-500 to-purple-500'
: 'buildpack hover:border-blue-500'}
on:click={selectBuildPack}
>
React/Preact
</div>
<div
class={$application.build.pack === 'nextjs'
? 'buildpack bg-blue-500'
: 'buildpack hover:border-blue-500'}
on:click={selectBuildPack}
>
NextJS
</div>
<div
class={$application.build.pack === 'gatsby'
? 'buildpack bg-blue-500'
: 'buildpack hover:border-blue-500'}
on:click={selectBuildPack}
>
Gatsby
</div>
<div
class={$application.build.pack === 'svelte'
? 'buildpack bg-orange-600'
: 'buildpack hover:border-orange-600'}
on:click={selectBuildPack}
>
Svelte
</div>
<div
class={$application.build.pack === 'php'
? 'buildpack bg-indigo-500'
: 'buildpack hover:border-indigo-500'}
on:click={selectBuildPack}
>
PHP
</div>
<div
class={$application.build.pack === 'rust'
? 'buildpack bg-pink-500'
: 'buildpack hover:border-pink-500'}
on:click={selectBuildPack}
>
Rust
</div>
<div
class={$application.build.pack === 'nestjs'
? 'buildpack bg-red-500'
: 'buildpack hover:border-red-500'}
on:click={selectBuildPack}
>
NestJS
</div>
<div
class={$application.build.pack === 'docker'
? 'buildpack bg-purple-500'
: 'buildpack hover:border-purple-500'}
on:click={selectBuildPack}
>
Docker
</div>
</div>
</div>
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
<div class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10">
<div class="grid grid-flow-col gap-2 items-center pb-6">
<div class="grid grid-flow-row">
<label for="Domain" class="">Domain</label>
<input
bind:this={domainInput}
class="border-2"
class:placeholder-red-500={$application.publish.domain == null ||
$application.publish.domain == ''}
class:border-red-500={$application.publish.domain == null ||
$application.publish.domain == ''}
id="Domain"
bind:value={$application.publish.domain}
placeholder="eg: coollabs.io (without www)"
/>
</div>
<div class="grid grid-flow-row">
<label for="Path"
>Path <TooltipInfo
label={`Path to deploy your application on your domain. eg: /api means it will be deployed to -> https://${
$application.publish.domain || '<yourdomain>'
}/api`}
/></label
>
<input id="Path" bind:value={$application.publish.path} placeholder="/" />
</div>
</div>
<label for="Port" class:text-warmGray-800={!buildpacks[$application.build.pack].port.active}
>Port</label
>
<input
disabled={!buildpacks[$application.build.pack].port.active}
id="Port"
class:bg-warmGray-900={!buildpacks[$application.build.pack].port.active}
class:text-warmGray-900={!buildpacks[$application.build.pack].port.active}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].port.active}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].port.active}
class:cursor-not-allowed={!buildpacks[$application.build.pack].port.active}
bind:value={$application.publish.port}
placeholder={buildpacks[$application.build.pack].port.number}
/>
<div class="grid grid-flow-col gap-2 items-center pt-6 pb-12">
<div class="grid grid-flow-row">
<label for="baseDir"
>Base Directory <TooltipInfo
label="The directory to use as base for every command (could be useful if you have a monorepo)."
/></label
>
<input id="baseDir" bind:value={$application.build.directory} placeholder="eg: sourcedir" />
</div>
<div class="grid grid-flow-row">
<label for="publishDir"
>Publish Directory <TooltipInfo
label="The directory to deploy after running the build command. eg: dist, _site, public."
/></label
>
<input
id="publishDir"
bind:value={$application.publish.directory}
placeholder="eg: dist, _site, public"
/>
</div>
</div>
</div>
<div
class="text-2xl font-bold w-40"
class:border-gradient={buildpacks[$application.build.pack].build}
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>
Commands
</div>
<div class=" max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10 pb-32">
<div class="grid grid-flow-col gap-2 items-center">
<div class="grid grid-flow-row">
<label
for="installCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>Install Command <TooltipInfo
label="Command to run for installing dependencies. eg: yarn install"
/>
</label>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
id="installCommand"
bind:value={$application.build.command.installation}
placeholder="eg: yarn install"
/>
<label
for="buildCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>Build Command <TooltipInfo
label="Command to run for building your application. If empty, no build phase initiated in the deploy process."
/></label
>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
id="buildCommand"
bind:value={$application.build.command.build}
placeholder="eg: yarn build"
/>
<label
for="startCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].start}
>Start Command <TooltipInfo label="Command to start the application. eg: yarn start" /></label
>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].start}
class:text-warmGray-900={!buildpacks[$application.build.pack].start}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].start}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].start}
class:cursor-not-allowed={!buildpacks[$application.build.pack].start}
id="startcommand"
bind:value={$application.build.command.start}
placeholder="eg: yarn start"
/>
</div>
</div>
</div>
</div>
<style lang="postcss">
.buildpack {
@apply px-6 py-2 mx-2 my-2 bg-warmGray-800 w-48 ease-in-out transform hover:scale-105 text-center rounded border-2 border-transparent border-dashed cursor-pointer transition duration-100;
}
</style>

View File

@@ -1,5 +1,5 @@
<script> <script>
import { application } from "@store"; import { application } from "$store";
let secret = { let secret = {
name: null, name: null,

View File

@@ -0,0 +1,46 @@
<script>
import { page } from '$app/stores';
export let loading, branches;
import { application } from '$store';
import Select from 'svelte-select';
const selectedValue = $page.path !== '/application/new' && $application.repository.branch;
function handleSelect(event) {
$application.repository.branch = null;
setTimeout(() => {
$application.repository.branch = event.detail.value;
}, 1);
}
</script>
{#if loading}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<div class="repository-select-search col-span-2">
<Select
containerClasses="w-full border-none bg-transparent"
placeholder="Loading branches..."
isDisabled
/>
</div>
</div>
{:else}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<div class="repository-select-search col-span-2">
<Select
containerClasses="w-full border-none bg-transparent"
on:select={handleSelect}
{selectedValue}
isClearable={false}
items={branches.map((b) => ({ label: b.name, value: b.name }))}
showIndicator={$page.path === '/application/new'}
noOptionsMessage="No branches found"
placeholder="Select a branch"
isDisabled={$page.path !== '/application/new'}
/>
</div>
</div>
{/if}

View File

@@ -0,0 +1,186 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { request } from '$lib/request';
import { session } from '$app/stores';
import { githubRepositories, application, githubInstallations } from '$store';
import { fade } from 'svelte/transition';
import Loading from '$components/Loading.svelte';
import { browser } from '$app/env';
import Branches from '$components/Application/Branches.svelte';
import Tabs from '$components/Application/Tabs.svelte';
import Repositories from '$components/Application/Repositories.svelte';
import Login from '$components/Application/Login.svelte';
let loading = {
github: false,
branches: false
};
let branches = [];
let relogin = false;
function dashify(str: string, options?: any) {
if (typeof str !== 'string') return str;
return str
.trim()
.replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-'))
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase();
}
async function getGithubRepos(id, page) {
return await request(
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
$session
);
}
async function loadGithubRepositories(force) {
if ($githubRepositories.length > 0 && !force) {
$application.github.installation.id = $githubInstallations.id;
$application.github.app.id = $githubInstallations.app_id;
const foundRepositoryOnGithub = $githubRepositories.find(
(r) =>
r.full_name === `${$application.repository.organization}/${$application.repository.name}`
);
if (foundRepositoryOnGithub) {
$application.repository.id = foundRepositoryOnGithub.id;
$application.repository.organization = foundRepositoryOnGithub.owner.login;
$application.repository.name = foundRepositoryOnGithub.name;
}
return;
} else {
loading.github = true;
let installations = [];
try {
const data = await request('https://api.github.com/user/installations', $session);
installations = data.installations;
} catch (error) {
relogin = true;
console.log(error);
return false;
}
if (installations.length === 0) {
relogin = true;
return false;
}
$application.github.installation.id = installations[0].id;
$application.github.app.id = installations[0].app_id;
$githubInstallations = installations[0];
try {
let page = 1;
let userRepos = 0;
const data = await getGithubRepos($application.github.installation.id, page);
$githubRepositories = $githubRepositories.concat(data.repositories);
userRepos = data.total_count;
if (userRepos > $githubRepositories.length) {
while (userRepos > $githubRepositories.length) {
page = page + 1;
const repos = await getGithubRepos($application.github.installation.id, page);
$githubRepositories = $githubRepositories.concat(repos.repositories);
}
}
const foundRepositoryOnGithub = $githubRepositories.find(
(r) =>
r.full_name ===
`${$application.repository.organization}/${$application.repository.name}`
);
if (foundRepositoryOnGithub) {
$application.repository.id = foundRepositoryOnGithub.id;
await loadBranches();
}
} catch (error) {
return false;
} finally {
loading.github = false;
}
}
}
async function loadBranches() {
loading.branches = true;
const selectedRepository = $githubRepositories.find((r) => r.id === $application.repository.id);
if (selectedRepository) {
$application.repository.organization = selectedRepository.owner.login;
$application.repository.name = selectedRepository.name;
}
branches = await request(
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/branches`,
$session
);
loading.branches = false;
}
async function modifyGithubAppConfig() {
if (browser) {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`https://github.com/apps/${dashify(
import.meta.env.VITE_GITHUB_APP_NAME
)}/installations/new`,
'Install App',
'resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(async () => {
if (newWindow.closed) {
clearInterval(timer);
loading.github = true;
if ($application.repository.name) {
try {
const config = await request(`/api/v1/application/config`, $session, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch
}
});
$application = { ...config };
} catch (error) {
browser && goto('/dashboard/applications', { replaceState: true });
}
}
branches = [];
$githubRepositories = [];
await loadGithubRepositories(true);
}
}, 100);
}
}
</script>
<div in:fade={{ duration: 100 }}>
{#if relogin}
<Login />
{:else}
{#await loadGithubRepositories(false)}
<Loading github githubLoadingText="Loading repositories..." />
{:then}
{#if loading.github}
<Loading github githubLoadingText="Loading repositories..." />
{:else}
<div class="space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
<Repositories
on:loadBranches={loadBranches}
on:modifyGithubAppConfig={modifyGithubAppConfig}
/>
{#if $application.repository.organization}
<Branches loading={loading.branches} {branches} />
{/if}
{#if $application.repository.branch}
<Tabs />
{/if}
</div>
{/if}
{/await}
{/if}
</div>

View File

@@ -1,340 +0,0 @@
<style lang="postcss">
.buildpack {
@apply px-6 py-2 mx-2 my-2 bg-warmGray-800 w-48 ease-in-out transform hover:scale-105 text-center rounded border-2 border-transparent border-dashed cursor-pointer transition duration-100;
}
</style>
<script>
import { application } from "@store";
import { onMount } from "svelte";
import TooltipInfo from "../../../Tooltip/TooltipInfo.svelte";
let domainInput;
const buildpacks = {
static: {
port: {
active: false,
number: 80,
},
build: true,
},
nodejs: {
port: {
active: true,
number: 3000,
},
build: true,
},
vuejs: {
port: {
active: false,
number: 80,
},
build: true,
},
nuxtjs: {
port: {
active: true,
number: 3000,
},
build: true,
},
react: {
port: {
active: false,
number: 80,
},
build: true,
},
nextjs: {
port: {
active: true,
number: 3000,
},
build: true,
},
gatsby: {
port: {
active: true,
number: 3000,
},
build: true,
},
svelte: {
port: {
active: false,
number: 80,
},
build: true,
},
php: {
port: {
active: false,
number: 80,
},
build: false,
},
rust: {
port: {
active: true,
number: 3000,
},
build: false,
},
docker: {
port: {
active: true,
number: 3000,
},
build: false,
},
};
function selectBuildPack(event) {
if (event.target.innerText === "React/Preact") {
$application.build.pack = "react";
} else {
$application.build.pack = event.target.innerText
.replace(/\./g, "")
.toLowerCase();
}
}
onMount(()=> {
domainInput.focus();
})
</script>
<div>
<div
class="grid grid-cols-1 text-sm max-w-4xl md:mx-auto mx-6 pb-16 auto-cols-max "
>
<div class="text-2xl font-bold border-gradient w-40">Build Packs</div>
<div class="flex font-bold flex-wrap justify-center pt-10">
<div
class="{$application.build.pack === 'static'
? 'buildpack bg-red-500'
: 'buildpack hover:border-red-500'}"
on:click="{selectBuildPack}"
>
Static
</div>
<div
class="{$application.build.pack === 'nodejs'
? 'buildpack bg-emerald-600'
: 'buildpack hover:border-emerald-600'}"
on:click="{selectBuildPack}"
>
NodeJS
</div>
<div
class="{$application.build.pack === 'vuejs'
? 'buildpack bg-green-500'
: 'buildpack hover:border-green-500'}"
on:click="{selectBuildPack}"
>
VueJS
</div>
<div
class="{$application.build.pack === 'nuxtjs'
? 'buildpack bg-green-500'
: 'buildpack hover:border-green-500'}"
on:click="{selectBuildPack}"
>
NuxtJS
</div>
<div
class="{$application.build.pack === 'react'
? 'buildpack bg-gradient-to-r from-blue-500 to-purple-500'
: 'buildpack hover:border-blue-500'}"
on:click="{selectBuildPack}"
>
React/Preact
</div>
<div
class="{$application.build.pack === 'nextjs'
? 'buildpack bg-blue-500'
: 'buildpack hover:border-blue-500'}"
on:click="{selectBuildPack}"
>
NextJS
</div>
<div
class="{$application.build.pack === 'gatsby'
? 'buildpack bg-blue-500'
: 'buildpack hover:border-blue-500'}"
on:click="{selectBuildPack}"
>
Gatsby
</div>
<div
class="{$application.build.pack === 'svelte'
? 'buildpack bg-orange-600'
: 'buildpack hover:border-orange-600'}"
on:click="{selectBuildPack}"
>
Svelte
</div>
<div
class="{$application.build.pack === 'php'
? 'buildpack bg-indigo-500'
: 'buildpack hover:border-indigo-500'}"
on:click="{selectBuildPack}"
>
PHP
</div>
<div
class="{$application.build.pack === 'rust'
? 'buildpack bg-pink-500'
: 'buildpack hover:border-pink-500'}"
on:click="{selectBuildPack}"
>
Rust
</div>
<div
class="{$application.build.pack === 'docker'
? 'buildpack bg-purple-500'
: 'buildpack hover:border-purple-500'}"
on:click="{selectBuildPack}"
>
Docker
</div>
</div>
</div>
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
<div
class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10"
>
<div class="grid grid-flow-col gap-2 items-center pb-6">
<div class="grid grid-flow-row">
<label for="Domain" class="">Domain</label>
<input
bind:this={domainInput}
class="border-2"
class:placeholder-red-500="{$application.publish.domain == null ||
$application.publish.domain == ''}"
class:border-red-500="{$application.publish.domain == null ||
$application.publish.domain == ''}"
id="Domain"
bind:value="{$application.publish.domain}"
placeholder="eg: coollabs.io (without www)"
/>
</div>
<div class="grid grid-flow-row">
<label for="Path"
>Path <TooltipInfo
label="{`Path to deploy your application on your domain. eg: /api means it will be deployed to -> https://${
$application.publish.domain || '<yourdomain>'
}/api`}"
/></label
>
<input
id="Path"
bind:value="{$application.publish.path}"
placeholder="/"
/>
</div>
</div>
<label
for="Port"
class:text-warmGray-800="{!buildpacks[$application.build.pack].port
.active}">Port</label
>
<input
disabled="{!buildpacks[$application.build.pack].port.active}"
id="Port"
class:bg-warmGray-900="{!buildpacks[$application.build.pack].port.active}"
class:text-warmGray-900="{!buildpacks[$application.build.pack].port
.active}"
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack].port
.active}"
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack].port
.active}"
class:cursor-not-allowed="{!buildpacks[$application.build.pack].port
.active}"
bind:value="{$application.publish.port}"
placeholder="{buildpacks[$application.build.pack].port.number}"
/>
<div class="grid grid-flow-col gap-2 items-center pt-6 pb-12">
<div class="grid grid-flow-row">
<label for="baseDir"
>Base Directory <TooltipInfo
label="The directory to use as base for every command (could be useful if you have a monorepo)."
/></label
>
<input
id="baseDir"
bind:value="{$application.build.directory}"
placeholder="eg: sourcedir"
/>
</div>
<div class="grid grid-flow-row">
<label for="publishDir"
>Publish Directory <TooltipInfo
label="The directory to deploy after running the build command. eg: dist, _site, public."
/></label
>
<input
id="publishDir"
bind:value="{$application.publish.directory}"
placeholder="eg: dist, _site, public"
/>
</div>
</div>
</div>
<div
class="text-2xl font-bold w-40"
class:border-gradient="{buildpacks[$application.build.pack].build}"
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
>
Commands
</div>
<div
class=" max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10 pb-32"
>
<div class="grid grid-flow-col gap-2 items-center">
<div class="grid grid-flow-row">
<label
for="installCommand"
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
>Install Command <TooltipInfo
label="Command to run for installing dependencies. eg: yarn install."
/>
</label>
<input
class="mb-6"
class:bg-warmGray-900="{!buildpacks[$application.build.pack].build}"
class:text-warmGray-900="{!buildpacks[$application.build.pack].build}"
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack]
.build}"
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack]
.build}"
class:cursor-not-allowed="{!buildpacks[$application.build.pack]
.build}"
id="installCommand"
bind:value="{$application.build.command.installation}"
placeholder="eg: yarn install"
/>
<label
for="buildCommand"
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
>Build Command <TooltipInfo
label="Command to run for building your application. If empty, no build phase initiated in the deploy process."
/></label
>
<input
class="mb-6"
class:bg-warmGray-900="{!buildpacks[$application.build.pack].build}"
class:text-warmGray-900="{!buildpacks[$application.build.pack].build}"
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack]
.build}"
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack]
.build}"
class:cursor-not-allowed="{!buildpacks[$application.build.pack]
.build}"
id="buildCommand"
bind:value="{$application.build.command.build}"
placeholder="eg: yarn build"
/>
</div>
</div>
</div>
</div>

View File

@@ -1,45 +0,0 @@
<script>
export let loading, branches;
import { application, activePage } from "@store";
import Select from "svelte-select";
const selectedValue =
$activePage.application !== "new" && $application.repository.branch;
function handleSelect(event) {
$application.repository.branch = null;
setTimeout(() => {
$application.repository.branch = event.detail.value;
}, 1);
}
</script>
{#if loading}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<div class="repository-select-search col-span-2">
<Select
containerClasses="w-full border-none bg-transparent"
placeholder="Loading branches..."
isDisabled
/>
</div>
</div>
{:else}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<div class="repository-select-search col-span-2">
<Select
containerClasses="w-full border-none bg-transparent"
on:select="{handleSelect}"
selectedValue="{selectedValue}"
isClearable="{false}"
items="{branches.map(b => ({ label: b.name, value: b.name }))}"
showIndicator="{$activePage.new}"
noOptionsMessage="No branches found"
placeholder="Select a branch"
isDisabled="{!$activePage.new}"
/>
</div>
</div>
{/if}

View File

@@ -1,263 +0,0 @@
<script>
import { redirect, isActive } from "@roxi/routify";
import { fade } from "svelte/transition";
import {
session,
application,
fetch,
initialApplication,
githubRepositories,
githubInstallations,
activePage,
} from "@store";
import Login from "./Login.svelte";
import Loading from "../../Loading.svelte";
import Repositories from "./Repositories.svelte";
import Branches from "./Branches.svelte";
import Tabs from "./Tabs.svelte";
let loading = {
branches: false,
github: false,
};
let branches = [];
function dashify(str, options) {
if (typeof str !== "string") return str;
return str
.trim()
.replace(/\W/g, m => (/[À-ž]/.test(m) ? m : "-"))
.replace(/^-+|-+$/g, "")
.replace(/-{2,}/g, m => (options && options.condense ? "-" : m))
.toLowerCase();
}
async function loadBranches() {
loading.branches = true;
if ($activePage.new) $application.repository.branch = null;
const selectedRepository = $githubRepositories.find(
r => r.id === $application.repository.id,
);
if (selectedRepository) {
$application.repository.organization = selectedRepository.owner.login;
$application.repository.name = selectedRepository.name;
}
branches = await $fetch(
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/branches`,
);
loading.branches = false;
}
async function getGithubRepos(id, page) {
const data = await $fetch(
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
);
return data;
}
async function loadGithub() {
if ($githubRepositories.length > 0) {
$application.github.installation.id = $githubInstallations.id;
$application.github.app.id = $githubInstallations.app_id;
const foundRepositoryOnGithub = $githubRepositories.find(
r =>
r.full_name ===
`${$application.repository.organization}/${$application.repository.name}`,
);
if (foundRepositoryOnGithub) {
$application.repository.id = foundRepositoryOnGithub.id;
$application.repository.organization = foundRepositoryOnGithub.owner.login;
$application.repository.name = foundRepositoryOnGithub.name;
// await loadBranches();
}
return;
}
loading.github = true;
try {
const { installations } = await $fetch(
"https://api.github.com/user/installations",
);
if (installations.length === 0) {
return false;
}
$application.github.installation.id = installations[0].id;
$application.github.app.id = installations[0].app_id;
$githubInstallations = installations[0];
let page = 1;
let userRepos = 0;
const data = await getGithubRepos(
$application.github.installation.id,
page,
);
$githubRepositories = $githubRepositories.concat(data.repositories);
userRepos = data.total_count;
if (userRepos > $githubRepositories.length) {
while (userRepos > $githubRepositories.length) {
page = page + 1;
const repos = await getGithubRepos(
$application.github.installation.id,
page,
);
$githubRepositories = $githubRepositories.concat(repos.repositories);
}
}
const foundRepositoryOnGithub = $githubRepositories.find(
r =>
r.full_name ===
`${$application.repository.organization}/${$application.repository.name}`,
);
if (foundRepositoryOnGithub) {
$application.repository.id = foundRepositoryOnGithub.id;
await loadBranches();
}
} catch (error) {
return false;
} finally {
loading.github = false;
}
}
function modifyGithubAppConfig() {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`https://github.com/apps/${dashify(
import.meta.env.VITE_GITHUB_APP_NAME,
)}/installations/new`,
"Install App",
"resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=" +
top +
", left=" +
left +
", toolbar=0, menubar=0, status=0",
);
const timer = setInterval(async () => {
if (newWindow.closed) {
clearInterval(timer);
loading.github = true;
if (!$activePage.new) {
try {
const config = await $fetch(`/api/v1/config`, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
},
});
$application = { ...config };
} catch (error) {
$redirect("/dashboard/applications");
}
} else {
$application = JSON.parse(JSON.stringify(initialApplication));
}
branches = [];
$githubRepositories = [];
await loadGithub();
}
}, 100);
}
</script>
{#if !$activePage.new}
<div class="min-h-full text-white">
<div
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
>
{$application.publish.domain
? `${$application.publish.domain}${
$application.publish.path !== "/" ? $application.publish.path : ""
}`
: "example.com"}
<a
target="_blank"
class="icon mx-2"
href="{'https://' +
$application.publish.domain +
$application.publish.path}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
></path>
</svg></a
>
<a
target="_blank"
class="icon"
href="{`https://github.com/${$application.repository.organization}/${$application.repository.name}`}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
></path></svg
></a
>
</div>
</div>
{:else if $activePage.new}
<div class="min-h-full text-white">
<div
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
>
New Application
</div>
</div>
{/if}
<div in:fade="{{ duration: 100 }}">
{#if !$session.githubAppToken}
<Login />
{:else}
{#await loadGithub()}
<Loading github githubLoadingText="Loading repositories..." />
{:then}
{#if loading.github}
<Loading github githubLoadingText="Loading repositories..." />
{:else}
<div
class="space-y-2 max-w-4xl mx-auto px-6"
in:fade="{{ duration: 100 }}"
>
<Repositories
on:loadBranches="{loadBranches}"
on:modifyGithubAppConfig="{modifyGithubAppConfig}"
/>
{#if $application.repository.organization}
<Branches loading="{loading.branches}" branches="{branches}" />
{/if}
{#if $application.repository.branch}
<Tabs />
{/if}
</div>
{/if}
{/await}
{/if}
</div>

View File

@@ -1,50 +0,0 @@
<script>
import { session } from "@store";
function login() {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`https://github.com/login/oauth/authorize?client_id=${
import.meta.env.VITE_GITHUB_APP_CLIENTID
}`,
"Authenticate",
"resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=" +
top +
", left=" +
left +
", toolbar=0, menubar=0, status=0",
);
const timer = setInterval(() => {
if (newWindow.closed) {
clearInterval(timer);
const ghToken = new URL(newWindow.document.URL).searchParams.get(
"ghToken",
);
if (ghToken) {
$session.githubAppToken = ghToken;
}
}
}, 100);
}
</script>
<div class="text-center text-white">
<div class="text-2xl font-bold text-center pb-4">
Choose your Git provider
</div>
<button on:click="{login}" class="hover:scale-110 transform duration-100 transition">
<svg
class="w-16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
></path></svg
>
</button>
</div>

View File

@@ -1,53 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import { application, githubRepositories, activePage } from "@store";
import Select from "svelte-select";
function handleSelect(event) {
$application.build.pack = 'static'
$application.repository.id = parseInt(event.detail.value, 10);
dispatch("loadBranches");
}
let items = $githubRepositories.map(repo => ({
label: `${repo.owner.login}/${repo.name}`,
value: repo.id.toString(),
}));
const selectedValue =
!$activePage.new &&
`${$application.repository.organization}/${$application.repository.name}`;
const dispatch = createEventDispatcher();
const modifyGithubAppConfig = () => dispatch("modifyGithubAppConfig");
</script>
<div class="grid grid-cols-1 pt-4">
{#if $githubRepositories.length !== 0}
<label for="repository">Organization / Repository</label>
<div class="grid grid-cols-3 ">
<div class="repository-select-search col-span-2">
<Select
isFocused="true"
containerClasses="w-full border-none bg-transparent"
on:select="{handleSelect}"
selectedValue="{selectedValue}"
isClearable="{false}"
items="{items}"
showIndicator="{$activePage.new}"
noOptionsMessage="No Repositories found"
placeholder="Select a Repository"
isDisabled="{!$activePage.new}"
/>
</div>
<button
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
on:click="{modifyGithubAppConfig}">Configure on Github</button
>
</div>
{:else}
<button
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white py-2"
on:click="{modifyGithubAppConfig}">Add repositories on Github</button
>
{/if}
</div>

View File

@@ -1,153 +0,0 @@
<script>
import { redirect } from "@roxi/routify";
import { onMount } from "svelte";
import { toast } from "@zerodevx/svelte-toast";
import templates from "../../../utils/templates";
import { application, fetch, deployments, activePage } from "@store";
import General from "./ActiveTab/General.svelte";
import Secrets from "./ActiveTab/Secrets.svelte";
import Loading from "../../Loading.svelte";
let activeTab = {
general: true,
buildStep: false,
secrets: false,
};
function activateTab(tab) {
if (activeTab.hasOwnProperty(tab)) {
activeTab = {
general: false,
buildStep: false,
secrets: false,
};
activeTab[tab] = true;
}
}
async function load() {
const found = $deployments?.applications?.deployed.find(deployment => {
if (
deployment.configuration.repository.organization ===
$application.repository.organization &&
deployment.configuration.repository.name ===
$application.repository.name &&
deployment.configuration.repository.branch ===
$application.repository.branch
) {
return deployment;
}
});
if (found) {
$application = { ...found.configuration };
if ($activePage.new) {
$activePage.new = false;
toast.push(
"This repository & branch is already defined. Redirecting...",
);
$redirect(`/application/:organization/:name/:branch/configuration`, {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
});
}
return;
}
if (!$activePage.new) {
const config = await $fetch(`/api/v1/config`, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
},
});
$application = { ...config };
} else {
try {
const dir = await $fetch(
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/contents/?ref=${$application.repository.branch}`,
);
const packageJson = dir.find(
f => f.type === "file" && f.name === "package.json",
);
const Dockerfile = dir.find(
f => f.type === "file" && f.name === "Dockerfile",
);
const CargoToml = dir.find(
f => f.type === "file" && f.name === "Cargo.toml",
);
if (packageJson) {
const { content } = await $fetch(packageJson.git_url);
const packageJsonContent = JSON.parse(atob(content));
const checkPackageJSONContents = dep => {
return (
packageJsonContent?.dependencies?.hasOwnProperty(dep) ||
packageJsonContent?.devDependencies?.hasOwnProperty(dep)
);
};
Object.keys(templates).map(dep => {
if (checkPackageJSONContents(dep)) {
const config = templates[dep];
$application.build.pack = config.pack;
if (config.installation)
$application.build.command.installation = config.installation;
if (config.port) $application.publish.port = config.port;
if (config.directory)
$application.publish.directory = config.directory;
if (
packageJsonContent.scripts.hasOwnProperty("build") &&
config.build
) {
$application.build.command.build = config.build;
}
toast.push(`${config.name} detected. Default values set.`);
}
});
} else if (CargoToml) {
$application.build.pack = "rust";
toast.push(`Rust language detected. Default values set.`);
} else if (Dockerfile) {
$application.build.pack = "docker";
toast.push("Custom Dockerfile found. Build pack set to docker.");
}
} catch (error) {
// Nothing detected
}
}
}
</script>
{#await load()}
<Loading github githubLoadingText="Scanning repository..." />
{:then}
<div class="block text-center py-8">
<nav
class="flex space-x-4 justify-center font-bold text-md text-white"
aria-label="Tabs"
>
<div
on:click="{() => activateTab('general')}"
class:text-green-500="{activeTab.general}"
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
General
</div>
<div
on:click="{() => activateTab('secrets')}"
class:text-green-500="{activeTab.secrets}"
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
Secrets
</div>
</nav>
</div>
<div class="max-w-4xl mx-auto">
<div class="h-full">
{#if activeTab.general}
<General />
{:else if activeTab.secrets}
<Secrets />
{/if}
</div>
</div>
{/await}

View File

@@ -0,0 +1,42 @@
<script>
function login() {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`https://github.com/login/oauth/authorize?client_id=${
import.meta.env.VITE_GITHUB_APP_CLIENTID
}`,
'Authenticate',
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow.closed) {
clearInterval(timer);
location.reload()
}
}, 100);
}
</script>
<div class="text-center text-white">
<div class="text-2xl font-bold text-center pb-4">Choose your Git provider</div>
<button on:click={login} class="hover:scale-110 transform duration-100 transition">
<svg
class="w-16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
/></svg
>
</button>
</div>

View File

@@ -1,32 +1,24 @@
<script> <script>
import { params, goto, redirect } from "@roxi/routify"; import { application, initialApplication, initConf } from '$store';
import { import { onDestroy } from 'svelte';
application, import { toast } from '@zerodevx/svelte-toast';
fetch, import Tooltip from '$components/Tooltip.svelte';
initialApplication, import { request } from '$lib/request';
initConf, import { page, session } from '$app/stores';
activePage, import { goto } from '$app/navigation';
} from "@store"; import { browser } from '$app/env';
import { onDestroy } from "svelte";
import { toast } from "@zerodevx/svelte-toast";
import Tooltip from "../../components/Tooltip/Tooltip.svelte";
$application.repository.organization = $params.organization;
$application.repository.name = $params.name;
$application.repository.branch = $params.branch;
async function removeApplication() { async function removeApplication() {
await $fetch(`/api/v1/application/remove`, { await request(`/api/v1/application/remove`, $session, {
body: { body: {
organization: $params.organization, organization: $application.repository.organization,
name: $params.name, name: $application.repository.name,
branch: $params.branch, branch: $application.repository.branch
}, }
}); });
toast.push("Application removed."); browser && toast.push('Application removed.');
$application = JSON.parse(JSON.stringify(initialApplication)); $application = JSON.parse(JSON.stringify(initialApplication));
$redirect(`/dashboard/applications`); browser && goto(`/dashboard/applications`, { replaceState: true });
} }
onDestroy(() => { onDestroy(() => {
@@ -35,47 +27,44 @@
async function deploy() { async function deploy() {
try { try {
toast.push("Checking configuration."); browser && toast.push('Checking configuration.');
await $fetch(`/api/v1/application/check`, { await request(`/api/v1/application/check`, $session, {
body: $application, body: $application
});
const { nickname, name, deployId } = await request(`/api/v1/application/deploy`, $session, {
body: $application
}); });
const { nickname, name, deployId } = await $fetch(
`/api/v1/application/deploy`,
{
body: $application,
},
);
$application.general.nickname = nickname; $application.general.nickname = nickname;
$application.build.container.name = name; $application.build.container.name = name;
$application.general.deployId = deployId; $application.general.deployId = deployId;
$initConf = JSON.parse(JSON.stringify($application)); $initConf = JSON.parse(JSON.stringify($application));
toast.push("Application deployment queued."); if (browser) {
$redirect( toast.push('Application deployment queued.');
goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs/${$application.general.deployId}`, `/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs/${$application.general.deployId}`,
{ replaceState: true }
); );
}
} catch (error) { } catch (error) {
console.log(error); // console.log(error);
toast.push(error.error || error || "Ooops something went wrong."); // toast.push(error.error || error || 'Ooops something went wrong.');
} }
} }
</script> </script>
<nav <nav class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50">
class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50"
>
<Tooltip position="bottom" label="Deploy"> <Tooltip position="bottom" label="Deploy">
<button <button
disabled="{$application.publish.domain === '' || disabled={$application.publish.domain === '' || $application.publish.domain === null}
$application.publish.domain === null}" class:cursor-not-allowed={$application.publish.domain === '' ||
class:cursor-not-allowed="{$application.publish.domain === '' || $application.publish.domain === null}
$application.publish.domain === null}" class:hover:bg-green-500={$application.publish.domain}
class:hover:bg-green-500="{$application.publish.domain}" class:bg-green-600={$application.publish.domain}
class:bg-green-600="{$application.publish.domain}" class:hover:bg-transparent={!$application.publish.domain && $page.path === '/application/new'}
class:hover:bg-transparent="{$activePage.new}" class:text-warmGray-700={$application.publish.domain === '' ||
class:text-warmGray-700="{$application.publish.domain === '' || $application.publish.domain === null}
$application.publish.domain === null}"
class="icon" class="icon"
on:click="{deploy}" on:click={deploy}
> >
<svg <svg
class="w-6" class="w-6"
@@ -86,34 +75,28 @@
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
><polyline points="16 16 12 12 8 16"></polyline><line ><polyline points="16 16 12 12 8 16" /><line x1="12" y1="12" x2="12" y2="21" /><path
x1="12"
y1="12"
x2="12"
y2="21"></line><path
d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3" d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"
></path><polyline points="16 16 12 12 8 16"></polyline></svg /><polyline points="16 16 12 12 8 16" /></svg
> >
</button> </button>
</Tooltip> </Tooltip>
<Tooltip position="bottom" label="Delete"> <Tooltip position="bottom" label="Delete">
<button <button
disabled="{$application.publish.domain === '' || disabled={$application.publish.domain === '' ||
$application.publish.domain === null || $application.publish.domain === null ||
$activePage.new}" $page.path === '/application/new'}
class:cursor-not-allowed="{$application.publish.domain === '' || class:cursor-not-allowed={$application.publish.domain === '' ||
$application.publish.domain === null || $application.publish.domain === null ||
$activePage.new}" $page.path === '/application/new'}
class:hover:text-red-500="{$application.publish.domain && class:hover:text-red-500={$application.publish.domain && $page.path !== '/application/new'}
!$activePage.new}" class:hover:bg-warmGray-700={$application.publish.domain && $page.path !== '/application/new'}
class:hover:bg-warmGray-700="{$application.publish.domain && class:hover:bg-transparent={$page.path === '/application/new'}
!$activePage.new}" class:text-warmGray-700={$application.publish.domain === '' ||
class:hover:bg-transparent="{$activePage.new}"
class:text-warmGray-700="{$application.publish.domain === '' ||
$application.publish.domain === null || $application.publish.domain === null ||
$activePage.new}" $page.path === '/application/new'}
class="icon" class="icon"
on:click="{removeApplication}" on:click={removeApplication}
> >
<svg <svg
class="w-6" class="w-6"
@@ -127,25 +110,25 @@
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
></path> />
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
<div class="border border-warmGray-700 h-8"></div> <div class="border border-warmGray-700 h-8" />
<Tooltip position="bottom" label="Logs"> <Tooltip position="bottom" label="Logs">
<button <button
class="icon" class="icon"
class:text-warmGray-700="{$activePage.new}" class:text-warmGray-700={$page.path === '/application/new'}
disabled="{$activePage.new}" disabled={$page.path === '/application/new'}
class:hover:text-blue-400="{!$activePage.new}" class:hover:text-blue-400={$page.path !== '/application/new'}
class:hover:bg-transparent="{$activePage.new}" class:hover:bg-transparent={$page.path === '/application/new'}
class:cursor-not-allowed="{$activePage.new}" class:cursor-not-allowed={$page.path === '/application/new'}
class:text-blue-400="{$activePage.application === 'logs'}" class:text-blue-400={/logs\/*/.test($page.path)}
class:bg-warmGray-700="{$activePage.application === 'logs'}" class:bg-warmGray-700={/logs\/*/.test($page.path)}
on:click="{() => on:click={() =>
$goto( goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`, `/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`
)}" )}
> >
<svg <svg
class="w-6" class="w-6"
@@ -159,22 +142,22 @@
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
></path> />
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
<Tooltip position="bottom-left" label="Configuration"> <Tooltip position="bottom-left" label="Configuration">
<button <button
class="icon hover:text-yellow-400" class="icon hover:text-yellow-400"
disabled="{$activePage.new}" disabled={$page.path === '/application/new'}
class:text-yellow-400="{$activePage.application === 'configuration' || class:text-yellow-400={$page.path.endsWith('configuration') ||
$activePage.new}" $page.path === '/application/new'}
class:bg-warmGray-700="{$activePage.application === 'configuration' || class:bg-warmGray-700={$page.path.endsWith('configuration') ||
$activePage.new}" $page.path === '/application/new'}
on:click="{() => on:click={() =>
$goto( goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`, `/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`
)}" )}
> >
<svg <svg
class="w-6" class="w-6"
@@ -188,7 +171,7 @@
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
></path> />
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>

View File

@@ -0,0 +1,54 @@
<script>
import { createEventDispatcher } from 'svelte';
import { application, githubRepositories } from '$store';
import Select from 'svelte-select';
import { page } from '$app/stores';
function handleSelect(event) {
$application.build.pack = 'static';
$application.repository.id = parseInt(event.detail.value, 10);
$application.repository.branch = null
dispatch('loadBranches');
}
const path = $page.path === '/application/new';
let items = $githubRepositories.map((repo) => ({
label: `${repo.owner.login}/${repo.name}`,
value: repo.id.toString()
}));
const selectedValue =
!path && `${$application.repository.organization}/${$application.repository.name}`;
const dispatch = createEventDispatcher();
const modifyGithubAppConfig = () => dispatch('modifyGithubAppConfig');
</script>
<div class="grid grid-cols-1 pt-4">
{#if $githubRepositories.length !== 0}
<label for="repository">Organization / Repository</label>
<div class="grid grid-cols-3 ">
<div class="repository-select-search col-span-2">
<Select
isFocused="{true}"
containerClasses="w-full border-none bg-transparent"
on:select={handleSelect}
{selectedValue}
isClearable={false}
{items}
showIndicator={path}
noOptionsMessage="No Repositories found"
placeholder="Select a Repository"
isDisabled={!path}
/>
</div>
<button
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
on:click={modifyGithubAppConfig}>Configure on Github</button
>
</div>
{:else}
<button
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white py-2"
on:click={modifyGithubAppConfig}>Add repositories on Github</button
>
{/if}
</div>

View File

@@ -0,0 +1,146 @@
<script>
import { toast } from '@zerodevx/svelte-toast';
import templates from '$lib/api/applications/packs/templates';
import { application, dashboard } from '$store';
import General from '$components/Application/ActiveTab/General.svelte';
import Secrets from '$components/Application/ActiveTab/Secrets.svelte';
import Loading from '$components/Loading.svelte';
import { goto } from '$app/navigation';
import { page, session } from '$app/stores';
import { request } from '$lib/request';
import { browser } from '$app/env';
let activeTab = {
general: true,
buildStep: false,
secrets: false
};
function activateTab(tab) {
if (activeTab.hasOwnProperty(tab)) {
activeTab = {
general: false,
buildStep: false,
secrets: false
};
activeTab[tab] = true;
}
}
async function load() {
const found = $dashboard?.applications?.deployed.find((deployment) => {
if (
deployment.configuration.repository.organization === $application.repository.organization &&
deployment.configuration.repository.name === $application.repository.name &&
deployment.configuration.repository.branch === $application.repository.branch
) {
return deployment;
}
});
if (found) {
$application = { ...found.configuration };
if ($page.path === '/application/new') {
if (browser) {
toast.push('This repository & branch is already defined. Redirecting...');
goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
{ replaceState: true }
);
}
}
return;
}
if ($page.path !== '/application/new') {
const config = await request(`/api/v1/application/config`, $session, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch
}
});
$application = { ...config };
} else {
try {
const dir = await request(
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/contents/?ref=${$application.repository.branch}`,
$session
);
const packageJson = dir.find((f) => f.type === 'file' && f.name === 'package.json');
const Dockerfile = dir.find((f) => f.type === 'file' && f.name === 'Dockerfile');
const CargoToml = dir.find((f) => f.type === 'file' && f.name === 'Cargo.toml');
if (packageJson) {
const { content } = await request(packageJson.git_url, $session);
const packageJsonContent = JSON.parse(atob(content));
const checkPackageJSONContents = (dep) => {
return (
packageJsonContent?.dependencies?.hasOwnProperty(dep) ||
packageJsonContent?.devDependencies?.hasOwnProperty(dep)
);
};
Object.keys(templates).map((dep) => {
if (checkPackageJSONContents(dep)) {
const config = templates[dep];
$application.build.pack = config.pack;
if (config.installation) {
$application.build.command.installation = config.installation;
}
if (config.start) {
$application.build.command.start = config.start;
}
if (config.port) {
$application.publish.port = config.port;
}
if (config.directory) {
$application.publish.directory = config.directory;
}
if (packageJsonContent.scripts.hasOwnProperty('build') && config.build) {
$application.build.command.build = config.build;
}
browser && toast.push(`${config.name} detected. Default values set.`);
}
});
} else if (CargoToml) {
$application.build.pack = 'rust';
browser && toast.push(`Rust language detected. Default values set.`);
} else if (Dockerfile) {
$application.build.pack = 'docker';
browser && toast.push('Custom Dockerfile found. Build pack set to docker.');
}
} catch (error) {
// Nothing detected
}
}
}
</script>
{#await load()}
<Loading github githubLoadingText="Scanning repository..." />
{:then}
<div class="block text-center py-8">
<nav class="flex space-x-4 justify-center font-bold text-md text-white" aria-label="Tabs">
<div
on:click={() => activateTab('general')}
class:text-green-500={activeTab.general}
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
General
</div>
<div
on:click={() => activateTab('secrets')}
class:text-green-500={activeTab.secrets}
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
Secrets
</div>
</nav>
</div>
<div class="max-w-4xl mx-auto">
<div class="h-full">
{#if activeTab.general}
<General />
{:else if activeTab.secrets}
<Secrets />
{/if}
</div>
</div>
{/await}

View File

@@ -0,0 +1,136 @@
<script>
import { fade } from 'svelte/transition';
import { toast } from '@zerodevx/svelte-toast';
import MongoDb from './SVGs/MongoDb.svelte';
import Postgresql from './SVGs/Postgresql.svelte';
import Mysql from './SVGs/Mysql.svelte';
import CouchDb from './SVGs/CouchDb.svelte';
import Redis from './SVGs/Redis.svelte';
import { page, session } from '$app/stores';
import { goto } from '$app/navigation';
import { request } from '$lib/request';
import { browser } from '$app/env';
import Loading from '$components/Loading.svelte';
let type;
let defaultDatabaseName;
let loading = false;
async function deploy() {
try {
loading = true;
await request(`/api/v1/databases/deploy`, $session, {
body: {
type,
defaultDatabaseName
}
});
if (browser) {
toast.push('Database deployment queued.');
goto(`/dashboard/databases`, { replaceState: true });
}
} catch (error) {
console.log(error);
} finally {
loading = false;
}
}
</script>
{#if loading}
<Loading />
{:else}
<div class="text-center space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
{#if $page.path === '/database/new'}
<div class="flex justify-center space-x-4 font-bold pb-6">
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-green-600 p-2 rounded bg-warmGray-800 w-32"
class:border-green-600={type === 'mongodb'}
on:click={() => (type = 'mongodb')}
>
<div class="flex items-center justify-center my-2">
<MongoDb customClass="w-6" />
</div>
<div class="text-white">MongoDB</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-red-600 p-2 rounded bg-warmGray-800 w-32"
class:border-red-600={type === 'couchdb'}
on:click={() => (type = 'couchdb')}
>
<div class="flex items-center justify-center my-2">
<CouchDb customClass="w-12 text-red-600 fill-current" />
</div>
<div class="text-white">Couchdb</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-600 p-2 rounded bg-warmGray-800 w-32"
class:border-blue-600={type === 'postgresql'}
on:click={() => (type = 'postgresql')}
>
<div class="flex items-center justify-center my-2">
<Postgresql customClass="w-12" />
</div>
<div class="text-white">PostgreSQL</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-orange-600 p-2 rounded bg-warmGray-800 w-32"
class:border-orange-600={type === 'mysql'}
on:click={() => (type = 'mysql')}
>
<div class="flex items-center justify-center">
<Mysql customClass="w-10" />
</div>
<div class="text-white">MySQL</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-red-600 p-2 rounded bg-warmGray-800 w-32"
class:border-red-600={type === 'redis'}
on:click={() => (type = 'redis')}
>
<div class="flex items-center justify-center">
<Redis customClass="w-12" />
</div>
<div class="text-white">Redis</div>
</div>
<!-- <button
class="button bg-gray-500 p-2 text-white hover:bg-yellow-500 cursor-pointer w-32"
on:click="{() => (type = 'clickhouse')}"
class:bg-yellow-500="{type === 'clickhouse'}"
>
Clickhouse
</button> -->
</div>
{#if type}
<div class="flex justify-center space-x-4 items-center">
{#if type !== 'redis'}
<label for="defaultDB">Default database</label>
<input
id="defaultDB"
class="w-64"
placeholder="random"
bind:value={defaultDatabaseName}
/>
{/if}
<button
class:bg-green-600={type === 'mongodb'}
class:hover:bg-green-500={type === 'mongodb'}
class:bg-blue-600={type === 'postgresql'}
class:hover:bg-blue-500={type === 'postgresql'}
class:bg-orange-600={type === 'mysql'}
class:hover:bg-orange-500={type === 'mysql'}
class:bg-red-600={type === 'couchdb' || type === 'redis'}
class:hover:bg-red-500={type === 'couchdb' || type === 'redis'}
class:bg-yellow-500={type === 'clickhouse'}
class:hover:bg-yellow-400={type === 'clickhouse'}
class="button p-2 w-32 text-white"
on:click={deploy}>Deploy</button
>
</div>
{/if}
{/if}
</div>
{/if}

Some files were not shown because too many files have changed in this diff Show More