Compare commits

...

6 Commits

Author SHA1 Message Date
Andras Bacsai
cccb9a5fec v1.0.11 (#41)
Features: 
- Build packs for popular frontend frameworks. It will help to understand which build packs should be chosen.

Fixes:
- Github queries optimized.
- Save repositories to store (faster navigation).
- Remove unnecessary data on dashboard requests.
- Speed up static site builds with a lot.

UI:
- Redesign of the application deployment page.
- Redesign of database deployments page.
2021-04-30 22:43:21 +02:00
Andras Bacsai
b416e3ab3e v1.0.10 (#39)
Fixes: 
- Default Nuxt port changed to 3000 (thanks to @yurgeman).
- Cleanup old images effectively.
- More attempts on container startups (3->6).
- Always clean up server logs on restart/redeploy.
2021-04-26 11:45:04 +02:00
Andras Bacsai
e16b7d65d4 Update README.md 2021-04-23 13:55:40 +02:00
Andras Bacsai
3744c64459 v1.0.9 (#37)
Features:
- Integrated the first service: [Plausible Analytics](https://plausible.io)!

Fixes:
- UI/UX fixes and new designs
2021-04-22 23:48:29 +02:00
Andras Bacsai
f742c2a3e2 Quick fix for Docker files 2021-04-19 13:08:17 +02:00
Andras Bacsai
142b83cc13 v1.0.7 (#32)
New features:
- Automatic error reporting (enabled by default)
- Increase build times by leveraging docker build caches
- 
Fixes:
- Fix error handling
- Fix vue autodetect
- Custom dockerfile is not the default

Others:
- Cleanup `logs-servers` collection, because old errors are not standardized
- New Traefik proxy version
- Standardized directory configurations
2021-04-19 09:46:05 +02:00
92 changed files with 3452 additions and 1592 deletions

1
.gitignore vendored
View File

@@ -8,5 +8,4 @@ dist-ssr
yarn-error.log
api/development/console.log
.pnpm-debug.log
yarn.lock
.pnpm-store

123
README.md
View File

@@ -1,93 +1,64 @@
# About
https://andrasbacsai.com/farewell-netlify-and-heroku-after-3-days-of-coding
# Coolify
# Features
- Deploy your Node.js, static sites, PHP or any custom application (with custom Dockerfile) just by pushing code to git.
- Hassle-free installation and upgrade process.
- One-click MongoDB, MySQL, PostgreSQL, CouchDB deployments!
An open-source, hassle-free, self-hostable Heroku & Netlify alternative.
# Upcoming features
- Backups & monitoring.
- User analytics with privacy in mind.
- And much more (see [Roadmap](https://github.com/coollabsio/coolify/projects/1)).
## Demo
[Small video](https://cdn.coollabs.io/assets/coolify/video/coolify.webm)
# FAQ
Q: What is a buildpack?
A: It defines your application's final form.
`Static` means that it will be hosted as a static site.
`NodeJs` means that it will be started as a node application.
# Screenshots
[Login](https://coollabs.io/coolify/login.jpg)
[Applications](https://coollabs.io/coolify/applications.jpg)
[Databases](https://coollabs.io/coolify/databases.jpg)
[Configuration](https://coollabs.io/coolify/configuration.jpg)
[Settings](https://coollabs.io/coolify/settings.jpg)
[Logs](https://coollabs.io/coolify/logs.jpg)
# Getting Started
Automatically: `/bin/bash -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"`
Manually:
### Requirements before installation
- [Docker](https://docs.docker.com/engine/install/) version 20+
- Docker in [swarm mode enabled](https://docs.docker.com/engine/reference/commandline/swarm_init/) (should be set manually before installation)
- A [MongoDB](https://docs.mongodb.com/manual/installation/) instance.
- We have a [simple installation](https://github.com/coollabsio/infrastructure/tree/main/mongo) if you need one
- A configured DNS entry (see `.env.template`)
- [Github App](https://docs.github.com/en/developers/apps/creating-a-github-app)
- GitHub App name: could be anything weird
- Homepage URL: https://yourdomain
Identifying and authorizing users:
- Callback URL: https://yourdomain/api/v1/login/github/app
- Request user authorization (OAuth) during installation -> Check!
Webhook:
- Active -> Check!
- Webhook URL: https://yourdomain/api/v1/webhooks/deploy
- Webhook Secret: it should be super secret
Repository permissions:
- Contents: Read-only
- Metadata: Read-only
User permissions:
- Email: Read-only
## Installation
Subscribe to events:
- Push -> Check!
Installation is automated with the following command:
### Installation
- Clone this repository: `git clone git@github.com:coollabsio/coolify.git`
- Set `.env` (see `.env.template`)
- Installation: `bash install.sh all`
```bash
/bin/bash -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"
```
## Manual updating process (You probably never need to do this!)
### Update everything (proxy+coolify)
- `bash install.sh all`
## Features
You can deploy any of the following applications, databases and services easily.
### Update coolify only
- `bash install.sh coolify`
(constantly growing lists)
### Update proxy only
- `bash install.sh proxy`
### Applications
With Github integration
- Static sites
- NodeJS
- VueJS
- NuxtJS
- React/Preact
- NextJS
- Gatsby
- Svelte
- PHP
- Rust
- or any custom dockerfile
### Databases
- MongoDB
- MySQL
- PostgreSQL
- CouchDB
### Services
- [Plausible Analytics](https://plausible.io)
## Support
# Contact
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://discord.com/invite/bvS3WhR)
## Roadmap
[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.

View File

@@ -1,7 +1,7 @@
module.exports = async function (fastify, opts) {
// Private routes
fastify.register(async function (server) {
if (process.env.NODE_ENV === 'production') server.register(require('./plugins/authentication'))
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' })
@@ -12,6 +12,8 @@ module.exports = async function (fastify, opts) {
server.register(require('./routes/v1/application/deploy'), { prefix: '/application/deploy' })
server.register(require('./routes/v1/application/deploy/logs'), { prefix: '/application/deploy/logs' })
server.register(require('./routes/v1/databases'), { prefix: '/databases' })
server.register(require('./routes/v1/services'), { prefix: '/services' })
server.register(require('./routes/v1/services/deploy'), { prefix: '/services/deploy' })
server.register(require('./routes/v1/server'), { prefix: '/server' })
})
// Public routes

View File

@@ -1,19 +0,0 @@
const fs = require('fs').promises
const { streamEvents, docker } = require('../../libs/docker')
module.exports = async function (configuration) {
try {
const path = `${configuration.general.workdir}/${configuration.build.directory ? configuration.build.directory : ''}`
if (fs.stat(`${path}/Dockerfile`)) {
const stream = await docker.engine.buildImage(
{ src: ['.'], context: path },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} else {
throw { error: 'No custom dockerfile found.', type: 'app' }
}
} catch (error) {
throw { error, type: 'server' }
}
}

View File

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,25 @@
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

@@ -4,22 +4,19 @@ const buildImageNodeDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
`COPY ${configuration.build.directory} ./`,
`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) {
try {
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, buildImageNodeDocker(configuration))
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
}
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 = {

View File

@@ -1,7 +1,13 @@
const static = require('./static')
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 custom = require('./custom')
const docker = require('./docker')
const rust = require('./rust')
module.exports = { static, nodejs, php, custom, rust }
module.exports = { static: Static, nodejs, php, docker, rust, react, vuejs, nextjs, nuxtjs, svelte, gatsby }

View File

@@ -0,0 +1,28 @@
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

@@ -8,23 +8,21 @@ const publishNodejsDocker = (configuration) => {
'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} ./`,
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
: `
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) {
try {
if (configuration.build.command.build) await buildImage(configuration)
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishNodejsDocker(configuration))
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
}
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

@@ -0,0 +1,28 @@
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

@@ -6,21 +6,17 @@ const publishPHPDocker = (configuration) => {
'FROM php:apache',
'RUN a2enmod rewrite',
'WORKDIR /usr/src/app',
`COPY .${configuration.build.directory} /var/www/html`,
`COPY ./${configuration.build.directory} /var/www/html`,
'EXPOSE 80',
' CMD ["apache2-foreground"]'
].join('\n')
}
module.exports = async function (configuration) {
try {
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishPHPDocker(configuration))
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
}
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

@@ -0,0 +1,25 @@
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

@@ -37,28 +37,24 @@ const cacheRustDocker = (configuration, custom) => {
}
module.exports = async function (configuration) {
try {
const cargoToml = await execShellAsync(`cat ${configuration.general.workdir}/Cargo.toml`)
const parsedToml = TOML.parse(cargoToml)
const custom = {
name: parsedToml.package.name
}
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, cacheRustDocker(configuration, custom))
let stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:cache` }
)
await streamEvents(stream, configuration)
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishRustDocker(configuration, custom))
stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
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

@@ -9,24 +9,19 @@ const publishStaticDocker = (configuration) => {
'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} /usr/src/app/${configuration.publish.directory} ./`
: `COPY ${configuration.build.directory} ./`,
? `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) {
try {
if (configuration.build.command.build) await buildImage(configuration)
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration))
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
)
await streamEvents(stream, configuration)
} catch (error) {
throw { error, type: 'server' }
}
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

@@ -0,0 +1,25 @@
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

@@ -0,0 +1,25 @@
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

@@ -9,22 +9,12 @@ module.exports = async function (configuration) {
const execute = packs[configuration.build.pack]
if (execute) {
try {
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)
} catch (error) {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
if (error.stack) throw { error: error.stack, type: 'server' }
throw { error, type: 'app' }
}
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(
@@ -33,7 +23,6 @@ module.exports = async function (configuration) {
} catch (error) {
// Hmm.
}
throw { error: 'No buildpack found.', type: 'app' }
throw new Error('No buildpack found.')
}
}

View File

@@ -2,40 +2,34 @@ const { docker } = require('../../docker')
const { execShellAsync } = require('../../common')
const Deployment = require('../../../models/Deployment')
async function purgeImagesContainers () {
try {
await execShellAsync('docker container prune -f')
await execShellAsync('docker image prune -f --filter=label!=coolify-reserve=true')
} catch (error) {
throw { error, type: 'server' }
async function 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 (configuration) {
const { id } = configuration.repository
const deployId = configuration.general.deployId
try {
// Cleanup stucked deployments.
const deployments = await Deployment.find({ repoId: id, deployId: { $ne: deployId }, progress: { $in: ['queued', 'inprogress'] } })
for (const deployment of deployments) {
await Deployment.findByIdAndUpdate(deployment._id, { $set: { progress: 'failed' } })
}
} catch (error) {
throw { error, type: 'server' }
}
async function cleanupStuckedDeploymentsInDB () {
// Cleanup stucked deployments.
await Deployment.updateMany(
{ progress: { $in: ['queued', 'inprogress'] } },
{ progress: 'failed' }
)
}
async function deleteSameDeployments (configuration) {
try {
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']}`)
}
})
} catch (error) {
throw { error, type: 'server' }
}
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

@@ -2,76 +2,48 @@ const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-na
const cuid = require('cuid')
const crypto = require('crypto')
const { docker } = require('../docker')
const { execShellAsync } = require('../common')
const { execShellAsync, baseServiceConfiguration } = require('../common')
function getUniq () {
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 })
}
function setDefaultConfiguration (configuration) {
try {
const nickname = getUniq()
const deployId = cuid()
const nickname = getUniq()
const deployId = cuid()
const shaBase = JSON.stringify({ repository: configuration.repository })
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex')
const shaBase = JSON.stringify({ repository: configuration.repository })
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex')
const baseServiceConfiguration = {
replicas: 1,
restart_policy: {
condition: 'any',
max_attempts: 3
},
update_config: {
parallelism: 1,
delay: '10s',
order: 'start-first'
},
rollback_config: {
parallelism: 1,
delay: '10s',
order: 'start-first',
failure_action: 'rollback'
}
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
}
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 === 'php') {
configuration.publish.port = 80
} else if (configuration.build.pack === 'static') {
configuration.publish.port = 80
} else if (configuration.build.pack === 'nodejs') {
configuration.publish.port = 3000
} else if (configuration.build.pack === 'rust') {
configuration.publish.port = 3000
}
}
if (!configuration.build.directory) {
configuration.build.directory = '/'
}
if (!configuration.publish.directory) {
configuration.publish.directory = '/'
}
if (configuration.build.pack === 'static' || configuration.build.pack === 'nodejs') {
if (!configuration.build.command.installation) configuration.build.command.installation = 'yarn install'
}
configuration.build.container.baseSHA = crypto.createHash('sha256').update(JSON.stringify(baseServiceConfiguration)).digest('hex')
configuration.baseServiceConfiguration = baseServiceConfiguration
return configuration
} catch (error) {
throw { error, type: 'server' }
}
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) {
@@ -86,12 +58,8 @@ async function updateServiceLabels (configuration) {
})
if (found) {
const { ID } = found
try {
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}`)
} catch (error) {
console.log(error)
}
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}`)
}
}
@@ -142,4 +110,4 @@ async function precheckDeployment ({ services, configuration }) {
forceUpdate
}
}
module.exports = { setDefaultConfiguration, updateServiceLabels, precheckDeployment }
module.exports = { setDefaultConfiguration, updateServiceLabels, precheckDeployment, baseServiceConfiguration }

View File

@@ -1,5 +1,6 @@
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') {
@@ -12,7 +13,7 @@ module.exports = async function (configuration) {
`)
}
// await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules')
if (configuration.build.pack === 'static') {
if (staticDeployments.includes(configuration.build.pack)) {
await fs.writeFile(
`${configuration.general.workdir}/nginx.conf`,
`user nginx;
@@ -59,6 +60,6 @@ module.exports = async function (configuration) {
)
}
} catch (error) {
throw { error, type: 'server' }
throw new Error(error)
}
}

View File

@@ -6,77 +6,71 @@ const { saveAppLog } = require('../../logging')
const { deleteSameDeployments } = require('../cleanup')
module.exports = async function (configuration, imageChanged) {
try {
const generateEnvs = {}
for (const secret of configuration.publish.secrets) {
generateEnvs[secret.name] = secret.value
}
const containerName = configuration.build.container.name
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
// 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.' +
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.' +
'traefik.http.routers.' +
configuration.build.container.name +
'.entrypoints=websecure',
'traefik.http.routers.' +
'traefik.http.routers.' +
configuration.build.container.name +
'.rule=Host(`' +
configuration.publish.domain +
'`) && PathPrefix(`' +
configuration.publish.path +
'`)',
'traefik.http.routers.' +
'traefik.http.routers.' +
configuration.build.container.name +
'.tls.certresolver=letsencrypt',
'traefik.http.routers.' +
'traefik.http.routers.' +
configuration.build.container.name +
'.middlewares=global-compress'
]
}
}
},
networks: {
[`${docker.network}`]: {
external: true
]
}
}
},
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)
} catch (error) {
console.log(error)
await saveAppLog(`Error occured during deployment: ${error.message}`, configuration)
throw { error, type: 'server' }
}
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,21 +1,23 @@
const jwt = require('jsonwebtoken')
const axios = require('axios')
const { execShellAsync, cleanupTmp } = require('../../common')
const { execShellAsync } = require('../../common')
module.exports = async function (configuration) {
const { workdir } = configuration.general
const { organization, name, branch } = configuration.repository
const github = configuration.github
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)
}
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'
})
@@ -29,7 +31,7 @@ module.exports = async function (configuration) {
}
})
await execShellAsync(
`mkdir -p ${workdir} && git clone -q -b ${branch} https://x-access-token:${accessToken.data.token}@github.com/${organization}/${name}.git ${workdir}/`
`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`)
@@ -37,8 +39,6 @@ module.exports = async function (configuration) {
.replace('\n', '')
.slice(0, 7)
} catch (error) {
cleanupTmp(workdir)
if (error.stack) console.log(error.stack)
throw { error, type: 'server' }
throw new Error(error)
}
}

View File

@@ -1,44 +1,27 @@
const dayjs = require('dayjs')
const { saveServerLog } = require('../logging')
const { cleanupTmp } = require('../common')
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 { cleanupStuckedDeploymentsInDB, purgeImagesContainers } = require('./cleanup')
const { updateServiceLabels } = require('./configuration')
async function queueAndBuild (configuration, imageChanged) {
const { id, organization, name, branch } = configuration.repository
const { domain } = configuration.publish
const { deployId, nickname, workdir } = configuration.general
try {
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)
cleanupTmp(workdir)
await purgeImagesContainers()
} catch (error) {
await cleanupStuckedDeploymentsInDB(configuration)
cleanupTmp(workdir)
const { type } = error.error
if (type === 'app') {
await saveAppLog(error.error, configuration, true)
} else {
await saveServerLog({ event: error.error, configuration })
}
}
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

@@ -6,6 +6,24 @@ 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 () {
@@ -94,5 +112,6 @@ module.exports = {
checkImageAvailable,
encryptData,
decryptData,
verifyUserId
verifyUserId,
baseServiceConfiguration
}

View File

@@ -8,24 +8,21 @@ const docker = {
network: process.env.DOCKER_NETWORK
}
async function streamEvents (stream, configuration) {
try {
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) {
reject(event.error)
return
}
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)
}
})
} catch (error) {
throw { error, type: 'app' }
}
}
})
}
module.exports = { streamEvents, docker }

View File

@@ -0,0 +1,75 @@
/* 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

@@ -0,0 +1,58 @@
/* 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

@@ -0,0 +1,6 @@
/* 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

@@ -0,0 +1,31 @@
/* 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

@@ -0,0 +1,239 @@
/* 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,10 +1,18 @@
const dayjs = require('dayjs')
const axios = require('axios')
const ApplicationLog = require('../models/Logs/Application')
const ServerLog = require('../models/Logs/Server')
const dayjs = require('dayjs')
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 {
@@ -12,25 +20,12 @@ async function saveAppLog (event, configuration, isError) {
const repoId = configuration.repository.id
const branch = configuration.repository.branch
if (isError) {
// console.log(event, config, isError)
let clearedEvent = null
if (event.error) clearedEvent = '[ERROR] ' + generateTimestamp() + event.error.replace(/(\r\n|\n|\r)/gm, '')
else if (event) clearedEvent = '[ERROR] ' + generateTimestamp() + event.replace(/(\r\n|\n|\r)/gm, '')
try {
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
} catch (error) {
console.log(error)
}
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(/(\r\n|\n|\r)/gm, '')
try {
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
} catch (error) {
console.log(error)
}
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) {
@@ -39,20 +34,14 @@ async function saveAppLog (event, configuration, isError) {
}
}
async function saveServerLog ({ event, configuration, type }) {
try {
if (configuration) {
const deployId = configuration.general.deployId
const repoId = configuration.repository.id
const branch = configuration.repository.branch
await new ApplicationLog({ repoId, branch, deployId, event: `[SERVER ERROR 😖]: ${event}` }).save()
}
await new ServerLog({ event, type }).save()
} catch (error) {
// Hmm.
}
}
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

@@ -0,0 +1,185 @@
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

@@ -2,10 +2,11 @@ const mongoose = require('mongoose')
const { version } = require('../../../package.json')
const logSchema = mongoose.Schema(
{
version: { type: String, required: true, default: version },
type: { type: String, required: true, enum: ['API', 'UPGRADE-P-1', 'UPGRADE-P-2'], default: 'API' },
event: { type: String, required: true },
seen: { type: Boolean, required: true, default: false }
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 } }
)

View File

@@ -3,7 +3,8 @@ const mongoose = require('mongoose')
const settingsSchema = mongoose.Schema(
{
applicationName: { type: String, required: true, default: 'coolify' },
allowRegistration: { type: Boolean, required: true, default: false }
allowRegistration: { type: Boolean, required: true, default: false },
sendErrors: { type: Boolean, required: true, default: true }
},
{ timestamps: true }
)

View File

@@ -1,15 +1,11 @@
const { verifyUserId } = require('../../../libs/common')
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 {
if (!await verifyUserId(request.headers.authorization)) {
reply.code(500).send({ error: 'Invalid request' })
return
}
const configuration = setDefaultConfiguration(request.body)
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
@@ -34,7 +30,8 @@ module.exports = async function (fastify) {
}
return { message: 'OK' }
} catch (error) {
throw { error, type: 'server' }
await saveServerLog(error)
throw new Error(error)
}
})
}

View File

@@ -1,37 +1,17 @@
const { verifyUserId, cleanupTmp } = require('../../../../libs/common')
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) {
// 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('/', async (request, reply) => {
let configuration
try {
await verifyUserId(request.headers.authorization)
} catch (error) {
@@ -40,7 +20,10 @@ module.exports = async function (fastify) {
}
try {
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
const configuration = setDefaultConfiguration(request.body)
configuration = setDefaultConfiguration(request.body)
if (!configuration) {
throw new Error('Whaat?')
}
await cloneRepository(configuration)
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
@@ -64,11 +47,23 @@ module.exports = async function (fastify) {
return
}
queueAndBuild(configuration, imageChanged)
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
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) {
throw { error, type: 'server' }
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

@@ -39,7 +39,7 @@ module.exports = async function (fastify) {
})
return finalLogs
} catch (error) {
throw { error, type: 'server' }
throw new Error(error)
}
})

View File

@@ -1,4 +1,5 @@
const { docker } = require('../../../libs/docker')
const { saveServerLog } = require('../../../libs/logging')
module.exports = async function (fastify) {
fastify.get('/', async (request, reply) => {
@@ -8,7 +9,8 @@ module.exports = async function (fastify) {
const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a)
return { logs }
} catch (error) {
throw { error, type: 'server' }
await saveServerLog(error)
throw new Error(error)
}
})
}

View File

@@ -1,7 +1,8 @@
const { docker } = require('../../../libs/docker')
const { execShellAsync } = require('../../../libs/common')
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) => {
@@ -25,6 +26,8 @@ module.exports = async function (fastify) {
}
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.' })
}

View File

@@ -1,48 +1,42 @@
const { docker } = require('../../../libs/docker')
const Deployment = require('../../../models/Deployment')
const ServerLog = require('../../../models/Logs/Server')
const { saveServerLog } = require('../../../libs/logging')
module.exports = async function (fastify) {
fastify.get('/', async (request, reply) => {
try {
const latestDeployments = await Deployment.aggregate([
{
$sort: { createdAt: -1 }
},
{
$group:
{
_id: {
repoId: '$repoId',
branch: '$branch'
},
createdAt: { $last: '$createdAt' },
progress: { $first: '$progress' }
}
}
])
const serverLogs = await ServerLog.find()
const services = await docker.engine.listServices()
let applications = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' && r.Spec.Labels.configuration)
let databases = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && r.Spec.Labels.configuration)
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)) {
const configuration = JSON.parse(r.Spec.Labels.configuration)
const status = latestDeployments.find(l => configuration.repository.id === l._id.repoId && configuration.repository.branch === l._id.branch)
if (status && status.progress) r.progress = status.progress
r.Spec.Labels.configuration = configuration
return r
return {
configuration: JSON.parse(r.Spec.Labels.configuration),
UpdatedAt: r.UpdatedAt
}
}
return {}
})
databases = databases.map(r => {
const configuration = r.Spec.Labels.configuration ? JSON.parse(r.Spec.Labels.configuration) : null
r.Spec.Labels.configuration = configuration
return r
if (JSON.parse(r.Spec.Labels.configuration)) {
return {
configuration: JSON.parse(r.Spec.Labels.configuration)
}
}
return {}
})
applications = [...new Map(applications.map(item => [item.Spec.Labels.configuration.publish.domain + item.Spec.Labels.configuration.publish.path, item])).values()]
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: {
@@ -50,13 +44,17 @@ module.exports = async function (fastify) {
},
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 {
throw { error, type: 'server' }
await saveServerLog(error)
throw new Error(error)
}
}
})

View File

@@ -3,6 +3,7 @@ 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')
@@ -17,13 +18,15 @@ module.exports = async function (fastify) {
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 = {}
for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
const s = d.split('=')
jsonEnvs[s[0]] = s[1]
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
envs: jsonEnvs || null
}
reply.code(200).send(payload)
} else {
@@ -38,7 +41,7 @@ module.exports = async function (fastify) {
body: {
type: 'object',
properties: {
type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb'] }
type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb', 'clickhouse'] }
},
required: ['type']
}
@@ -63,7 +66,6 @@ module.exports = async function (fastify) {
if (!defaultDatabaseName) defaultDatabaseName = nickname
reply.code(201).send({ message: 'Deploying.' })
// TODO: Persistent volume, custom inputs
const deployId = cuid()
const configuration = {
general: {
@@ -81,9 +83,11 @@ module.exports = async function (fastify) {
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],
@@ -118,6 +122,15 @@ module.exports = async function (fastify) {
}
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 = {
@@ -128,6 +141,7 @@ module.exports = async function (fastify) {
networks: [`${docker.network}`],
environment: generateEnvs,
volumes: [volume],
ulimits,
deploy: {
replicas: 1,
update_config: {
@@ -159,13 +173,14 @@ module.exports = async function (fastify) {
}
}
}
await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
await execShellAsync(
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
)
} catch (error) {
throw { error, type: 'server' }
console.log(error)
await saveServerLog(error)
throw new Error(error)
}
})

View File

@@ -4,6 +4,8 @@ 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: {
@@ -59,8 +61,12 @@ module.exports = async function (fastify) {
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 })
@@ -111,8 +117,8 @@ module.exports = async function (fastify) {
return
}
} catch (error) {
console.log(error)
reply.code(500).send({ success: false, error: error.message })
await saveServerLog(error)
throw new Error(error)
}
})
fastify.get('/success', async (request, reply) => {

View File

@@ -8,7 +8,7 @@ module.exports = async function (fastify) {
serverLogs
}
} catch (error) {
throw { error, type: 'server' }
throw new Error(error)
}
})
}

View File

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,27 @@
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,13 +1,16 @@
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' }
allowRegistration: { type: 'boolean' },
sendErrors: { type: 'boolean' }
},
required: ['allowRegistration']
required: []
}
}
@@ -25,7 +28,8 @@ module.exports = async function (fastify) {
settings
}
} catch (error) {
throw { error, type: 'server' }
await saveServerLog(error)
throw new Error(error)
}
})
@@ -38,7 +42,8 @@ module.exports = async function (fastify) {
).select('-_id -__v')
reply.code(201).send({ settings })
} catch (error) {
throw { error, type: 'server' }
await saveServerLog(error)
throw new Error(error)
}
})
}

View File

@@ -4,9 +4,9 @@ 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({ event: upgradeP1, type: 'UPGRADE-P-1' })
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({ event: upgradeP2, type: 'UPGRADE-P-2' })
await saveServerLog({ message: upgradeP2, type: 'UPGRADE-P-2' })
})
}

View File

@@ -1,13 +1,17 @@
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) {
// TODO: Add this to fastify plugin
const postSchema = {
body: {
type: 'object',
@@ -33,6 +37,7 @@ module.exports = async function (fastify) {
}
}
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')
@@ -48,7 +53,7 @@ module.exports = async function (fastify) {
try {
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
let configuration = services.find(r => {
configuration = services.find(r => {
if (request.body.ref.startsWith('refs')) {
const branch = request.body.ref.split('/')[2]
if (
@@ -89,11 +94,28 @@ module.exports = async function (fastify) {
return
}
queueAndBuild(configuration, imageChanged)
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
await queueAndBuild(configuration, imageChanged)
} catch (error) {
throw { error, type: 'server' }
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,21 +1,25 @@
require('dotenv').config()
const fs = require('fs')
const util = require('util')
const { saveServerLog } = require('./libs/logging')
const { execShellAsync } = require('./libs/common')
const { purgeImagesContainers, cleanupStuckedDeploymentsInDB } = require('./libs/applications/cleanup')
const Deployment = require('./models/Deployment')
const fastify = require('fastify')({
logger: { level: 'error' }
})
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', (reason, p) => {
console.log(reason)
console.log(p)
process.on('unhandledRejection', async (reason, p) => {
await saveServerLog({ message: reason.message, type: 'unhandledRejection' })
})
fastify.register(require('fastify-env'), {
schema,
dotenv: true
@@ -36,18 +40,6 @@ if (process.env.NODE_ENV === 'production') {
}
fastify.register(require('./app'), { prefix: '/api/v1' })
fastify.setErrorHandler(async (error, request, reply) => {
if (error.statusCode) {
reply.status(error.statusCode).send({ message: error.message } || { message: 'Something is NOT okay. Are you okay?' })
} else {
reply.status(500).send({ message: error.message } || { message: 'Something is NOT okay. Are you okay?' })
}
try {
await saveServerLog({ event: error })
} catch (error) {
//
}
})
if (process.env.NODE_ENV === 'production') {
mongoose.connect(
@@ -91,6 +83,12 @@ mongoose.connection.once('open', async function () {
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()
@@ -98,19 +96,14 @@ mongoose.connection.once('open', async function () {
// Could not cleanup DB 🤔
}
try {
// Doing because I do not want to prune these images. Prune skip coolify-reserve labeled images.
const basicImages = ['nginx:stable-alpine', 'node:lts', 'ubuntu:20.04']
// 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(`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)
}
try {
await purgeImagesContainers()
} catch (error) {
console.log('Could not purge containers/images.')
console.log(error)
}
})

View File

@@ -6,7 +6,7 @@
<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: Heroku & Netlify alternative</title>
<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" />

View File

@@ -1,24 +1,15 @@
FROM ubuntu:20.04 as binaries
FROM node:lts
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
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
RUN apt update && apt install -y docker-ce-cli && apt clean all
FROM node:lts
WORKDIR /usr/src/app
LABEL coolify-preserve=true
COPY --from=binaries /usr/bin/docker /usr/bin/docker
COPY --from=binaries /usr/bin/envsubst /usr/bin/envsubst
COPY --from=binaries /usr/bin/jq /usr/bin/jq
COPY . .
RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm@6
RUN chmod +x /usr/bin/envsubst /usr/bin/jq /usr/bin/docker
RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm
COPY ./*package.json .
RUN pnpm install
COPY . .
RUN pnpm build
RUN rm -fr node_modules .pnpm-store
RUN pnpm install -P
CMD ["pnpm", "start"]
EXPOSE 3000

View File

@@ -0,0 +1,73 @@
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

@@ -2,7 +2,7 @@ version: '3.8'
services:
proxy:
image: traefik:v2.3
image: traefik:v2.4
hostname: coollabs-proxy
ports:
- target: 80

View File

@@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
"version": "1.0.6",
"version": "1.0.11",
"license": "AGPL-3.0",
"scripts": {
"lint": "standard",
@@ -18,7 +18,8 @@
"dependencies": {
"@iarna/toml": "^2.2.5",
"@roxi/routify": "^2.15.1",
"@zerodevx/svelte-toast": "^0.2.1",
"@zerodevx/svelte-toast": "^0.2.2",
"ajv": "^8.1.0",
"axios": "^0.21.1",
"commander": "^7.2.0",
"compare-versions": "^3.6.0",
@@ -33,6 +34,7 @@
"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",

655
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@
@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 !important;
@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;

View File

@@ -1,24 +0,0 @@
<script>
import { application } from "@store";
import Tooltip from "../../../Tooltip/TooltipInfo.svelte";
</script>
<div class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 text-center">
<label for="installCommand"
>Install Command <Tooltip label="Command to run for installing dependencies. eg: yarn install." />
</label>
<input
class="mb-6"
id="installCommand"
bind:value="{$application.build.command.installation}"
placeholder="eg: yarn install"
/>
<label for="buildCommand">Build Command <Tooltip label="Command to run for building your application. If empty, no build phase initiated in the deploy process." /></label>
<input
class="mb-6"
id="buildCommand"
bind:value="{$application.build.command.build}"
placeholder="eg: yarn build"
/>
</div>

View File

@@ -1,56 +1,213 @@
<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 { application } from "@store";
import { onMount } from "svelte";
import TooltipInfo from "../../../Tooltip/TooltipInfo.svelte";
const showPorts = ['nodejs','custom','rust']
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-2xl md:mx-auto mx-6 pb-6 auto-cols-max "
class="grid grid-cols-1 text-sm max-w-4xl md:mx-auto mx-6 pb-16 auto-cols-max "
>
<label for="buildPack"
>Build Pack
{#if $application.build.pack === 'custom'}
<TooltipInfo
label="Your custom Dockerfile will be used from the root directory (or from 'Base Directory' specified below) of your repository. "
/>
{:else if $application.build.pack === 'static'}
<TooltipInfo
label="Published as a static site (for build phase see 'Build Step' tab)."
/>
{:else if $application.build.pack === 'nodejs'}
<TooltipInfo
label="Published as a Node.js application (for build phase see 'Build Step' tab)."
/>
{:else if $application.build.pack === 'php'}
<TooltipInfo
size="large"
label="Published as a PHP application."
/>
{:else if $application.build.pack === 'rust'}
<TooltipInfo
size="large"
label="Published as a Rust application."
/>
{/if}
</label
>
<select id="buildPack" bind:value="{$application.build.pack}">
<option selected class="font-bold">static</option>
<option class="font-bold">nodejs</option>
<option class="font-bold">php</option>
<option class="font-bold">custom</option>
<option class="font-bold">rust</option>
</select>
<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"
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 ||
@@ -63,7 +220,9 @@
<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}/api`}"
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
@@ -73,16 +232,27 @@
/>
</div>
</div>
{#if showPorts.includes($application.build.pack)}
<label for="Port" >Port</label>
<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="mb-6"
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="{$application.build.pack === 'static' ? '80' : '3000'}"
placeholder="{buildpacks[$application.build.pack].port.number}"
/>
{/if}
<div class="grid grid-flow-col gap-2 items-center pt-12">
<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
@@ -92,7 +262,7 @@
<input
id="baseDir"
bind:value="{$application.build.directory}"
placeholder="/"
placeholder="eg: sourcedir"
/>
</div>
<div class="grid grid-flow-row">
@@ -104,7 +274,65 @@
<input
id="publishDir"
bind:value="{$application.publish.directory}"
placeholder="/"
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>

View File

@@ -36,40 +36,43 @@
];
}
</script>
<div class="max-w-2xl md:mx-auto mx-6 text-center">
<div class="text-2xl font-bold border-gradient w-24">Secrets</div>
<div class="max-w-xl mx-auto text-center pt-4">
<div class="text-left text-base font-bold tracking-tight text-warmGray-400">
New Secret
</div>
<div class="grid md:grid-flow-col grid-flow-row gap-2">
<input id="secretName" bind:value="{secret.name}" placeholder="Name" />
<input id="secretValue" bind:value="{secret.value}" placeholder="Value" />
<button
class="button p-1 w-20 bg-green-600 hover:bg-green-500 text-white"
on:click="{saveSecret}">Save</button
>
<div class="flex space-x-4">
<input id="secretName" bind:value="{secret.name}" placeholder="Name" class="w-64 border-2 border-transparent" />
<input id="secretValue" bind:value="{secret.value}" placeholder="Value" class="w-64 border-2 border-transparent" />
<button class="icon hover:text-green-500" on:click="{saveSecret}">
<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="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
{#if $application.publish.secrets.length > 0}
<div class="py-4">
{#each $application.publish.secrets as s}
<div class="grid md:grid-flow-col grid-flow-row gap-2">
<div class="flex space-x-4">
<input
id="{s.name}"
value="{s.name}"
disabled
class="border-2 bg-transparent border-transparent"
class="border-2 bg-transparent border-transparent w-64"
class:border-red-600="{foundSecret && foundSecret.name === s.name}"
/>
<input
id="{s.createdAt}"
value="SAVED"
disabled
class="bg-transparent border-transparent"
class="border-2 bg-transparent border-transparent w-64"
/>
<button
class="button w-20 bg-red-600 hover:bg-red-500 text-white"
on:click="{() => removeSecret(s.name)}">Delete</button
>
<button class="icon hover:text-red-500" on:click="{() => removeSecret(s.name)}">
<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 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
{/each}
</div>

View File

@@ -1,11 +1,10 @@
<script>
export let loading, branches;
import { isActive } from "@roxi/routify";
import { application } from "@store";
import { application, activePage } from "@store";
import Select from "svelte-select";
const selectedValue =
!$isActive("/application/new") && $application.repository.branch
$activePage.application !== "new" && $application.repository.branch;
function handleSelect(event) {
$application.repository.branch = null;
@@ -36,10 +35,10 @@
selectedValue="{selectedValue}"
isClearable="{false}"
items="{branches.map(b => ({ label: b.name, value: b.name }))}"
showIndicator="{$isActive('/application/new')}"
showIndicator="{$activePage.new}"
noOptionsMessage="No branches found"
placeholder="Select a branch"
isDisabled="{!$isActive('/application/new')}"
isDisabled="{!$activePage.new}"
/>
</div>
</div>

View File

@@ -1,7 +1,15 @@
<script>
import { redirect, isActive } from "@roxi/routify";
import { fade } from "svelte/transition";
import { session, application, fetch, initialApplication } from "@store";
import {
session,
application,
fetch,
initialApplication,
githubRepositories,
githubInstallations,
activePage,
} from "@store";
import Login from "./Login.svelte";
import Loading from "../../Loading.svelte";
@@ -15,8 +23,6 @@
};
let branches = [];
let repositories = [];
function dashify(str, options) {
if (typeof str !== "string") return str;
return str
@@ -29,8 +35,8 @@
async function loadBranches() {
loading.branches = true;
if ($isActive("/application/new")) $application.repository.branch = null;
const selectedRepository = repositories.find(
if ($activePage.new) $application.repository.branch = null;
const selectedRepository = $githubRepositories.find(
r => r.id === $application.repository.id,
);
@@ -54,6 +60,23 @@
}
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(
@@ -64,6 +87,7 @@
}
$application.github.installation.id = installations[0].id;
$application.github.app.id = installations[0].app_id;
$githubInstallations = installations[0];
let page = 1;
let userRepos = 0;
@@ -72,21 +96,20 @@
page,
);
repositories = repositories.concat(data.repositories);
$githubRepositories = $githubRepositories.concat(data.repositories);
userRepos = data.total_count;
if (userRepos > repositories.length) {
while (userRepos > repositories.length) {
if (userRepos > $githubRepositories.length) {
while (userRepos > $githubRepositories.length) {
page = page + 1;
const repos = await getGithubRepos(
$application.github.installation.id,
page,
);
repositories = repositories.concat(repos.repositories);
$githubRepositories = $githubRepositories.concat(repos.repositories);
}
}
const foundRepositoryOnGithub = repositories.find(
const foundRepositoryOnGithub = $githubRepositories.find(
r =>
r.full_name ===
`${$application.repository.organization}/${$application.repository.name}`,
@@ -120,7 +143,7 @@
if (newWindow.closed) {
clearInterval(timer);
loading.github = true;
if (!$isActive("/application/new")) {
if (!$activePage.new) {
try {
const config = await $fetch(`/api/v1/config`, {
body: {
@@ -137,28 +160,46 @@
$application = JSON.parse(JSON.stringify(initialApplication));
}
branches = [];
repositories = [];
$githubRepositories = [];
await loadGithub();
}
}, 100);
}
</script>
{#if !$isActive("/application/new")}
{#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="text-green-500 hover:underline cursor-pointer px-2"
class="icon mx-2"
href="{'https://' +
$application.publish.domain +
$application.publish.path}"
>{$application.publish.domain
? `${$application.publish.domain}${$application.publish.path !== '/' ? $application.publish.path : ''}`
: "Loading..."}</a
>
<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"
@@ -180,7 +221,7 @@
>
</div>
</div>
{:else if $isActive("/application/new")}
{: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"
@@ -205,11 +246,10 @@
in:fade="{{ duration: 100 }}"
>
<Repositories
bind:repositories
on:loadBranches="{loadBranches}"
on:modifyGithubAppConfig="{modifyGithubAppConfig}"
/>
{#if $application.repository.organization !== "new"}
{#if $application.repository.organization}
<Branches loading="{loading.branches}" branches="{branches}" />
{/if}

View File

@@ -1,43 +1,42 @@
<script>
import { createEventDispatcher } from "svelte";
import { isActive } from "@roxi/routify";
import { application } from "@store";
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");
}
export let repositories;
let items = repositories.map(repo => ({
let items = $githubRepositories.map(repo => ({
label: `${repo.owner.login}/${repo.name}`,
value: repo.id.toString(),
}));
const selectedValue =
!$isActive("/application/new") &&
!$activePage.new &&
`${$application.repository.organization}/${$application.repository.name}`;
const dispatch = createEventDispatcher();
const modifyGithubAppConfig = () => dispatch("modifyGithubAppConfig");
</script>
<div class="grid grid-cols-1">
{#if repositories.length !== 0}
<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="{$isActive('/application/new')}"
showIndicator="{$activePage.new}"
noOptionsMessage="No Repositories found"
placeholder="Select a Repository"
isDisabled="{!$isActive('/application/new')}"
isDisabled="{!$activePage.new}"
/>
</div>
<button

View File

@@ -1,109 +1,13 @@
<script>
import { redirect, isActive } from "@roxi/routify";
import { redirect } from "@roxi/routify";
import { onMount } from "svelte";
import { toast } from "@zerodevx/svelte-toast";
import templates from "../../../utils/templates";
import { application, fetch, deployments } from "@store";
import { application, fetch, deployments, activePage } from "@store";
import General from "./ActiveTab/General.svelte";
import BuildStep from "./ActiveTab/BuildStep.svelte";
import Secrets from "./ActiveTab/Secrets.svelte";
import Loading from "../../Loading.svelte";
const buildPhaseActive = ["nodejs", "static"];
let loading = false;
onMount(async () => {
if (!$isActive("/application/new")) {
const config = await $fetch(`/api/v1/config`, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
},
});
$application = { ...config };
$redirect(`/application/:organization/:name/:branch/configuration`, {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
});
} else {
loading = true;
$deployments?.applications?.deployed.find(d => {
const conf = d?.Spec?.Labels.configuration;
if (
conf?.repository?.organization ===
$application.repository.organization &&
conf?.repository?.name === $application.repository.name &&
conf?.repository?.branch === $application.repository.branch
) {
$redirect(`/application/:organization/:name/:branch/configuration`, {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
});
toast.push("This repository & branch is already defined. Redirecting...");
}
});
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 (Dockerfile) {
$application.build.pack = "custom";
toast.push("Custom Dockerfile found. Build pack set to custom.");
} else 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} App detected. Default values set.`);
}
});
} else if (CargoToml) {
$application.build.pack = "rust";
toast.push(`Rust language detected. Default values set.`);
}
} catch (error) {
// Nothing detected
}
}
loading = false;
});
let activeTab = {
general: true,
buildStep: false,
@@ -119,12 +23,104 @@
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>
{#if loading}
{#await load()}
<Loading github githubLoadingText="Scanning repository..." />
{:else}
<div class="block text-center py-4">
{: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"
@@ -132,47 +128,26 @@
<div
on:click="{() => activateTab('general')}"
class:text-green-500="{activeTab.general}"
class="px-3 py-2 cursor-pointer hover:text-green-500"
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
General
</div>
{#if !buildPhaseActive.includes($application.build.pack)}
<div disabled class="px-3 py-2 text-warmGray-700 cursor-not-allowed">
Build Step
</div>
{:else}
<div
on:click="{() => activateTab('buildStep')}"
class:text-green-500="{activeTab.buildStep}"
class="px-3 py-2 cursor-pointer hover:text-green-500"
>
Build Step
</div>
{/if}
{#if $application.build.pack === "custom"}
<div disabled class="px-3 py-2 text-warmGray-700 cursor-not-allowed">
Secrets
</div>
{:else}
<div
on:click="{() => activateTab('secrets')}"
class:text-green-500="{activeTab.secrets}"
class="px-3 py-2 cursor-pointer hover:text-green-500"
>
Secrets
</div>
{/if}
<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.buildStep}
<BuildStep />
{:else if activeTab.secrets}
<Secrets />
{/if}
</div>
</div>
{/if}
{/await}

View File

@@ -0,0 +1,195 @@
<script>
import { params, goto, redirect } from "@roxi/routify";
import {
application,
fetch,
initialApplication,
initConf,
activePage,
} from "@store";
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() {
await $fetch(`/api/v1/application/remove`, {
body: {
organization: $params.organization,
name: $params.name,
branch: $params.branch,
},
});
toast.push("Application removed.");
$application = JSON.parse(JSON.stringify(initialApplication));
$redirect(`/dashboard/applications`);
}
onDestroy(() => {
$application = JSON.parse(JSON.stringify(initialApplication));
});
async function deploy() {
try {
toast.push("Checking configuration.");
await $fetch(`/api/v1/application/check`, {
body: $application,
});
const { nickname, name, deployId } = await $fetch(
`/api/v1/application/deploy`,
{
body: $application,
},
);
$application.general.nickname = nickname;
$application.build.container.name = name;
$application.general.deployId = deployId;
$initConf = JSON.parse(JSON.stringify($application));
toast.push("Application deployment queued.");
$redirect(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs/${$application.general.deployId}`,
);
} catch (error) {
console.log(error);
toast.push(error.error || error || "Ooops something went wrong.");
}
}
</script>
<nav
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">
<button
disabled="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class:cursor-not-allowed="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class:hover:bg-green-500="{$application.publish.domain}"
class:bg-green-600="{$application.publish.domain}"
class:hover:bg-transparent="{$activePage.new}"
class:text-warmGray-700="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class="icon"
on:click="{deploy}"
>
<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"
><polyline points="16 16 12 12 8 16"></polyline><line
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"
></path><polyline points="16 16 12 12 8 16"></polyline></svg
>
</button>
</Tooltip>
<Tooltip position="bottom" label="Delete">
<button
disabled="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$activePage.new}"
class:cursor-not-allowed="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$activePage.new}"
class:hover:text-red-500="{$application.publish.domain &&
!$activePage.new}"
class:hover:bg-warmGray-700="{$application.publish.domain &&
!$activePage.new}"
class:hover:bg-transparent="{$activePage.new}"
class:text-warmGray-700="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$activePage.new}"
class="icon"
on:click="{removeApplication}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
<div class="border border-warmGray-700 h-8"></div>
<Tooltip position="bottom" label="Logs">
<button
class="icon"
class:text-warmGray-700="{$activePage.new}"
disabled="{$activePage.new}"
class:hover:text-blue-400="{!$activePage.new}"
class:hover:bg-transparent="{$activePage.new}"
class:cursor-not-allowed="{$activePage.new}"
class:text-blue-400="{$activePage.application === 'logs'}"
class:bg-warmGray-700="{$activePage.application === 'logs'}"
on:click="{() =>
$goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
)}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
<Tooltip position="bottom-left" label="Configuration">
<button
class="icon hover:text-yellow-400"
disabled="{$activePage.new}"
class:text-yellow-400="{$activePage.application === 'configuration' ||
$activePage.new}"
class:bg-warmGray-700="{$activePage.application === 'configuration' ||
$activePage.new}"
on:click="{() =>
$goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
)}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
</nav>

View File

@@ -3,6 +3,10 @@
import { isActive, redirect } from "@roxi/routify/runtime";
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";
let type;
let defaultDatabaseName;
@@ -15,7 +19,7 @@
defaultDatabaseName,
},
});
$dbInprogress = true
$dbInprogress = true;
toast.push("Database deployment queued.");
$redirect(`/dashboard/databases`);
} catch (error) {
@@ -30,48 +34,65 @@
>
{#if $isActive("/database/new")}
<div class="flex justify-center space-x-4 font-bold pb-6">
<button
class="button bg-gray-500 p-2 text-white hover:bg-green-600 cursor-pointer w-32"
<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')}"
class:bg-green-600="{type === 'mongodb'}"
>
MongoDB
</button>
<button
class="button bg-gray-500 p-2 text-white hover:bg-blue-600 cursor-pointer w-32"
<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')}"
class:bg-blue-600="{type === 'postgresql'}"
>
PostgreSQL
</button>
<button
class="button bg-gray-500 p-2 text-white hover:bg-orange-600 cursor-pointer w-32"
<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')}"
class:bg-orange-600="{type === 'mysql'}"
>
MySQL
</button>
<button
class="button bg-gray-500 p-2 text-white hover:bg-red-600 cursor-pointer w-32"
on:click="{() => (type = 'couchdb')}"
class:bg-red-600="{type === 'couchdb'}"
>
Couchdb
</button>
<div class="flex items-center justify-center">
<Mysql customClass="w-10" />
</div>
<div class="text-white">MySQL</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>
<div
class="grid grid-rows-1 justify-center items-center text-center pb-5"
>
<label for="defaultDB">Default database</label>
<input
id="defaultDB"
class="w-64"
placeholder="random"
bind:value="{defaultDatabaseName}"
/>
</div>
<div class="flex justify-center space-x-4 items-center">
<label for="defaultDB">Default database</label>
<input
id="defaultDB"
class="w-64"
placeholder="random"
bind:value="{defaultDatabaseName}"
/>
<button
class:bg-green-600="{type === 'mongodb'}"
class:hover:bg-green-500="{type === 'mongodb'}"
@@ -81,6 +102,8 @@
class:hover:bg-orange-500="{type === 'mysql'}"
class:bg-red-600="{type === 'couchdb'}"
class:hover:bg-red-500="{type === 'couchdb'}"
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
>

View File

@@ -0,0 +1,9 @@
<script>
export let customClass;
</script>
<svg class="{customClass}" viewBox="0 0 9 8" xmlns="http://www.w3.org/2000/svg"
><path d="m0 7h1v1h-1z" fill="#f00"></path><path
d="m0 0h1v7h-1zm2 0h1v8h-1zm2 0h1v8h-1zm2 0h1v8h-1zm2 3.25h1v1.5h-1z"
fill="#fc0"></path></svg
>

View File

@@ -0,0 +1,54 @@
<script>
export let value;
let showPassword = false;
</script>
<div class="relative w-full">
<input
type="{showPassword ? 'text' : 'password'}"
class="w-full "
{value}
disabled
/>
<div
class="absolute top-0 my-2 mx-2 right-0 cursor-pointer text-warmGray-600 hover:text-white"
on:click="{() => showPassword = !showPassword}"
>
{#if showPassword}
<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="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
</svg>
{:else}
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
{/if}
</div>
</div>

View File

@@ -0,0 +1,82 @@
<script>
import { fetch } from "@store";
import { params } from "@roxi/routify/runtime";
import { fade } from "svelte/transition";
import { toast } from "@zerodevx/svelte-toast";
import Loading from "../Loading.svelte";
import TooltipInfo from "../Tooltip/TooltipInfo.svelte";
import PasswordField from "../PasswordField.svelte";
import Tooltip from "../Tooltip/Tooltip.svelte";
export let service;
$: name = $params.name;
let loading = false;
async function activate() {
try {
loading = true;
await $fetch(`/api/v1/services/deploy/${name}/activate`, {
method: "PATCH",
body: {},
});
toast.push(`All users are activated for Plausible.`);
} catch (error) {
console.log(error);
toast.push(`Ooops, there was an error activating users for Plausible?!`);
} finally {
loading = false;
}
}
</script>
{#if loading}
<Loading />
{:else}
<div class="text-left max-w-5xl mx-auto px-6" in:fade="{{ duration: 100 }}">
<div class="pb-2 pt-5 space-y-4">
<div class="flex space-x-5 items-center">
<div class="text-2xl font-bold border-gradient">General</div>
<div class="flex-1"></div>
<Tooltip
position="bottom"
size="large"
label="Activate all users in Plausible database, so you can login without the email verification."
>
<button
class="button bg-blue-500 hover:bg-blue-400 px-2"
on:click="{activate}">Activate All Users</button
>
</Tooltip>
</div>
<div class="flex items-center pt-4">
<div class="font-bold w-64 text-warmGray-400">Domain</div>
<input class="w-full" value="{service.config.baseURL}" disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Email address</div>
<input class="w-full" value="{service.config.email}" disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Username</div>
<input class="w-full" value="{service.config.userName}" disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Password</div>
<PasswordField value="{service.config.userPassword}" />
</div>
<div class="text-2xl font-bold pt-4 border-gradient w-32">PostgreSQL</div>
<div class="flex items-center pt-4">
<div class="font-bold w-64 text-warmGray-400">Username</div>
<input class="w-full" value="{service.config.generateEnvsPostgres.POSTGRESQL_USERNAME}" disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Password</div>
<PasswordField value="{service.config.generateEnvsPostgres.POSTGRESQL_PASSWORD}" />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Database</div>
<input class="w-full" value="{service.config.generateEnvsPostgres.POSTGRESQL_DATABASE}" disabled />
</div>
</div>
</div>
{/if}

View File

@@ -22,6 +22,11 @@ body {
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);
@@ -29,7 +34,7 @@ body {
font-family: 'Inter';
font-size: 16px;
font-weight: 600;
white-space: normal;
white-space: normal;
}
[role~="tooltip"][data-microtip-position|="bottom"]::before {

View File

@@ -5,8 +5,8 @@
</style>
<script>
import { goto, route, isActive } from "@roxi/routify/runtime";
import { loggedIn, session, fetch, deployments } from "@store";
import { goto, route, isChangingPage } from "@roxi/routify/runtime";
import { loggedIn, session, fetch, deployments, activePage } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import { onMount } from "svelte";
import compareVersions from "compare-versions";
@@ -17,9 +17,65 @@
let upgradeDisabled = false;
let upgradeDone = false;
let latest = {};
let showAck = false;
const branch =
process.env.NODE_ENV === "production" &&
window.location.hostname !== "test.andrasbacsai.dev"
? "main"
: "next";
$: if ($isChangingPage) {
const path = window.location.pathname;
if (path === "/dashboard/applications" || path.match(/\/application/)) {
$activePage.mainmenu = "applications";
} else if (path === "/dashboard/databases" || path.match(/\/database/)) {
$activePage.mainmenu = "databases";
} else if (path === "/dashboard/services" || path.match(/\/service/)) {
$activePage.mainmenu = "services";
} else if (path === "/settings") {
$activePage.mainmenu = "settings";
} else {
$activePage.mainmenu = null;
}
if (path.match(/\/application\/.*\/logs/)) {
$activePage.application = "logs";
$activePage.new = false;
} else if (path === "/application/new") {
$activePage.application = "configuration";
$activePage.new = true;
} else if (path.match(/\/application\/.*\/configuration/)) {
$activePage.application = "configuration";
$activePage.new = false;
} else {
$activePage.application = null;
}
}
onMount(async () => {
if ($session.token) upgradeAvailable = await checkUpgrade();
if ($session.token) {
upgradeAvailable = await checkUpgrade();
if (!localStorage.getItem("automaticErrorReportsAck")) {
showAck = true;
if (latest?.coolify[branch]?.settings?.sendErrors) {
const settings = {
sendErrors: true,
};
await $fetch("/api/v1/settings", {
body: {
...settings,
},
headers: {
Authorization: `Bearer ${$session.token}`,
},
});
}
}
}
});
function ackError() {
localStorage.setItem("automaticErrorReportsAck", "true");
showAck = false;
}
async function verifyToken() {
if ($session.token) {
try {
@@ -69,11 +125,6 @@
cache: "no-cache",
})
.then(r => r.json());
const branch =
process.env.NODE_ENV === "production" &&
window.location.hostname !== "test.andrasbacsai.dev"
? "main"
: "next";
return compareVersions(
latest.coolify[branch].version,
@@ -85,24 +136,47 @@
</script>
{#await verifyToken() then notUsed}
{#if showAck}
<div
class="p-2 fixed top-0 right-0 z-50 w-64 m-2 rounded border-gradient-full bg-black"
>
<div class="text-white text-xs space-y-2 text-justify font-medium">
<div>
We implemented an automatic error reporting feature, which is enabled
by default.
</div>
<div>
Why? Because we would like to hunt down bugs faster and easier.
</div>
<div class="py-5">
If you do not like it, you can turn it off in the <button
class="underline font-bold"
on:click="{$goto('/settings')}">Settings menu</button
>.
</div>
<button
class="button p-2 bg-warmGray-800 w-full text-center hover:bg-warmGray-700"
on:click="{ackError}">OK</button
>
</div>
</div>
{/if}
{#if $route.path !== "/index"}
<nav
class="w-16 bg-warmGray-800 text-white top-0 left-0 fixed min-w-4rem min-h-screen"
>
<div
class="flex flex-col w-full h-screen items-center transition-all duration-100"
class:border-green-500="{$isActive('/dashboard/applications')}"
class:border-purple-500="{$isActive('/dashboard/databases')}"
class:border-green-500="{$activePage.mainmenu === 'applications'}"
class:border-purple-500="{$activePage.mainmenu === 'databases'}"
>
<img class="w-10 pt-4 pb-4" src="/favicon.png" alt="coolLabs logo" />
<Tooltip position="right" label="Applications">
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 my-4 transition-all duration-100 cursor-pointer"
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 mt-4 transition-all duration-100 cursor-pointer"
on:click="{() => $goto('/dashboard/applications')}"
class:text-green-500="{$isActive('/dashboard/applications') ||
$isActive('/application')}"
class:bg-warmGray-700="{$isActive('/dashboard/applications') ||
$isActive('/application')}"
class:text-green-500="{$activePage.mainmenu === 'applications'}"
class:bg-warmGray-700="{$activePage.mainmenu === 'applications'}"
>
<svg
class="w-8"
@@ -137,12 +211,10 @@
</Tooltip>
<Tooltip position="right" label="Databases">
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-purple-500 transition-all duration-100 cursor-pointer"
class="p-2 hover:bg-warmGray-700 rounded hover:text-purple-500 my-4 transition-all duration-100 cursor-pointer"
on:click="{() => $goto('/dashboard/databases')}"
class:text-purple-500="{$isActive('/dashboard/databases') ||
$isActive('/database')}"
class:bg-warmGray-700="{$isActive('/dashboard/databases') ||
$isActive('/database')}"
class:text-purple-500="{$activePage.mainmenu === 'databases'}"
class:bg-warmGray-700="{$activePage.mainmenu === 'databases'}"
>
<svg
class="w-8"
@@ -160,12 +232,35 @@
</svg>
</div>
</Tooltip>
<Tooltip position="right" label="Services">
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-blue-500 transition-all duration-100 cursor-pointer"
on:click="{() => $goto('/dashboard/services')}"
class:text-blue-500="{$activePage.mainmenu === 'services'}"
class:bg-warmGray-700="{$activePage.mainmenu === 'services'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
></path>
</svg>
</div>
</Tooltip>
<div class="flex-1"></div>
<Tooltip position="right" label="Settings">
<button
class="p-2 hover:bg-warmGray-700 rounded hover:text-yellow-500 transition-all duration-100 cursor-pointer"
class:text-yellow-500="{$isActive('/settings')}"
class:bg-warmGray-700="{$isActive('/settings')}"
class:text-yellow-500="{$activePage.mainmenu === 'settings'}"
class:bg-warmGray-700="{$activePage.mainmenu === 'settings'}"
on:click="{() => $goto('/settings')}"
>
<svg
@@ -222,7 +317,7 @@
{/if}
{#if upgradeAvailable}
<footer
class="absolute bottom-0 right-0 p-4 px-6 w-auto rounded-tl text-white "
class="fixed bottom-0 right-0 p-4 px-6 w-auto rounded-tl text-white hover:scale-110 transform transition duration-100"
>
<div class="flex items-center">
<div></div>

View File

@@ -1,5 +1,5 @@
<script>
import { params } from "@roxi/routify";
import { params, redirect } from "@roxi/routify";
import { onDestroy, onMount } from "svelte";
import { fade } from "svelte/transition";
import { fetch } from "@store";
@@ -15,13 +15,18 @@
});
async function loadLogs() {
const { events, progress } = await $fetch(
try {
const { events, progress } = await $fetch(
`/api/v1/application/deploy/logs/${$params.deployId}`,
);
logs = [...events];
if (progress === "done" || progress === "failed") {
clearInterval(loadLogsInterval);
}
} catch(error) {
$redirect('/dashboard')
}
}
onDestroy(() => {
clearInterval(loadLogsInterval);

View File

@@ -10,7 +10,7 @@
Overview of
<a
target="_blank"
class="text-green-500 hover:underline cursor-pointer px-2"
class="hover:underline cursor-pointer px-2"
href="{'https://' +
$application.publish.domain +
$application.publish.path}">{$application.publish.domain}</a

View File

@@ -1,223 +1,75 @@
<script>
import { params, goto, redirect, isActive } from "@roxi/routify";
import { application, fetch, initialApplication, initConf } from "@store";
import { params, redirect } from "@roxi/routify";
import {
application,
fetch,
initialApplication,
initConf,
deployments,
activePage,
} from "@store";
import { onDestroy } from "svelte";
import { fade } from "svelte/transition";
import Loading from "../../components/Loading.svelte";
import { toast } from "@zerodevx/svelte-toast";
import Tooltip from "../../components/Tooltip/Tooltip.svelte";
import Navbar from "../../components/Application/Navbar.svelte";
$application.repository.organization = $params.organization;
$application.repository.name = $params.name;
$application.repository.branch = $params.branch;
async function setConfiguration() {
try {
const config = await $fetch(`/api/v1/config`, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
},
});
$application = { ...config };
$initConf = JSON.parse(JSON.stringify($application));
} catch (error) {
toast.push("Configuration not found.");
$redirect("/dashboard/applications");
}
}
async function loadConfiguration() {
if (!$isActive("/application/new")) {
try {
const config = await $fetch(`/api/v1/config`, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
},
if (!$activePage.new) {
if ($deployments.length === 0) {
await setConfiguration();
} else {
const found = $deployments.applications.deployed.find(app => {
const { organization, name, branch } = app.configuration;
if (
organization === $application.repository.organization &&
name === $application.repository.name &&
branch === $application.repository.branch
) {
return app;
}
});
$application = { ...config };
$initConf = JSON.parse(JSON.stringify($application));
} catch (error) {
toast.push("Configuration not found.");
$redirect("/dashboard/applications");
if (found) {
$application = { ...found.configuration };
$initConf = JSON.parse(JSON.stringify($application));
} else {
await setConfiguration();
}
}
} else {
$application = JSON.parse(JSON.stringify(initialApplication));
}
}
async function removeApplication() {
await $fetch(`/api/v1/application/remove`, {
body: {
organization: $params.organization,
name: $params.name,
branch: $params.branch,
},
});
toast.push("Application removed.");
$redirect(`/dashboard/applications`);
}
onDestroy(() => {
$application = JSON.parse(JSON.stringify(initialApplication));
});
async function deploy() {
try {
$application.build.pack = $application.build.pack.replace('.','').toLowerCase()
toast.push("Checking inputs.");
await $fetch(`/api/v1/application/check`, {
body: $application,
});
const { nickname, name } = await $fetch(`/api/v1/application/deploy`, {
body: $application,
});
$application.general.nickname = nickname;
$application.build.container.name = name;
$initConf = JSON.parse(JSON.stringify($application));
toast.push("Application deployment queued.");
$redirect(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
);
} catch (error) {
console.log(error);
toast.push(error.error ? error.error : "Ooops something went wrong.");
}
}
</script>
{#await loadConfiguration()}
<Loading />
{:then}
<nav
class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4"
>
<Tooltip position="bottom" label="Deploy" >
<button
disabled="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class:cursor-not-allowed="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class:hover:text-green-500="{$application.publish.domain}"
class:hover:bg-warmGray-700="{$application.publish.domain}"
class:hover:bg-transparent="{$isActive('/application/new')}"
class:text-warmGray-700="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class="icon"
on:click="{deploy}"
>
<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"
><polyline points="16 16 12 12 8 16"></polyline><line
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"></path><polyline
points="16 16 12 12 8 16"></polyline></svg
>
</button>
</Tooltip>
<Tooltip position="bottom" label="Delete" >
<button
disabled="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$isActive('/application/new')}"
class:cursor-not-allowed="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$isActive('/application/new')}"
class:hover:text-red-500="{$application.publish.domain &&
!$isActive('/application/new')}"
class:hover:bg-warmGray-700="{$application.publish.domain &&
!$isActive('/application/new')}"
class:hover:bg-transparent="{$isActive('/application/new')}"
class:text-warmGray-700="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$isActive('/application/new')}"
class="icon"
on:click="{removeApplication}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
<div class="border border-warmGray-700 h-8"></div>
<Tooltip position="bottom" label="Logs" >
<button
class="icon"
class:text-warmGray-700="{$isActive('/application/new')}"
disabled="{$isActive('/application/new')}"
class:hover:text-blue-400="{!$isActive('/application/new')}"
class:hover:bg-transparent="{$isActive('/application/new')}"
class:cursor-not-allowed="{$isActive('/application/new')}"
class:text-blue-400="{$isActive(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
)}"
class:bg-warmGray-700="{$isActive(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
)}"
on:click="{() =>
$goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
)}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
<Tooltip position="bottom-left" label="Configuration" >
<button
class="icon hover:text-yellow-400"
disabled="{$isActive(`/application/new`)}"
class:text-yellow-400="{$isActive(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
) || $isActive(`/application/new`)}"
class:bg-warmGray-700="{$isActive(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
) || $isActive(`/application/new`)}"
on:click="{() =>
$goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
)}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
</nav>
<div class="text-white">
<slot />
</div>
<Navbar />
<div class="text-white">
<slot />
</div>
{/await}

View File

@@ -1,8 +1,6 @@
<script>
import { fetch, deployments } from "@store";
import { onDestroy, onMount } from "svelte";
import { fade } from "svelte/transition";
import { goto, isActive } from "@roxi/routify/runtime";
import { toast } from "@zerodevx/svelte-toast";
let loadDashboardInterval = null;

View File

@@ -5,7 +5,7 @@
function switchTo(application) {
const { branch, name, organization } = application;
$goto(`/application/:organization/:name/:branch`, {
$goto(`/application/:organization/:name/:branch/configuration`, {
name,
organization,
branch,
@@ -39,24 +39,24 @@
</div>
<div in:fade="{{ duration: 100 }}">
{#if $deployments.applications?.deployed.length > 0}
<div class="px-4 mx-auto py-5">
<div class="px-4 mx-auto py-5 z-auto">
<div class="flex items-center justify-center flex-wrap">
{#each $deployments.applications.deployed as application}
<div class="px-4 pb-4">
<div
class="relative rounded-xl py-6 w-52 h-32 bg-warmGray-800 hover:bg-green-500 text-white shadow-md cursor-pointer ease-in-out transform hover:scale-105 duration-200 hover:rotate-1 group"
class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-green-500 text-white shadow-md cursor-pointer ease-in-out transform hover:scale-105 duration-100 group"
on:click="{() =>
switchTo({
branch:
application.Spec.Labels.configuration.repository.branch,
name: application.Spec.Labels.configuration.repository.name,
application.configuration.repository.branch,
name: application.configuration.repository.name,
organization:
application.Spec.Labels.configuration.repository
application.configuration.repository
.organization,
})}"
>
<div class="flex items-center">
{#if application.Spec.Labels.configuration.build.pack === "static"}
{#if application.configuration.build.pack === "static"}
<svg
class="text-white w-10 h-10 absolute top-0 left-0 -m-4"
viewBox="0 0 32 32"
@@ -83,7 +83,74 @@
></defs
></svg
>
{:else if application.Spec.Labels.configuration.build.pack === "nodejs"}
{:else if application.configuration.build.pack === "react"}
<svg
class="text-blue-500 w-10 h-10 absolute top-0 left-0 -m-4"
viewBox="0 0 128 128"
>
<g fill="#61DAFB"
><circle cx="64" cy="64" r="11.4"></circle><path
d="M107.3 45.2c-2.2-.8-4.5-1.6-6.9-2.3.6-2.4 1.1-4.8 1.5-7.1 2.1-13.2-.2-22.5-6.6-26.1-1.9-1.1-4-1.6-6.4-1.6-7 0-15.9 5.2-24.9 13.9-9-8.7-17.9-13.9-24.9-13.9-2.4 0-4.5.5-6.4 1.6-6.4 3.7-8.7 13-6.6 26.1.4 2.3.9 4.7 1.5 7.1-2.4.7-4.7 1.4-6.9 2.3-12.5 4.8-19.3 11.4-19.3 18.8s6.9 14 19.3 18.8c2.2.8 4.5 1.6 6.9 2.3-.6 2.4-1.1 4.8-1.5 7.1-2.1 13.2.2 22.5 6.6 26.1 1.9 1.1 4 1.6 6.4 1.6 7.1 0 16-5.2 24.9-13.9 9 8.7 17.9 13.9 24.9 13.9 2.4 0 4.5-.5 6.4-1.6 6.4-3.7 8.7-13 6.6-26.1-.4-2.3-.9-4.7-1.5-7.1 2.4-.7 4.7-1.4 6.9-2.3 12.5-4.8 19.3-11.4 19.3-18.8s-6.8-14-19.3-18.8zm-14.8-30.5c4.1 2.4 5.5 9.8 3.8 20.3-.3 2.1-.8 4.3-1.4 6.6-5.2-1.2-10.7-2-16.5-2.5-3.4-4.8-6.9-9.1-10.4-13 7.4-7.3 14.9-12.3 21-12.3 1.3 0 2.5.3 3.5.9zm-11.2 59.3c-1.8 3.2-3.9 6.4-6.1 9.6-3.7.3-7.4.4-11.2.4-3.9 0-7.6-.1-11.2-.4-2.2-3.2-4.2-6.4-6-9.6-1.9-3.3-3.7-6.7-5.3-10 1.6-3.3 3.4-6.7 5.3-10 1.8-3.2 3.9-6.4 6.1-9.6 3.7-.3 7.4-.4 11.2-.4 3.9 0 7.6.1 11.2.4 2.2 3.2 4.2 6.4 6 9.6 1.9 3.3 3.7 6.7 5.3 10-1.7 3.3-3.4 6.6-5.3 10zm8.3-3.3c1.5 3.5 2.7 6.9 3.8 10.3-3.4.8-7 1.4-10.8 1.9 1.2-1.9 2.5-3.9 3.6-6 1.2-2.1 2.3-4.2 3.4-6.2zm-25.6 27.1c-2.4-2.6-4.7-5.4-6.9-8.3 2.3.1 4.6.2 6.9.2 2.3 0 4.6-.1 6.9-.2-2.2 2.9-4.5 5.7-6.9 8.3zm-18.6-15c-3.8-.5-7.4-1.1-10.8-1.9 1.1-3.3 2.3-6.8 3.8-10.3 1.1 2 2.2 4.1 3.4 6.1 1.2 2.2 2.4 4.1 3.6 6.1zm-7-25.5c-1.5-3.5-2.7-6.9-3.8-10.3 3.4-.8 7-1.4 10.8-1.9-1.2 1.9-2.5 3.9-3.6 6-1.2 2.1-2.3 4.2-3.4 6.2zm25.6-27.1c2.4 2.6 4.7 5.4 6.9 8.3-2.3-.1-4.6-.2-6.9-.2-2.3 0-4.6.1-6.9.2 2.2-2.9 4.5-5.7 6.9-8.3zm22.2 21l-3.6-6c3.8.5 7.4 1.1 10.8 1.9-1.1 3.3-2.3 6.8-3.8 10.3-1.1-2.1-2.2-4.2-3.4-6.2zm-54.5-16.2c-1.7-10.5-.3-17.9 3.8-20.3 1-.6 2.2-.9 3.5-.9 6 0 13.5 4.9 21 12.3-3.5 3.8-7 8.2-10.4 13-5.8.5-11.3 1.4-16.5 2.5-.6-2.3-1-4.5-1.4-6.6zm-24.7 29c0-4.7 5.7-9.7 15.7-13.4 2-.8 4.2-1.5 6.4-2.1 1.6 5 3.6 10.3 6 15.6-2.4 5.3-4.5 10.5-6 15.5-13.8-4-22.1-10-22.1-15.6zm28.5 49.3c-4.1-2.4-5.5-9.8-3.8-20.3.3-2.1.8-4.3 1.4-6.6 5.2 1.2 10.7 2 16.5 2.5 3.4 4.8 6.9 9.1 10.4 13-7.4 7.3-14.9 12.3-21 12.3-1.3 0-2.5-.3-3.5-.9zm60.8-20.3c1.7 10.5.3 17.9-3.8 20.3-1 .6-2.2.9-3.5.9-6 0-13.5-4.9-21-12.3 3.5-3.8 7-8.2 10.4-13 5.8-.5 11.3-1.4 16.5-2.5.6 2.3 1 4.5 1.4 6.6zm9-15.6c-2 .8-4.2 1.5-6.4 2.1-1.6-5-3.6-10.3-6-15.6 2.4-5.3 4.5-10.5 6-15.5 13.8 4 22.1 10 22.1 15.6 0 4.7-5.8 9.7-15.7 13.4z"
></path></g
>
</svg>
{:else if application.configuration.build.pack === "gatsby"}
<svg class="w-10 h-10 absolute top-0 left-0 -m-4" viewBox="0 0 128 128">
<path fill="#64328B" d="M64,0C28.7,0,0,28.7,0,64v0c0,35.3,28.7,64,64,64s64-28.7,64-64v0C128,28.7,99.3,0,64,0z M13.2,64L64,114.8 C35.9,114.8,13.2,92.1,13.2,64z M75.4,113.5l-60.9-61C19.7,30,39.9,13.2,64,13.2c16.6,0,31.3,7.9,40.5,20.2l-7.5,7.2 C89.7,30.2,77.7,23.5,64,23.5c-17.6,0-32.5,11.2-38.1,26.8C33.1,57,75.4,98.8,78.1,102c12.7-4.7,22.3-15.5,25.4-28.9H81.9v-9.4 l33,0.2C114.8,88.2,98,108.4,75.4,113.5z"></path>
</svg>
{:else if application.configuration.build.pack === "nuxtjs"}
<svg class="w-10 h-10 absolute top-0 left-0 -m-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
<g fill-rule="nonzero" transform="translate(0 50)" fill="none">
<path d="M227.92099 83.45116l-13.6889 24.10141-46.8148-82.44693L23.7037 278.17052h97.3037c0 13.31084 10.61252 24.10142 23.70371 24.10142H23.70371c-8.46771 0-16.29145-4.59601-20.5246-12.05272-4.23315-7.4567-4.23272-16.64312.00114-24.0994L146.89383 13.05492c4.23415-7.45738 12.0596-12.05138 20.5284-12.05138 8.46878 0 16.29423 4.594 20.52839 12.05138l39.97037 70.39623z" fill="#00C58E"/>
<path d="M331.6642 266.11981l-90.05432-158.56724-13.6889-24.10141-13.68888 24.10141-90.04445 158.56724c-4.23385 7.45629-4.23428 16.64271-.00113 24.09941 4.23314 7.4567 12.05689 12.05272 20.5246 12.05272h166.4c8.46946 0 16.29644-4.591 20.532-12.04837 4.23555-7.45736 4.23606-16.64592.00132-24.10376h.01976zM144.7111 278.17052L227.921 131.65399l83.19012 146.51653h-166.4z" fill="#FFF"/>
<path d="M396.04938 290.22123c-4.23344 7.45557-12.05656 12.0507-20.52345 12.0507H311.1111c13.0912 0 23.7037-10.79057 23.7037-24.10141h40.66173L260.09877 74.98553l-18.4889 32.56704L227.921 83.45116l11.65432-20.51634c4.23416-7.45738 12.0596-12.05138 20.5284-12.05138 8.46879 0 16.29423 4.594 20.52839 12.05138l115.41728 203.185c4.23426 7.457 4.23426 16.6444 0 24.1014z" fill="#108775"/>
</g>
</svg>
{:else if application.configuration.build.pack === "svelte"}
<svg
class="w-10 h-10 absolute top-0 left-0 -m-4"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 98.1 118"
style="enable-background:new 0 0 98.1 118;"
xml:space="preserve"
>
<path
fill="#FF3E00"
d="M91.8,15.6C80.9-0.1,59.2-4.7,43.6,5.2L16.1,22.8C8.6,27.5,3.4,35.2,1.9,43.9c-1.3,7.3-0.2,14.8,3.3,21.3 c-2.4,3.6-4,7.6-4.7,11.8c-1.6,8.9,0.5,18.1,5.7,25.4c11,15.7,32.6,20.3,48.2,10.4l27.5-17.5c7.5-4.7,12.7-12.4,14.2-21.1 c1.3-7.3,0.2-14.8-3.3-21.3c2.4-3.6,4-7.6,4.7-11.8C99.2,32.1,97.1,22.9,91.8,15.6"
></path>
<path
fill="#FFFFFF"
d="M40.9,103.9c-8.9,2.3-18.2-1.2-23.4-8.7c-3.2-4.4-4.4-9.9-3.5-15.3c0.2-0.9,0.4-1.7,0.6-2.6l0.5-1.6l1.4,1 c3.3,2.4,6.9,4.2,10.8,5.4l1,0.3l-0.1,1c-0.1,1.4,0.3,2.9,1.1,4.1c1.6,2.3,4.4,3.4,7.1,2.7c0.6-0.2,1.2-0.4,1.7-0.7L65.5,72 c1.4-0.9,2.3-2.2,2.6-3.8c0.3-1.6-0.1-3.3-1-4.6c-1.6-2.3-4.4-3.3-7.1-2.6c-0.6,0.2-1.2,0.4-1.7,0.7l-10.5,6.7 c-1.7,1.1-3.6,1.9-5.6,2.4c-8.9,2.3-18.2-1.2-23.4-8.7c-3.1-4.4-4.4-9.9-3.4-15.3c0.9-5.2,4.1-9.9,8.6-12.7l27.5-17.5 c1.7-1.1,3.6-1.9,5.6-2.5c8.9-2.3,18.2,1.2,23.4,8.7c3.2,4.4,4.4,9.9,3.5,15.3c-0.2,0.9-0.4,1.7-0.7,2.6l-0.5,1.6l-1.4-1 c-3.3-2.4-6.9-4.2-10.8-5.4l-1-0.3l0.1-1c0.1-1.4-0.3-2.9-1.1-4.1c-1.6-2.3-4.4-3.3-7.1-2.6c-0.6,0.2-1.2,0.4-1.7,0.7L32.4,46.1 c-1.4,0.9-2.3,2.2-2.6,3.8s0.1,3.3,1,4.6c1.6,2.3,4.4,3.3,7.1,2.6c0.6-0.2,1.2-0.4,1.7-0.7l10.5-6.7c1.7-1.1,3.6-1.9,5.6-2.5 c8.9-2.3,18.2,1.2,23.4,8.7c3.2,4.4,4.4,9.9,3.5,15.3c-0.9,5.2-4.1,9.9-8.6,12.7l-27.5,17.5C44.8,102.5,42.9,103.3,40.9,103.9"
></path>
</svg>
{:else if application.configuration.build.pack === "vuejs"}
<svg
class="text-green-500 w-10 h-10 absolute top-0 left-0 -m-4"
viewBox="0 0 128 128"
>
<path
d="m-2.3125e-8 8.9337 49.854 0.1586 14.167 24.47 14.432-24.47 49.547-0.1577-63.834 110.14zm126.98 0.6374-24.36 0.0207-38.476 66.052-38.453-66.052-24.749-0.0194 63.211 107.89zm-25.149-0.008-22.745 0.16758l-15.053 24.647-14.817-24.647-22.794-0.1679 37.731 64.476zM25.997 9.3929l23.002 0.0087M25.997 9.3929l23.002 0.0087"
fill="none"></path><path
d="m25.997 9.3929 23.002 0.0087l15.036 24.958 14.983-24.956 22.982-0.0057-37.85 65.655z"
fill="#35495e"></path><path
d="m0.91068 9.5686 25.066-0.1711 38.151 65.658 37.852-65.654 25.11 0.0263-62.966 108.06z"
fill="#41b883"></path>
</svg>
{:else if application.configuration.build.pack === "nextjs"}
<svg
class="text-blue-500 w-10 h-10 absolute top-0 left-0 -m-4 fill-current"
viewBox="0 0 128 128"
>
<path
d="M64 0C28.7 0 0 28.7 0 64s28.7 64 64 64c11.2 0 21.7-2.9 30.8-7.9L48.4 55.3v36.6h-6.8V41.8h6.8l50.5 75.8C116.4 106.2 128 86.5 128 64c0-35.3-28.7-64-64-64zm22.1 84.6l-7.5-11.3V41.8h7.5v42.8z"
></path>
</svg>
{:else if application.configuration.build.pack === "nodejs"}
<svg
class="text-green-400 w-10 h-10 absolute top-0 left-0 -m-4"
xmlns="http://www.w3.org/2000/svg"
@@ -98,7 +165,7 @@
d="M224 508c-6.7 0-13.5-1.8-19.4-5.2l-61.7-36.5c-9.2-5.2-4.7-7-1.7-8 12.3-4.3 14.8-5.2 27.9-12.7 1.4-.8 3.2-.5 4.6.4l47.4 28.1c1.7 1 4.1 1 5.7 0l184.7-106.6c1.7-1 2.8-3 2.8-5V149.3c0-2.1-1.1-4-2.9-5.1L226.8 37.7c-1.7-1-4-1-5.7 0L36.6 144.3c-1.8 1-2.9 3-2.9 5.1v213.1c0 2 1.1 4 2.9 4.9l50.6 29.2c27.5 13.7 44.3-2.4 44.3-18.7V167.5c0-3 2.4-5.3 5.4-5.3h23.4c2.9 0 5.4 2.3 5.4 5.3V378c0 36.6-20 57.6-54.7 57.6-10.7 0-19.1 0-42.5-11.6l-48.4-27.9C8.1 389.2.7 376.3.7 362.4V149.3c0-13.8 7.4-26.8 19.4-33.7L204.6 9c11.7-6.6 27.2-6.6 38.8 0l184.7 106.7c12 6.9 19.4 19.8 19.4 33.7v213.1c0 13.8-7.4 26.7-19.4 33.7L243.4 502.8c-5.9 3.4-12.6 5.2-19.4 5.2zm149.1-210.1c0-39.9-27-50.5-83.7-58-57.4-7.6-63.2-11.5-63.2-24.9 0-11.1 4.9-25.9 47.4-25.9 37.9 0 51.9 8.2 57.7 33.8.5 2.4 2.7 4.2 5.2 4.2h24c1.5 0 2.9-.6 3.9-1.7s1.5-2.6 1.4-4.1c-3.7-44.1-33-64.6-92.2-64.6-52.7 0-84.1 22.2-84.1 59.5 0 40.4 31.3 51.6 81.8 56.6 60.5 5.9 65.2 14.8 65.2 26.7 0 20.6-16.6 29.4-55.5 29.4-48.9 0-59.6-12.3-63.2-36.6-.4-2.6-2.6-4.5-5.3-4.5h-23.9c-3 0-5.3 2.4-5.3 5.3 0 31.1 16.9 68.2 97.8 68.2 58.4-.1 92-23.2 92-63.4z"
></path>
</svg>
{:else if application.Spec.Labels.configuration.build.pack === "php"}
{:else if application.configuration.build.pack === "php"}
<svg
viewBox="0 0 128 128"
class="text-white w-14 h-14 absolute top-0 left-0 -m-6"
@@ -108,7 +175,7 @@
d="M64 33.039c-33.74 0-61.094 13.862-61.094 30.961s27.354 30.961 61.094 30.961 61.094-13.862 61.094-30.961-27.354-30.961-61.094-30.961zm-15.897 36.993c-1.458 1.364-3.077 1.927-4.86 2.507-1.783.581-4.052.461-6.811.461h-6.253l-1.733 10h-7.301l6.515-34h14.04c4.224 0 7.305 1.215 9.242 3.432 1.937 2.217 2.519 5.364 1.747 9.337-.319 1.637-.856 3.159-1.614 4.515-.759 1.357-1.75 2.624-2.972 3.748zm21.311 2.968l2.881-14.42c.328-1.688.208-2.942-.361-3.555-.57-.614-1.782-1.025-3.635-1.025h-5.79l-3.731 19h-7.244l6.515-33h7.244l-1.732 9h6.453c4.061 0 6.861.815 8.402 2.231s2.003 3.356 1.387 6.528l-3.031 15.241h-7.358zm40.259-11.178c-.318 1.637-.856 3.133-1.613 4.488-.758 1.357-1.748 2.598-2.971 3.722-1.458 1.364-3.078 1.927-4.86 2.507-1.782.581-4.053.461-6.812.461h-6.253l-1.732 10h-7.301l6.514-34h14.041c4.224 0 7.305 1.215 9.241 3.432 1.935 2.217 2.518 5.418 1.746 9.39zM95.919 54h-5.001l-2.727 14h4.442c2.942 0 5.136-.29 6.576-1.4 1.442-1.108 2.413-2.828 2.918-5.421.484-2.491.264-4.434-.66-5.458-.925-1.024-2.774-1.721-5.548-1.721zM38.934 54h-5.002l-2.727 14h4.441c2.943 0 5.136-.29 6.577-1.4 1.441-1.108 2.413-2.828 2.917-5.421.484-2.491.264-4.434-.66-5.458s-2.772-1.721-5.546-1.721z"
></path>
</svg>
{:else if application.Spec.Labels.configuration.build.pack === "custom"}
{:else if application.configuration.build.pack === "docker"}
<svg
viewBox="0 0 128 128"
class="w-16 h-16 absolute top-0 left-0 -m-8"
@@ -185,7 +252,7 @@
></path></g
>
</svg>
{:else if application.Spec.Labels.configuration.build.pack === "rust"}
{:else if application.configuration.build.pack === "rust"}
<svg
class="w-14 h-14 absolute top-0 left-0 -m-6"
viewBox="0 0 128 128"
@@ -199,12 +266,12 @@
{/if}
<div class="flex flex-col justify-center items-center w-full">
<div
class="text-xs font-bold text-center w-full text-warmGray-300 group-hover:text-white pb-6"
class="text-base font-bold text-center w-full text-white pb-6"
>
{application.Spec.Labels.configuration.publish
.domain}{application.Spec.Labels.configuration.publish
{application.configuration.publish
.domain}{application.configuration.publish
.path !== "/"
? application.Spec.Labels.configuration.publish.path
? application.configuration.publish.path
: ""}
</div>
<div

View File

@@ -49,6 +49,7 @@
import Postgresql from "../../components/Databases/SVGs/Postgresql.svelte";
import Mysql from "../../components/Databases/SVGs/Mysql.svelte";
import CouchDb from "../../components/Databases/SVGs/CouchDb.svelte";
import Clickhouse from "../../components/Databases/SVGs/Clickhouse.svelte";
const initialNumberOfDBs = $deployments.databases?.deployed.length;
$: if ($deployments.databases?.deployed.length) {
if (initialNumberOfDBs !== $deployments.databases?.deployed.length) {
@@ -86,34 +87,43 @@
<div class="flex items-center justify-center flex-wrap">
{#each $deployments.databases.deployed as database}
<div
in:fade="{{ duration: 200 }}"
in:fade="{{ duration: 200 }}"
class="px-4 pb-4"
on:click="{() =>
$goto(
`/database/${database.Spec.Labels.configuration.general.deployId}/overview`,
`/database/${database.configuration.general.deployId}/configuration`,
)}"
>
<div
class="relative rounded-xl p-6 w-52 h-32 bg-warmGray-800 hover:bg-purple-500 text-white shadow-md cursor-pointer ease-in-out transform hover:scale-105 duration-200 hover:rotate-1 group"
class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-purple-500 text-white shadow-md cursor-pointer ease-in-out transform hover:scale-105 duration-100 group"
>
<div class="flex items-center">
{#if database.Spec.Labels.configuration.general.type == "mongodb"}
{#if database.configuration.general.type == "mongodb"}
<MongoDb customClass="w-10 h-10 absolute top-0 left-0 -m-4" />
{:else if database.Spec.Labels.configuration.general.type == "postgresql"}
{:else if database.configuration.general.type == "postgresql"}
<Postgresql
customClass="w-10 h-10 absolute top-0 left-0 -m-4"
/>
{:else if database.Spec.Labels.configuration.general.type == "mysql"}
{:else if database.configuration.general.type == "mysql"}
<Mysql customClass="w-10 h-10 absolute top-0 left-0 -m-4" />
{:else if database.Spec.Labels.configuration.general.type == "couchdb"}
{:else if database.configuration.general.type == "couchdb"}
<CouchDb
customClass="w-10 h-10 fill-current text-red-600 absolute top-0 left-0 -m-4"
/>
{:else if database.configuration.general.type == "clickhouse"}
<Clickhouse
customClass="w-10 h-10 fill-current text-red-600 absolute top-0 left-0 -m-4"
/>
{/if}
<div
class="text-xs font-bold text-center w-full text-warmGray-300 group-hover:text-white"
>
{database.Spec.Labels.configuration.general.nickname}
<div class="text-center w-full">
<div
class="text-base font-bold text-white group-hover:text-white"
>
{database.configuration.general.nickname}
</div>
<div class="text-xs font-bold text-warmGray-300 ">
({database.configuration.general.type})
</div>
</div>
</div>
</div>
@@ -121,27 +131,26 @@
{/each}
{#if $dbInprogress}
<div class=" px-4 pb-4">
<div class="gradient-border text-xs font-bold text-warmGray-300 pt-6">
<div
class="gradient-border text-xs font-bold text-warmGray-300 pt-6"
>
Working...
</div>
</div>
{/if}
</div>
</div>
{:else}
{#if $dbInprogress}
{:else if $dbInprogress}
<div class="px-4 mx-auto py-5">
<div class="flex items-center justify-center flex-wrap">
<div class=" px-4 pb-4">
<div class="gradient-border text-xs font-bold text-warmGray-300 pt-6">
Working...
<div class="flex items-center justify-center flex-wrap">
<div class=" px-4 pb-4">
<div class="gradient-border text-xs font-bold text-warmGray-300 pt-6">
Working...
</div>
</div>
</div>
</div>
</div>
</div>
{:else}
{:else}
<div class="text-2xl font-bold text-center">No databases found</div>
{/if}
{/if}
</div>

View File

@@ -0,0 +1,74 @@
<script>
import { deployments, dateOptions } from "@store";
import { fade } from "svelte/transition";
import { goto } from "@roxi/routify/runtime";
function switchTo(application) {
const { branch, name, organization } = application;
$goto(`/application/:organization/:name/:branch`, {
name,
organization,
branch,
});
}
</script>
<div
in:fade="{{ duration: 100 }}"
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
>
<div>Services</div>
<button
class="icon p-1 ml-4 bg-blue-500 hover:bg-blue-400"
on:click="{() => $goto('/service/new')}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
</button>
</div>
<div in:fade="{{ duration: 100 }}">
{#if $deployments?.services?.deployed.length > 0}
<div class="px-4 mx-auto py-5">
<div class="flex items-center justify-center flex-wrap">
{#each $deployments?.services?.deployed as service}
<div
in:fade="{{ duration: 200 }}"
class="px-4 pb-4"
on:click="{() =>
$goto(`/service/${service.serviceName}/configuration`)}"
>
<div
class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-blue-500 text-white shadow-md cursor-pointer ease-in-out transform hover:scale-105 duration-100 group"
>
<div class="flex items-center">
{#if service.serviceName == "plausible"}
<div>
<img
alt="plausible logo"
class="w-10 absolute top-0 left-0 -m-6"
src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png"
/>
<div class="text-white font-bold">Plausible Analytics</div>
</div>
{/if}
</div>
</div>
</div>
{/each}
</div>
</div>
{:else}
<div class="text-2xl font-bold text-center">No services found</div>
{/if}
</div>

View File

@@ -1,13 +1,16 @@
<script>
import { fetch, database } from "@store";
import { redirect, params } from "@roxi/routify/runtime";
import { toast } from "@zerodevx/svelte-toast";
import { fade } from "svelte/transition";
import CouchDb from "../../../components/Databases/SVGs/CouchDb.svelte";
import MongoDb from "../../../components/Databases/SVGs/MongoDb.svelte";
import Mysql from "../../../components/Databases/SVGs/Mysql.svelte";
import Postgresql from "../../../components/Databases/SVGs/Postgresql.svelte";
import Loading from "../../../components/Loading.svelte";
import Loading from "../../../components/Loading.svelte";
import PasswordField from "../../../components/PasswordField.svelte";
$: name = $params.name;
async function loadDatabaseConfig() {
@@ -23,7 +26,7 @@ import Loading from "../../../components/Loading.svelte";
</script>
{#await loadDatabaseConfig()}
<Loading/>
<Loading />
{:then}
<div class="min-h-full text-white">
<div
@@ -43,48 +46,41 @@ import Loading from "../../../components/Loading.svelte";
</div>
</div>
</div>
<div
class="text-left max-w-5xl mx-auto px-6"
in:fade="{{ duration: 100 }}"
>
<div class="pb-2 pt-5">
<div class="flex items-center">
<div class="font-bold w-48 text-warmGray-400">Connection string</div>
<div class="text-left max-w-6xl mx-auto px-6" in:fade="{{ duration: 100 }}">
<div class="pb-2 pt-5 space-y-4">
<div class="text-2xl font-bold border-gradient w-32">Database</div>
<div class="flex items-center pt-4">
<div class="font-bold w-64 text-warmGray-400">Connection string</div>
{#if $database.config.general.type === "mongodb"}
<textarea
disabled
class="w-full"
<PasswordField
value="{`mongodb://${$database.envs.MONGODB_USERNAME}:${$database.envs.MONGODB_PASSWORD}@${$database.config.general.deployId}:27017/${$database.envs.MONGODB_DATABASE}`}"
/>
{:else if $database.config.general.type === "postgresql"}
<textarea
disabled
class="w-full"
<PasswordField
value="{`postgresql://${$database.envs.POSTGRESQL_USERNAME}:${$database.envs.POSTGRESQL_PASSWORD}@${$database.config.general.deployId}:5432/${$database.envs.POSTGRESQL_DATABASE}`}"
/>
{:else if $database.config.general.type === "mysql"}
<textarea
disabled
class="w-full"
<PasswordField
value="{`mysql://${$database.envs.MYSQL_USER}:${$database.envs.MYSQL_PASSWORD}@${$database.config.general.deployId}:3306/${$database.envs.MYSQL_DATABASE}`}"
/>
{:else if $database.config.general.type === "couchdb"}
<textarea
disabled
class="w-full"
<PasswordField
value="{`http://${$database.envs.COUCHDB_USER}:${$database.envs.COUCHDB_PASSWORD}@${$database.config.general.deployId}:5984`}"
/>
{:else if $database.config.general.type === "clickhouse"}
<!-- {JSON.stringify($database)} -->
<!-- <textarea
disabled
class="w-full"
value="{`postgresql://${$database.envs.POSTGRESQL_USERNAME}:${$database.envs.POSTGRESQL_PASSWORD}@${$database.config.general.deployId}:5432/${$database.envs.POSTGRESQL_DATABASE}`}"
></textarea> -->
{/if}
</div>
</div>
{#if $database.config.general.type === "mongodb"}
<div class="flex items-center">
<div class="font-bold w-48 text-warmGray-400">Root password</div>
<textarea
disabled
class="w-full"
value="{$database.envs.MONGODB_ROOT_PASSWORD}"
></textarea>
<div class="font-bold w-64 text-warmGray-400">Root password</div>
<PasswordField value="{$database.envs.MONGODB_ROOT_PASSWORD}" />
</div>
{/if}
</div>

View File

@@ -1,10 +1,11 @@
<script>
import { params, goto, isActive, redirect, url } from "@roxi/routify";
import { params, goto, isActive, redirect } from "@roxi/routify";
import { fetch, database, initialDatabase } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import { onDestroy } from "svelte";
import Tooltip from "../../components/Tooltip/Tooltip.svelte";
$: name = $params.name
$: name = $params.name;
onDestroy(() => {
$database = JSON.parse(JSON.stringify(initialDatabase));
@@ -23,47 +24,55 @@
<nav
class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4"
>
<button
title="Delete"
class="icon hover:text-red-500"
on:click="{removeDB}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
<Tooltip position="bottom" label="Delete">
<button
title="Delete"
class="icon hover:text-red-500"
on:click="{removeDB}"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
<div class="border border-warmGray-700 h-8"></div>
<button
title="Configuration"
disabled
class="icon text-warmGray-700 hover:bg-transparent cursor-not-allowed"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
<Tooltip position="bottom-left" label="Configuration">
<button
class="icon hover:text-yellow-400"
disabled="{$isActive(`/database/new`)}"
class:text-yellow-400="{$isActive(`/database/${name}/configuration`) ||
$isActive(`/application/new`)}"
class:bg-warmGray-700="{$isActive(`/database/${name}/configuration`) ||
$isActive(`/database/new`)}"
on:click="{() => $goto(`/database/${name}/configuration`)}"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
</nav>
{/if}
<div class="text-white">

View File

@@ -52,10 +52,10 @@
</h2>
<div class="text-center py-10">
{#if !$loggedIn}
<button class="text-white bg-warmGray-700 hover:bg-warmGray-600 rounded p-2 px-10 font-bold" on:click="{login}">Login with Github</button
<button class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold" on:click="{login}">Login with Github</button
>
{:else}
<button class="text-white bg-warmGray-700 hover:bg-warmGray-600 rounded p-2 px-10 font-bold" on:click="{() => $goto('/dashboard/applications')}"
<button class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold" on:click="{() => $goto('/dashboard/applications')}"
>Get Started</button
>
{/if}

View File

@@ -0,0 +1,74 @@
<script>
import { params, goto, isActive, redirect } from "@roxi/routify";
import { fetch } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import Tooltip from "../../../components/Tooltip/Tooltip.svelte";
$: name = $params.name;
async function removeService() {
await $fetch(`/api/v1/services/${name}`, {
method: "DELETE",
});
toast.push("Service removed.");
$redirect(`/dashboard/services`);
}
</script>
<nav
class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4"
>
<Tooltip position="bottom" label="Delete">
<button
title="Delete"
class="icon hover:text-red-500"
on:click="{removeService}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
<div class="border border-warmGray-700 h-8"></div>
<Tooltip position="bottom-left" label="Configuration">
<button
class="icon hover:text-yellow-400"
disabled="{$isActive(`/application/new`)}"
class:text-yellow-400="{$isActive(`/service/${name}/configuration`) ||
$isActive(`/application/new`)}"
class:bg-warmGray-700="{$isActive(`/service/${name}/configuration`) ||
$isActive(`/application/new`)}"
on:click="{() => $goto(`/service/${name}/configuration`)}"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
></path>
</svg>
</button>
</Tooltip>
</nav>
<div class="text-white">
<slot />
</div>

View File

@@ -0,0 +1,68 @@
<script>
import { fetch } from "@store";
import { redirect, params } from "@roxi/routify/runtime";
import { fade } from "svelte/transition";
import { toast } from "@zerodevx/svelte-toast";
import Loading from "../../../components/Loading.svelte";
import Plausible from "../../../components/Services/Plausible.svelte";
$: name = $params.name;
let service = {};
async function loadServiceConfig() {
if (name) {
try {
service = await $fetch(`/api/v1/services/${name}`);
} catch (error) {
toast.push(`Cannot find service ${name}?!`);
$redirect(`/dashboard/services`);
}
}
}
async function activate() {
try {
await $fetch(`/api/v1/services/deploy/${name}/activate`, {
method: "PATCH",
body: {},
});
toast.push(`All users are activated for Plausible.`);
} catch (error) {
console.log(error);
toast.push(`Ooops, there was an error activating users for Plausible?!`);
}
}
</script>
{#await loadServiceConfig()}
<Loading />
{:then}
<div class="min-h-full text-white">
<div
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
>
<a
href="{service.config.baseURL}"
target="_blank"
class="inline-flex hover:underline cursor-pointer px-2"
>
<div>{name === "plausible" ? "Plausible Analytics" : name}</div>
<div class="px-4">
{#if name === "plausible"}
<img
alt="plausible logo"
class="w-6 mx-auto"
src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png"
/>
{/if}
</div>
</a>
</div>
</div>
<div class="space-y-2 max-w-4xl mx-auto px-6" in:fade="{{ duration: 100 }}">
<div class="block text-center py-4">
{#if name === "plausible"}
<Plausible service="{service}" />
{/if}
</div>
</div>
{/await}

View File

@@ -0,0 +1,5 @@
<script>
import { redirect } from "@roxi/routify";
$redirect("/dashboard/services");
</script>

View File

@@ -0,0 +1,32 @@
<script>
import { params, redirect } from "@roxi/routify";
import { fetch, newService, initialNewService } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import { onDestroy } from "svelte";
import Loading from "../../../../components/Loading.svelte";
$: type = $params.type;
async function checkService() {
try {
await $fetch(`/api/v1/services/${type}`);
$redirect(`/dashboard/services`);
toast.push(
`${
type === "plausible" ? "Plausible Analytics" : type
} already deployed.`,
);
} catch (error) {
//
}
}
onDestroy(() => {
$newService = JSON.parse(JSON.stringify(initialNewService));
});
</script>
{#await checkService()}
<Loading />
{:then}
<div class="text-white">
<slot />
</div>
{/await}

View File

@@ -0,0 +1,136 @@
<script>
import { redirect, params, isActive } from "@roxi/routify/runtime";
import { newService, fetch } from "@store";
import { fade } from "svelte/transition";
import Loading from "../../../../components/Loading.svelte";
import TooltipInfo from "../../../../components/Tooltip/TooltipInfo.svelte";
import { toast } from "@zerodevx/svelte-toast";
$: type = $params.type;
$: deployable =
$newService.baseURL === "" ||
$newService.baseURL === null ||
$newService.email === "" ||
$newService.email === null ||
$newService.userName === "" ||
$newService.userName === null ||
$newService.userPassword === "" ||
$newService.userPassword === null ||
$newService.userPassword.length <= 6 ||
$newService.userPassword !== $newService.userPasswordAgain;
let loading = false;
async function deploy() {
try {
loading = true;
const payload = $newService;
delete payload.userPasswordAgain;
await $fetch(`/api/v1/services/deploy/${type}`, {
body: payload,
});
toast.push(
"Service deployment queued.<br><br><br>It could take 2-5 minutes to be ready, be patient and grab a coffee/tea!",
{ duration: 4000 },
);
$redirect(`/dashboard/services`);
} catch (error) {
console.log(error);
toast.push("Oops something went wrong. See console.log.");
} finally {
loading = false;
}
}
</script>
<div class="min-h-full text-white">
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold">
Deploy new
{#if type === "plausible"}
<span class="text-blue-500 px-2 capitalize">Plausible Analytics</span>
{/if}
</div>
</div>
{#if loading}
<Loading />
{:else}
<div
class="space-y-2 max-w-4xl mx-auto px-6 flex-col text-center"
in:fade="{{ duration: 100 }}"
>
<div class="grid grid-flow-row">
<label for="Domain"
>Domain <TooltipInfo
position="right"
label="{`You will have your Plausible instance at here.`}"
/></label
>
<input
id="Domain"
class:border-red-500="{$newService.baseURL == null ||
$newService.baseURL == ''}"
bind:value="{$newService.baseURL}"
placeholder="analytics.coollabs.io"
/>
</div>
<div class="grid grid-flow-row">
<label for="Email">Email</label>
<input
id="Email"
class:border-red-500="{$newService.email == null ||
$newService.email == ''}"
bind:value="{$newService.email}"
placeholder="hi@coollabs.io"
/>
</div>
<div class="grid grid-flow-row">
<label for="Username">Username </label>
<input
id="Username"
class:border-red-500="{$newService.userName == null ||
$newService.userName == ''}"
bind:value="{$newService.userName}"
placeholder="admin"
/>
</div>
<div class="grid grid-flow-row">
<label for="Password"
>Password <TooltipInfo
position="right"
label="{`Must be at least 7 characters.`}"
/></label
>
<input
id="Password"
type="password"
class:border-red-500="{$newService.userPassword == null ||
$newService.userPassword == '' ||
$newService.userPassword.length <= 6}"
bind:value="{$newService.userPassword}"
/>
</div>
<div class="grid grid-flow-row pb-5">
<label for="PasswordAgain">Password again </label>
<input
id="PasswordAgain"
type="password"
class:placeholder-red-500="{$newService.userPassword !==
$newService.userPasswordAgain}"
class:border-red-500="{$newService.userPassword !==
$newService.userPasswordAgain}"
bind:value="{$newService.userPasswordAgain}"
/>
</div>
<button
disabled="{deployable}"
class:cursor-not-allowed="{deployable}"
class:bg-blue-500="{!deployable}"
class:hover:bg-blue-400="{!deployable}"
class:hover:bg-transparent="{deployable}"
class:text-warmGray-700="{deployable}"
class:text-white="{!deployable}"
class="button p-2"
on:click="{deploy}"
>
Deploy
</button>
</div>
{/if}

View File

@@ -0,0 +1,32 @@
<script>
import { isActive, goto } from "@roxi/routify/runtime";
import { fade } from "svelte/transition";
</script>
<div class="min-h-full text-white">
<div
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
>
Select a service
</div>
</div>
<div
class="text-center space-y-2 max-w-4xl mx-auto px-6"
in:fade="{{ duration: 100 }}"
>
{#if $isActive("/service/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-blue-500 p-2 rounded bg-warmGray-800"
on:click="{$goto('/service/new/plausible')}"
>
<img
alt="plausible logo"
class="w-12 mx-auto"
src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png"
/>
<div class="text-white">Plausible Analytics</div>
</div>
</div>
{/if}
</div>

View File

@@ -6,11 +6,13 @@
let settings = {
allowRegistration: false,
sendErrors: true
};
async function loadSettings() {
const response = await $fetch(`/api/v1/settings`);
settings.allowRegistration = response.settings.allowRegistration;
settings.sendErrors = response.settings.sendErrors;
}
async function changeSettings(value) {
settings[value] = !settings[value];
@@ -23,7 +25,7 @@
}
</script>
<div class="min-h-full text-white" in:fade="{{ duration: 100 }}">
<div class="min-h-full text-white" in:fade="{{ duration: 100 }}">
<div
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
>
@@ -35,10 +37,11 @@
{:then}
<div in:fade="{{ duration: 100 }}">
<div class="max-w-4xl mx-auto px-6 pb-4">
<div class="">
<div class="divide-y divide-gray-200">
<div>
<div class="text-2xl font-bold border-gradient w-32 pt-4 text-white">General</div>
<div class=" pt-4">
<div class="px-4 sm:px-6">
<ul class="mt-2 divide-y divide-gray-200">
<ul class="mt-2 divide-y divide-warmGray-800">
<li class="py-4 flex items-center justify-between">
<div class="flex flex-col">
<p class="text-base font-bold text-warmGray-100">
@@ -101,6 +104,67 @@
</span>
</button>
</li>
<li class="py-4 flex items-center justify-between">
<div class="flex flex-col">
<p class="text-base font-bold text-warmGray-100">
Send errors automatically?
</p>
<p class="text-sm font-medium text-warmGray-400">
Allow to send errors automatically to developer(s) at coolLabs (<a href="https://twitter.com/andrasbacsai" target="_blank" class="underline text-white font-bold hover:text-blue-400">Andras Bacsai</a>). This will help to fix bugs quicker. 🙏
</p>
</div>
<button
type="button"
on:click="{() => changeSettings('sendErrors')}"
aria-pressed="true"
class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200"
class:bg-green-600="{settings.sendErrors}"
class:bg-warmGray-700="{!settings.sendErrors}"
>
<span class="sr-only">Use setting</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"
class:translate-x-5="{settings.sendErrors}"
class:translate-x-0="{!settings.sendErrors}"
>
<span
class=" ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
class:opacity-0="{settings.sendErrors}"
class:opacity-100="{!settings.sendErrors}"
aria-hidden="true"
>
<svg
class="bg-white h-3 w-3 text-red-600"
fill="none"
viewBox="0 0 12 12"
>
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
</span>
<span
class="ease-out duration-100 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
aria-hidden="true"
class:opacity-100="{settings.sendErrors}"
class:opacity-0="{!settings.sendErrors}"
>
<svg
class="bg-white h-3 w-3 text-green-600"
fill="currentColor"
viewBox="0 0 12 12"
>
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
></path>
</svg>
</span>
</span>
</button>
</li>
</ul>
</div>
</div>

View File

@@ -43,7 +43,8 @@ export const fetch = writable(
if (body) {
config.body = JSON.stringify(body)
}
const response = await waitAtLeast(350, window.fetch(url, config))
// const response = await waitAtLeast(350, window.fetch(url, config))
const response = await window.fetch(url, config)
if (response.status >= 200 && response.status <= 299) {
if (response.headers.get('content-type').match(/application\/json/)) {
return await response.json()
@@ -77,10 +78,17 @@ export const fetch = writable(
}
}
)
export const activePage = writable({
application: null,
new: false,
mainmenu: null
})
export const session = writable(sessionStore)
export const loggedIn = derived(session, ($session) => {
return $session.token
})
export const githubRepositories = writable([])
export const githubInstallations = writable({})
export const savedBranch = writable()
export const dateOptions = readable({
@@ -93,7 +101,7 @@ export const dateOptions = readable({
hour12: false
})
export const deployments = writable({})
export const deployments = writable([])
export const initConf = writable({})
export const application = writable({
@@ -149,8 +157,8 @@ export const initialApplication = {
},
repository: {
id: null,
organization: 'new',
name: 'start',
organization: null,
name: null,
branch: null
},
general: {
@@ -219,3 +227,18 @@ export const database = writable({
})
export const dbInprogress = writable(false)
export const newService = writable({
email: null,
userName: 'admin',
userPassword: null,
userPasswordAgain: null,
baseURL: null
})
export const initialNewService = {
email: null,
userName: 'admin',
userPassword: null,
userPasswordAgain: null,
baseURL: null
}

View File

@@ -4,23 +4,29 @@ const defaultBuildAndDeploy = {
}
const templates = {
svelte: {
pack: 'svelte',
...defaultBuildAndDeploy,
directory: 'public',
name: 'Svelte'
},
next: {
pack: 'nodejs',
pack: 'nextjs',
...defaultBuildAndDeploy,
port: 3000,
name: 'Next.js'
name: 'NextJS'
},
nuxt: {
pack: 'nodejs',
pack: 'nuxtjs',
...defaultBuildAndDeploy,
port: 8080,
name: 'Nuxt'
port: 3000,
name: 'NuxtJS'
},
'react-scripts': {
pack: 'static',
pack: 'react',
...defaultBuildAndDeploy,
directory: 'build',
name: 'Create React'
name: 'React'
},
'parcel-bundler': {
pack: 'static',
@@ -28,23 +34,23 @@ const templates = {
directory: 'dist',
name: 'Parcel'
},
'vue-cli-service': {
pack: 'static',
'@vue/cli-service': {
pack: 'vuejs',
...defaultBuildAndDeploy,
directory: 'dist',
name: 'Vue CLI'
name: 'Vue'
},
gatsby: {
pack: 'static',
pack: 'gatsby',
...defaultBuildAndDeploy,
directory: 'public',
name: 'Gatsby'
},
'preact-cli': {
pack: 'static',
pack: 'react',
...defaultBuildAndDeploy,
directory: 'build',
name: 'Preact CLI'
name: 'Preact'
}
}

View File

@@ -16,7 +16,7 @@ module.exports = {
],
preserveHtmlElements: true,
options: {
safelist: [/svelte-/, 'border-green-500', 'border-yellow-300', 'border-red-500', 'hover:border-green-500', 'hover:border-red-200', 'hover:bg-red-200'],
safelist: [/svelte-/, 'border-green-500', 'border-yellow-300', 'border-red-500', 'hover:border-green-500', 'hover:border-red-200', 'hover:bg-red-200', 'hover:bg-warmGray-900', 'hover:bg-transparent'],
defaultExtractor: (content) => {
// WARNING: tailwindExtractor is internal tailwind api
// if this breaks after a tailwind update, report to svite repo

View File

@@ -27,7 +27,9 @@ module.exports = {
'mongodb-memory-server-core',
'unique-names-generator',
'generate-password',
'@iarna/toml'
'@iarna/toml',
'http-errors-enhanced',
'ajv'
]
},
proxy: {