mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-21 05:09:23 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cccb9a5fec | ||
|
|
b416e3ab3e | ||
|
|
e16b7d65d4 | ||
|
|
3744c64459 | ||
|
|
f742c2a3e2 | ||
|
|
142b83cc13 | ||
|
|
bad84289c4 | ||
|
|
166a573392 | ||
|
|
3585e365e7 | ||
|
|
5114ac7721 | ||
|
|
703d941f23 | ||
|
|
c691c52751 | ||
|
|
69f050b864 | ||
|
|
3af1fd4d1b | ||
|
|
bdbf356910 | ||
|
|
5573187d43 | ||
|
|
767c65ab10 | ||
|
|
a1cccd479e | ||
|
|
73d3d43215 | ||
|
|
b91bfa21b3 | ||
|
|
4e69c56bd3 | ||
|
|
3162336fcc |
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.routify
|
.routify
|
||||||
|
.pnpm-store
|
||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ko_fi: andrasbacsai
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,4 +7,5 @@ dist-ssr
|
|||||||
.env
|
.env
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
api/development/console.log
|
api/development/console.log
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
|
.pnpm-store
|
||||||
@@ -11,4 +11,4 @@
|
|||||||
"svelteBracketNewLine": true,
|
"svelteBracketNewLine": true,
|
||||||
"svelteAllowShorthand": true,
|
"svelteAllowShorthand": true,
|
||||||
"plugins": ["prettier-plugin-svelte"]
|
"plugins": ["prettier-plugin-svelte"]
|
||||||
}
|
}
|
||||||
|
|||||||
127
README.md
127
README.md
@@ -1,97 +1,64 @@
|
|||||||
# About
|
|
||||||
|
|
||||||
https://andrasbacsai.com/farewell-netlify-and-heroku-after-3-days-of-coding
|
# Coolify
|
||||||
|
|
||||||
# Features
|
An open-source, hassle-free, self-hostable Heroku & Netlify alternative.
|
||||||
- Deploy your Node.js and static sites just by pushing code to git.
|
|
||||||
- Hassle-free installation and upgrade process.
|
|
||||||
- One-click MongoDB, MySQL, PostgreSQL, CouchDB deployments!
|
|
||||||
|
|
||||||
# Upcoming features
|
## Demo
|
||||||
- Backups & monitoring.
|
|
||||||
- User analytics with privacy in mind.
|
|
||||||
- And much more (see [Roadmap](https://github.com/coollabsio/coolify/projects/1)).
|
|
||||||
|
|
||||||
|
[Small video](https://cdn.coollabs.io/assets/coolify/video/coolify.webm)
|
||||||
|
|
||||||
# FAQ
|
|
||||||
Q: What does Buildpack means?
|
|
||||||
|
|
||||||
A: It defines your application's final form. Static means that it will be hosted as a static site in the end. (see next question below 👇)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Q: How can I build a static site, like Next.js, Sapper (prerendered), etc ?
|
|
||||||
|
|
||||||
A: Use `static` builder and set your `Build command`.
|
|
||||||
|
|
||||||
# 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: `sh <(curl -fsSL https://get.coollabs.io/install.sh) coolify`
|
|
||||||
|
|
||||||
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:
|
## Installation
|
||||||
- Email: Read-only
|
|
||||||
|
|
||||||
Subscribe to events:
|
Installation is automated with the following command:
|
||||||
- Push -> Check!
|
|
||||||
|
|
||||||
### Installation
|
```bash
|
||||||
- Clone this repository: `git clone git@github.com:coollabsio/coolify.git`
|
/bin/bash -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"
|
||||||
- Set `.env` (see `.env.template`)
|
```
|
||||||
- Installation: `bash install.sh all`
|
|
||||||
|
|
||||||
## Updating process
|
|
||||||
### Update everything (proxy+coolify)
|
## Features
|
||||||
- `bash install.sh all`
|
You can deploy any of the following applications, databases and services easily.
|
||||||
|
|
||||||
### Update coolify only
|
(constantly growing lists)
|
||||||
- `bash install.sh coolify`
|
|
||||||
|
|
||||||
### Update proxy only
|
### Applications
|
||||||
- `bash install.sh proxy`
|
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)
|
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
|
||||||
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
|
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
|
||||||
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
|
- 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.
|
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.
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = async function (fastify, opts) {
|
module.exports = async function (fastify, opts) {
|
||||||
// Private routes
|
// Private routes
|
||||||
fastify.register(async function (server) {
|
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/upgrade'), { prefix: '/upgrade' })
|
||||||
server.register(require('./routes/v1/settings'), { prefix: '/settings' })
|
server.register(require('./routes/v1/settings'), { prefix: '/settings' })
|
||||||
server.register(require('./routes/v1/dashboard'), { prefix: '/dashboard' })
|
server.register(require('./routes/v1/dashboard'), { prefix: '/dashboard' })
|
||||||
@@ -12,6 +12,9 @@ module.exports = async function (fastify, opts) {
|
|||||||
server.register(require('./routes/v1/application/deploy'), { prefix: '/application/deploy' })
|
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/application/deploy/logs'), { prefix: '/application/deploy/logs' })
|
||||||
server.register(require('./routes/v1/databases'), { prefix: '/databases' })
|
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
|
// Public routes
|
||||||
fastify.register(require('./routes/v1/verify'), { prefix: '/verify' })
|
fastify.register(require('./routes/v1/verify'), { prefix: '/verify' })
|
||||||
|
|||||||
15
api/buildPacks/docker/index.js
Normal file
15
api/buildPacks/docker/index.js
Normal 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.')
|
||||||
|
}
|
||||||
|
}
|
||||||
25
api/buildPacks/gatsby/index.js
Normal file
25
api/buildPacks/gatsby/index.js
Normal 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)
|
||||||
|
}
|
||||||
24
api/buildPacks/helpers.js
Normal file
24
api/buildPacks/helpers.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const fs = require('fs').promises
|
||||||
|
const { streamEvents, docker } = require('../libs/docker')
|
||||||
|
const buildImageNodeDocker = (configuration) => {
|
||||||
|
return [
|
||||||
|
'FROM node:lts',
|
||||||
|
'WORKDIR /usr/src/app',
|
||||||
|
`COPY ${configuration.build.directory}/package*.json ./`,
|
||||||
|
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
|
||||||
|
`COPY ./${configuration.build.directory} ./`,
|
||||||
|
`RUN ${configuration.build.command.build}`
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
async function buildImage (configuration, cacheBuild) {
|
||||||
|
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, buildImageNodeDocker(configuration))
|
||||||
|
const stream = await docker.engine.buildImage(
|
||||||
|
{ src: ['.'], context: configuration.general.workdir },
|
||||||
|
{ t: `${configuration.build.container.name}:${cacheBuild ? `${configuration.build.container.tag}-cache` : configuration.build.container.tag}` }
|
||||||
|
)
|
||||||
|
await streamEvents(stream, configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildImage
|
||||||
|
}
|
||||||
13
api/buildPacks/index.js
Normal file
13
api/buildPacks/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const Static = require('./static')
|
||||||
|
const react = require('./react')
|
||||||
|
const nextjs = require('./nextjs')
|
||||||
|
const nuxtjs = require('./nuxtjs')
|
||||||
|
const gatsby = require('./gatsby')
|
||||||
|
const vuejs = require('./vuejs')
|
||||||
|
const svelte = require('./svelte')
|
||||||
|
const nodejs = require('./nodejs')
|
||||||
|
const php = require('./php')
|
||||||
|
const docker = require('./docker')
|
||||||
|
const rust = require('./rust')
|
||||||
|
|
||||||
|
module.exports = { static: Static, nodejs, php, docker, rust, react, vuejs, nextjs, nuxtjs, svelte, gatsby }
|
||||||
28
api/buildPacks/nextjs/index.js
Normal file
28
api/buildPacks/nextjs/index.js
Normal 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)
|
||||||
|
}
|
||||||
28
api/buildPacks/nodejs/index.js
Normal file
28
api/buildPacks/nodejs/index.js
Normal 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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
28
api/buildPacks/nuxtjs/index.js
Normal file
28
api/buildPacks/nuxtjs/index.js
Normal 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)
|
||||||
|
}
|
||||||
22
api/buildPacks/php/index.js
Normal file
22
api/buildPacks/php/index.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const fs = require('fs').promises
|
||||||
|
const { streamEvents, docker } = require('../../libs/docker')
|
||||||
|
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
|
||||||
|
const publishPHPDocker = (configuration) => {
|
||||||
|
return [
|
||||||
|
'FROM php:apache',
|
||||||
|
'RUN a2enmod rewrite',
|
||||||
|
'WORKDIR /usr/src/app',
|
||||||
|
`COPY ./${configuration.build.directory} /var/www/html`,
|
||||||
|
'EXPOSE 80',
|
||||||
|
' CMD ["apache2-foreground"]'
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async function (configuration) {
|
||||||
|
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishPHPDocker(configuration))
|
||||||
|
const stream = await docker.engine.buildImage(
|
||||||
|
{ src: ['.'], context: configuration.general.workdir },
|
||||||
|
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
||||||
|
)
|
||||||
|
await streamEvents(stream, configuration)
|
||||||
|
}
|
||||||
25
api/buildPacks/react/index.js
Normal file
25
api/buildPacks/react/index.js
Normal 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)
|
||||||
|
}
|
||||||
60
api/buildPacks/rust/index.js
Normal file
60
api/buildPacks/rust/index.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const fs = require('fs').promises
|
||||||
|
const { streamEvents, docker } = require('../../libs/docker')
|
||||||
|
const { execShellAsync } = require('../../libs/common')
|
||||||
|
const TOML = require('@iarna/toml')
|
||||||
|
|
||||||
|
const publishRustDocker = (configuration, custom) => {
|
||||||
|
return [
|
||||||
|
'FROM rust:latest',
|
||||||
|
'WORKDIR /app',
|
||||||
|
`COPY --from=${configuration.build.container.name}:cache /app/target target`,
|
||||||
|
`COPY --from=${configuration.build.container.name}:cache /usr/local/cargo /usr/local/cargo`,
|
||||||
|
'COPY . .',
|
||||||
|
`RUN cargo build --release --bin ${custom.name}`,
|
||||||
|
'FROM debian:buster-slim',
|
||||||
|
'WORKDIR /app',
|
||||||
|
'RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*',
|
||||||
|
'RUN update-ca-certificates',
|
||||||
|
`COPY --from=${configuration.build.container.name}:cache /app/target/release/${custom.name} ${custom.name}`,
|
||||||
|
`EXPOSE ${configuration.publish.port}`,
|
||||||
|
`CMD ["/app/${custom.name}"]`
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheRustDocker = (configuration, custom) => {
|
||||||
|
return [
|
||||||
|
`FROM rust:latest AS planner-${configuration.build.container.name}`,
|
||||||
|
'WORKDIR /app',
|
||||||
|
'RUN cargo install cargo-chef',
|
||||||
|
'COPY . .',
|
||||||
|
'RUN cargo chef prepare --recipe-path recipe.json',
|
||||||
|
'FROM rust:latest',
|
||||||
|
'WORKDIR /app',
|
||||||
|
'RUN cargo install cargo-chef',
|
||||||
|
`COPY --from=planner-${configuration.build.container.name} /app/recipe.json recipe.json`,
|
||||||
|
'RUN cargo chef cook --release --recipe-path recipe.json'
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async function (configuration) {
|
||||||
|
const cargoToml = await execShellAsync(`cat ${configuration.general.workdir}/Cargo.toml`)
|
||||||
|
const parsedToml = TOML.parse(cargoToml)
|
||||||
|
const custom = {
|
||||||
|
name: parsedToml.package.name
|
||||||
|
}
|
||||||
|
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, cacheRustDocker(configuration, custom))
|
||||||
|
|
||||||
|
let stream = await docker.engine.buildImage(
|
||||||
|
{ src: ['.'], context: configuration.general.workdir },
|
||||||
|
{ t: `${configuration.build.container.name}:cache` }
|
||||||
|
)
|
||||||
|
await streamEvents(stream, configuration)
|
||||||
|
|
||||||
|
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishRustDocker(configuration, custom))
|
||||||
|
|
||||||
|
stream = await docker.engine.buildImage(
|
||||||
|
{ src: ['.'], context: configuration.general.workdir },
|
||||||
|
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
||||||
|
)
|
||||||
|
await streamEvents(stream, configuration)
|
||||||
|
}
|
||||||
27
api/buildPacks/static/index.js
Normal file
27
api/buildPacks/static/index.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const fs = require('fs').promises
|
||||||
|
const { buildImage } = require('../helpers')
|
||||||
|
const { streamEvents, docker } = require('../../libs/docker')
|
||||||
|
|
||||||
|
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
|
||||||
|
const publishStaticDocker = (configuration) => {
|
||||||
|
return [
|
||||||
|
'FROM nginx:stable-alpine',
|
||||||
|
'COPY nginx.conf /etc/nginx/nginx.conf',
|
||||||
|
'WORKDIR /usr/share/nginx/html',
|
||||||
|
configuration.build.command.build
|
||||||
|
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`
|
||||||
|
: `COPY ./${configuration.build.directory} ./`,
|
||||||
|
'EXPOSE 80',
|
||||||
|
'CMD ["nginx", "-g", "daemon off;"]'
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async function (configuration) {
|
||||||
|
if (configuration.build.command.build) await buildImage(configuration, true)
|
||||||
|
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration))
|
||||||
|
const stream = await docker.engine.buildImage(
|
||||||
|
{ src: ['.'], context: configuration.general.workdir },
|
||||||
|
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
||||||
|
)
|
||||||
|
await streamEvents(stream, configuration)
|
||||||
|
}
|
||||||
25
api/buildPacks/svelte/index.js
Normal file
25
api/buildPacks/svelte/index.js
Normal 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)
|
||||||
|
}
|
||||||
25
api/buildPacks/vuejs/index.js
Normal file
25
api/buildPacks/vuejs/index.js
Normal 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)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const packs = require('../../../packs')
|
const packs = require('../../../buildPacks')
|
||||||
const { saveAppLog } = require('../../logging')
|
const { saveAppLog } = require('../../logging')
|
||||||
const Deployment = require('../../../models/Deployment')
|
const Deployment = require('../../../models/Deployment')
|
||||||
|
|
||||||
@@ -9,26 +9,20 @@ module.exports = async function (configuration) {
|
|||||||
|
|
||||||
const execute = packs[configuration.build.pack]
|
const execute = packs[configuration.build.pack]
|
||||||
if (execute) {
|
if (execute) {
|
||||||
|
await Deployment.findOneAndUpdate(
|
||||||
|
{ repoId: id, branch, deployId, organization, name, domain },
|
||||||
|
{ repoId: id, branch, deployId, organization, name, domain, progress: 'inprogress' })
|
||||||
|
await saveAppLog('### Building application.', configuration)
|
||||||
|
await execute(configuration)
|
||||||
|
await saveAppLog('### Building done.', configuration)
|
||||||
|
} else {
|
||||||
try {
|
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(
|
await Deployment.findOneAndUpdate(
|
||||||
{ repoId: id, branch, deployId, organization, name, domain },
|
{ repoId: id, branch, deployId, organization, name, domain },
|
||||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
||||||
if (error.stack) throw { error: error.stack, type: 'server' }
|
} catch (error) {
|
||||||
throw { error, type: 'app' }
|
// Hmm.
|
||||||
}
|
}
|
||||||
} else {
|
throw new Error('No buildpack found.')
|
||||||
await Deployment.findOneAndUpdate(
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain },
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
|
||||||
throw { error: 'No buildpack found.', type: 'app' }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,35 @@
|
|||||||
const { docker } = require('../../docker')
|
const { docker } = require('../../docker')
|
||||||
const { execShellAsync, delay } = require('../../common')
|
const { execShellAsync } = require('../../common')
|
||||||
const Deployment = require('../../../models/Deployment')
|
const Deployment = require('../../../models/Deployment')
|
||||||
|
|
||||||
async function purgeOldThings () {
|
async function purgeImagesContainers (configuration, deleteAll = false) {
|
||||||
try {
|
const { name, tag } = configuration.build.container
|
||||||
await docker.engine.pruneImages()
|
await execShellAsync('docker container prune -f')
|
||||||
await docker.engine.pruneContainers()
|
if (deleteAll) {
|
||||||
} catch (error) {
|
const IDsToDelete = (await execShellAsync(`docker images ls --filter=reference='${name}' --format '{{json .ID }}'`)).trim().replace(/"/g, '').split('\n')
|
||||||
throw { error, type: 'server' }
|
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 cleanup (configuration) {
|
async function cleanupStuckedDeploymentsInDB () {
|
||||||
const { id } = configuration.repository
|
// Cleanup stucked deployments.
|
||||||
const deployId = configuration.general.deployId
|
await Deployment.updateMany(
|
||||||
try {
|
{ progress: { $in: ['queued', 'inprogress'] } },
|
||||||
// Cleanup stucked deployments.
|
{ progress: 'failed' }
|
||||||
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 deleteSameDeployments (configuration) {
|
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 => {
|
||||||
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)
|
||||||
const running = JSON.parse(s.Spec.Labels.configuration)
|
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
|
||||||
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']}`)
|
||||||
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`)
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
throw { error, type: 'server' }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { cleanup, deleteSameDeployments, purgeOldThings }
|
module.exports = { cleanupStuckedDeploymentsInDB, deleteSameDeployments, purgeImagesContainers }
|
||||||
|
|||||||
@@ -1,47 +1,54 @@
|
|||||||
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
|
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
|
||||||
const cuid = require('cuid')
|
const cuid = require('cuid')
|
||||||
const { execShellAsync } = require('../common')
|
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
|
const { docker } = require('../docker')
|
||||||
|
const { execShellAsync, baseServiceConfiguration } = require('../common')
|
||||||
|
|
||||||
function getUniq () {
|
function getUniq () {
|
||||||
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 })
|
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDefaultConfiguration (configuration) {
|
function setDefaultConfiguration (configuration) {
|
||||||
try {
|
const nickname = getUniq()
|
||||||
const nickname = getUniq()
|
const deployId = cuid()
|
||||||
const deployId = cuid()
|
|
||||||
|
|
||||||
const shaBase = JSON.stringify({ repository: configuration.repository })
|
const shaBase = JSON.stringify({ repository: configuration.repository })
|
||||||
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex')
|
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex')
|
||||||
|
|
||||||
configuration.build.container.name = sha256.slice(0, 15)
|
configuration.build.container.name = sha256.slice(0, 15)
|
||||||
|
|
||||||
configuration.general.nickname = nickname
|
configuration.general.nickname = nickname
|
||||||
configuration.general.deployId = deployId
|
configuration.general.deployId = deployId
|
||||||
configuration.general.workdir = `/tmp/${deployId}`
|
configuration.general.workdir = `/tmp/${deployId}`
|
||||||
|
|
||||||
if (!configuration.publish.path) configuration.publish.path = '/'
|
if (!configuration.publish.path) configuration.publish.path = '/'
|
||||||
if (!configuration.publish.port) configuration.publish.port = configuration.build.pack === 'static' ? 80 : 3000
|
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') {
|
||||||
if (configuration.build.pack === 'static') {
|
configuration.publish.port = 3000
|
||||||
if (!configuration.build.command.installation) configuration.build.command.installation = 'yarn install'
|
} else {
|
||||||
if (!configuration.build.directory) configuration.build.directory = '/'
|
configuration.publish.port = 80
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configuration.build.pack === 'nodejs') {
|
|
||||||
if (!configuration.build.command.installation) configuration.build.command.installation = 'yarn install'
|
|
||||||
if (!configuration.build.directory) configuration.build.directory = '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
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, services) {
|
async function updateServiceLabels (configuration) {
|
||||||
// In case of any failure during deployment, still update the current configuration.
|
// In case of any failure during deployment, still update the current configuration.
|
||||||
|
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||||
const found = services.find(s => {
|
const found = services.find(s => {
|
||||||
const config = JSON.parse(s.Spec.Labels.configuration)
|
const config = JSON.parse(s.Spec.Labels.configuration)
|
||||||
if (config.repository.id === configuration.repository.id && config.repository.branch === configuration.repository.branch) {
|
if (config.repository.id === configuration.repository.id && config.repository.branch === configuration.repository.branch) {
|
||||||
@@ -51,12 +58,56 @@ async function updateServiceLabels (configuration, services) {
|
|||||||
})
|
})
|
||||||
if (found) {
|
if (found) {
|
||||||
const { ID } = found
|
const { ID } = found
|
||||||
try {
|
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration }
|
||||||
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}`)
|
||||||
execShellAsync(`docker service update --label-add configuration='${JSON.stringify(Labels)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${configuration.build.container.tag}' ${ID}`)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = { setDefaultConfiguration, updateServiceLabels }
|
|
||||||
|
async function precheckDeployment ({ services, configuration }) {
|
||||||
|
let foundService = false
|
||||||
|
let configChanged = false
|
||||||
|
let imageChanged = false
|
||||||
|
|
||||||
|
let forceUpdate = false
|
||||||
|
|
||||||
|
for (const service of services) {
|
||||||
|
const running = JSON.parse(service.Spec.Labels.configuration)
|
||||||
|
if (running) {
|
||||||
|
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
|
||||||
|
// Base service configuration changed
|
||||||
|
if (!running.build.container.baseSHA || running.build.container.baseSHA !== configuration.build.container.baseSHA) {
|
||||||
|
forceUpdate = true
|
||||||
|
}
|
||||||
|
// If the deployment is in error state, forceUpdate
|
||||||
|
const state = await execShellAsync(`docker stack ps ${running.build.container.name} --format '{{ json . }}'`)
|
||||||
|
const isError = state.split('\n').filter(n => n).map(s => JSON.parse(s)).filter(n => n.DesiredState !== 'Running' && n.Image.split(':')[1] === running.build.container.tag)
|
||||||
|
if (isError.length > 0) forceUpdate = true
|
||||||
|
foundService = true
|
||||||
|
|
||||||
|
const runningWithoutContainer = JSON.parse(JSON.stringify(running))
|
||||||
|
delete runningWithoutContainer.build.container
|
||||||
|
|
||||||
|
const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration))
|
||||||
|
delete configurationWithoutContainer.build.container
|
||||||
|
|
||||||
|
// If only the configuration changed
|
||||||
|
if (JSON.stringify(runningWithoutContainer.build) !== JSON.stringify(configurationWithoutContainer.build) || JSON.stringify(runningWithoutContainer.publish) !== JSON.stringify(configurationWithoutContainer.publish)) configChanged = true
|
||||||
|
// If only the image changed
|
||||||
|
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true
|
||||||
|
// If build pack changed, forceUpdate the service
|
||||||
|
if (running.build.pack !== configuration.build.pack) forceUpdate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (forceUpdate) {
|
||||||
|
imageChanged = false
|
||||||
|
configChanged = false
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
foundService,
|
||||||
|
imageChanged,
|
||||||
|
configChanged,
|
||||||
|
forceUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = { setDefaultConfiguration, updateServiceLabels, precheckDeployment, baseServiceConfiguration }
|
||||||
|
|||||||
@@ -1,53 +1,65 @@
|
|||||||
const fs = require('fs').promises
|
const fs = require('fs').promises
|
||||||
module.exports = async function (configuration) {
|
module.exports = async function (configuration) {
|
||||||
|
const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby']
|
||||||
try {
|
try {
|
||||||
// TODO: Do it better.
|
// TODO: Write full .dockerignore for all deployments!!
|
||||||
await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules')
|
if (configuration.build.pack === 'php') {
|
||||||
await fs.writeFile(
|
await fs.writeFile(`${configuration.general.workdir}/.htaccess`, `
|
||||||
`${configuration.general.workdir}/nginx.conf`,
|
RewriteEngine On
|
||||||
`user nginx;
|
RewriteBase /
|
||||||
worker_processes auto;
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
error_log /var/log/nginx/error.log warn;
|
RewriteRule ^(.+)$ index.php [QSA,L]
|
||||||
pid /var/run/nginx.pid;
|
`)
|
||||||
|
}
|
||||||
events {
|
// await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules')
|
||||||
worker_connections 1024;
|
if (staticDeployments.includes(configuration.build.pack)) {
|
||||||
}
|
await fs.writeFile(
|
||||||
|
`${configuration.general.workdir}/nginx.conf`,
|
||||||
http {
|
`user nginx;
|
||||||
include /etc/nginx/mime.types;
|
worker_processes auto;
|
||||||
|
|
||||||
access_log off;
|
error_log /var/log/nginx/error.log warn;
|
||||||
sendfile on;
|
pid /var/run/nginx.pid;
|
||||||
#tcp_nopush on;
|
|
||||||
keepalive_timeout 65;
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
server {
|
}
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html;
|
access_log off;
|
||||||
index index.html;
|
sendfile on;
|
||||||
try_files $uri $uri/index.html $uri/ /index.html =404;
|
#tcp_nopush on;
|
||||||
}
|
keepalive_timeout 65;
|
||||||
|
|
||||||
error_page 404 /50x.html;
|
server {
|
||||||
|
listen 80;
|
||||||
# redirect server error pages to the static page /50x.html
|
server_name localhost;
|
||||||
#
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
location / {
|
||||||
location = /50x.html {
|
root /usr/share/nginx/html;
|
||||||
root /usr/share/nginx/html;
|
index index.html;
|
||||||
}
|
try_files $uri $uri/index.html $uri/ /index.html =404;
|
||||||
|
}
|
||||||
}
|
|
||||||
|
error_page 404 /50x.html;
|
||||||
}
|
|
||||||
`
|
# redirect server error pages to the static page /50x.html
|
||||||
)
|
#
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw { error, type: 'server' }
|
throw new Error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,97 +1,76 @@
|
|||||||
const yaml = require('js-yaml')
|
const yaml = require('js-yaml')
|
||||||
|
const fs = require('fs').promises
|
||||||
const { execShellAsync } = require('../../common')
|
const { execShellAsync } = require('../../common')
|
||||||
const { docker } = require('../../docker')
|
const { docker } = require('../../docker')
|
||||||
const { saveAppLog } = require('../../logging')
|
const { saveAppLog } = require('../../logging')
|
||||||
const { deleteSameDeployments } = require('../cleanup')
|
const { deleteSameDeployments } = require('../cleanup')
|
||||||
const fs = require('fs').promises
|
|
||||||
|
|
||||||
module.exports = async function (configuration, configChanged, imageChanged) {
|
module.exports = async function (configuration, imageChanged) {
|
||||||
try {
|
const generateEnvs = {}
|
||||||
const generateEnvs = {}
|
for (const secret of configuration.publish.secrets) {
|
||||||
for (const secret of configuration.publish.secrets) {
|
generateEnvs[secret.name] = secret.value
|
||||||
generateEnvs[secret.name] = secret.value
|
}
|
||||||
}
|
const containerName = configuration.build.container.name
|
||||||
const containerName = configuration.build.container.name
|
|
||||||
const stack = {
|
// Only save SHA256 of it in the configuration label
|
||||||
version: '3.8',
|
const baseServiceConfiguration = configuration.baseServiceConfiguration
|
||||||
services: {
|
delete configuration.baseServiceConfiguration
|
||||||
[containerName]: {
|
|
||||||
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
|
const stack = {
|
||||||
networks: [`${docker.network}`],
|
version: '3.8',
|
||||||
environment: generateEnvs,
|
services: {
|
||||||
deploy: {
|
[containerName]: {
|
||||||
replicas: 1,
|
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
|
||||||
restart_policy: {
|
networks: [`${docker.network}`],
|
||||||
condition: 'on-failure',
|
environment: generateEnvs,
|
||||||
delay: '5s',
|
deploy: {
|
||||||
max_attempts: 1,
|
...baseServiceConfiguration,
|
||||||
window: '120s'
|
labels: [
|
||||||
},
|
'managedBy=coolify',
|
||||||
update_config: {
|
'type=application',
|
||||||
parallelism: 1,
|
'configuration=' + JSON.stringify(configuration),
|
||||||
delay: '10s',
|
'traefik.enable=true',
|
||||||
order: 'start-first'
|
'traefik.http.services.' +
|
||||||
},
|
|
||||||
rollback_config: {
|
|
||||||
parallelism: 1,
|
|
||||||
delay: '10s',
|
|
||||||
order: 'start-first'
|
|
||||||
},
|
|
||||||
labels: [
|
|
||||||
'managedBy=coolify',
|
|
||||||
'type=application',
|
|
||||||
'configuration=' + JSON.stringify(configuration),
|
|
||||||
'traefik.enable=true',
|
|
||||||
'traefik.http.services.' +
|
|
||||||
configuration.build.container.name +
|
configuration.build.container.name +
|
||||||
`.loadbalancer.server.port=${configuration.publish.port}`,
|
`.loadbalancer.server.port=${configuration.publish.port}`,
|
||||||
'traefik.http.routers.' +
|
'traefik.http.routers.' +
|
||||||
configuration.build.container.name +
|
configuration.build.container.name +
|
||||||
'.entrypoints=websecure',
|
'.entrypoints=websecure',
|
||||||
'traefik.http.routers.' +
|
'traefik.http.routers.' +
|
||||||
configuration.build.container.name +
|
configuration.build.container.name +
|
||||||
'.rule=Host(`' +
|
'.rule=Host(`' +
|
||||||
configuration.publish.domain +
|
configuration.publish.domain +
|
||||||
'`) && PathPrefix(`' +
|
'`) && PathPrefix(`' +
|
||||||
configuration.publish.path +
|
configuration.publish.path +
|
||||||
'`)',
|
'`)',
|
||||||
'traefik.http.routers.' +
|
'traefik.http.routers.' +
|
||||||
configuration.build.container.name +
|
configuration.build.container.name +
|
||||||
'.tls.certresolver=letsencrypt',
|
'.tls.certresolver=letsencrypt',
|
||||||
'traefik.http.routers.' +
|
'traefik.http.routers.' +
|
||||||
configuration.build.container.name +
|
configuration.build.container.name +
|
||||||
'.middlewares=global-compress'
|
'.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 (configChanged) {
|
|
||||||
// console.log('configuration changed')
|
|
||||||
await execShellAsync(
|
|
||||||
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}`
|
|
||||||
)
|
|
||||||
} else 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')
|
|
||||||
await deleteSameDeployments(configuration)
|
|
||||||
await execShellAsync(
|
|
||||||
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveAppLog('### Published done!', configuration)
|
|
||||||
} catch (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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
const jwt = require('jsonwebtoken')
|
const jwt = require('jsonwebtoken')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const { execShellAsync, cleanupTmp } = require('../../common')
|
const { execShellAsync } = require('../../common')
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
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 {
|
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, {
|
const jwtToken = jwt.sign(payload, githubPrivateKey, {
|
||||||
algorithm: 'RS256'
|
algorithm: 'RS256'
|
||||||
})
|
})
|
||||||
@@ -29,7 +31,7 @@ module.exports = async function (configuration) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
await execShellAsync(
|
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 = (
|
configuration.build.container.tag = (
|
||||||
await execShellAsync(`cd ${configuration.general.workdir}/ && git rev-parse HEAD`)
|
await execShellAsync(`cd ${configuration.general.workdir}/ && git rev-parse HEAD`)
|
||||||
@@ -37,8 +39,6 @@ module.exports = async function (configuration) {
|
|||||||
.replace('\n', '')
|
.replace('\n', '')
|
||||||
.slice(0, 7)
|
.slice(0, 7)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cleanupTmp(workdir)
|
throw new Error(error)
|
||||||
if (error.stack) console.log(error.stack)
|
|
||||||
throw { error, type: 'server' }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,27 @@
|
|||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
|
|
||||||
const { saveServerLog } = require('../logging')
|
|
||||||
const { cleanupTmp } = require('../common')
|
|
||||||
|
|
||||||
const { saveAppLog } = require('../logging')
|
const { saveAppLog } = require('../logging')
|
||||||
const copyFiles = require('./deploy/copyFiles')
|
const copyFiles = require('./deploy/copyFiles')
|
||||||
const buildContainer = require('./build/container')
|
const buildContainer = require('./build/container')
|
||||||
const deploy = require('./deploy/deploy')
|
const deploy = require('./deploy/deploy')
|
||||||
const Deployment = require('../../models/Deployment')
|
const Deployment = require('../../models/Deployment')
|
||||||
const { cleanup, purgeOldThings } = require('./cleanup')
|
|
||||||
const { updateServiceLabels } = require('./configuration')
|
const { updateServiceLabels } = require('./configuration')
|
||||||
|
|
||||||
async function queueAndBuild (configuration, services, configChanged, imageChanged) {
|
async function queueAndBuild (configuration, imageChanged) {
|
||||||
const { id, organization, name, branch } = configuration.repository
|
const { id, organization, name, branch } = configuration.repository
|
||||||
const { domain } = configuration.publish
|
const { domain } = configuration.publish
|
||||||
const { deployId, nickname, workdir } = configuration.general
|
const { deployId, nickname } = configuration.general
|
||||||
try {
|
await new Deployment({
|
||||||
await new Deployment({
|
repoId: id, branch, deployId, domain, organization, name, nickname
|
||||||
repoId: id, branch, deployId, domain, organization, name, nickname
|
}).save()
|
||||||
}).save()
|
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration)
|
||||||
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration)
|
await copyFiles(configuration)
|
||||||
await copyFiles(configuration)
|
await buildContainer(configuration)
|
||||||
await buildContainer(configuration)
|
await deploy(configuration, imageChanged)
|
||||||
await deploy(configuration, configChanged, imageChanged)
|
await Deployment.findOneAndUpdate(
|
||||||
await Deployment.findOneAndUpdate(
|
{ repoId: id, branch, deployId, organization, name, domain },
|
||||||
{ repoId: id, branch, deployId, organization, name, domain },
|
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' })
|
||||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' })
|
await updateServiceLabels(configuration)
|
||||||
await updateServiceLabels(configuration, services)
|
|
||||||
cleanupTmp(workdir)
|
|
||||||
await purgeOldThings()
|
|
||||||
} catch (error) {
|
|
||||||
await cleanup(configuration)
|
|
||||||
cleanupTmp(workdir)
|
|
||||||
const { type } = error.error
|
|
||||||
if (type === 'app') {
|
|
||||||
await saveAppLog(error.error, configuration, true)
|
|
||||||
} else {
|
|
||||||
await saveServerLog({ event: error.error, configuration })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { queueAndBuild }
|
module.exports = { queueAndBuild }
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ const User = require('../models/User')
|
|||||||
const algorithm = 'aes-256-cbc'
|
const algorithm = 'aes-256-cbc'
|
||||||
const key = process.env.SECRETS_ENCRYPTION_KEY
|
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) {
|
function delay (t) {
|
||||||
return new Promise(function (resolve) {
|
return new Promise(function (resolve) {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
@@ -15,12 +33,16 @@ function delay (t) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function verifyUserId (authorization) {
|
async function verifyUserId (authorization) {
|
||||||
const token = authorization.split(' ')[1]
|
try {
|
||||||
const verify = jsonwebtoken.verify(token, process.env.JWT_SIGN_KEY)
|
const token = authorization.split(' ')[1]
|
||||||
const found = await User.findOne({ uid: verify.jti })
|
const verify = jsonwebtoken.verify(token, process.env.JWT_SIGN_KEY)
|
||||||
if (found) {
|
const found = await User.findOne({ uid: verify.jti })
|
||||||
return true
|
if (found) {
|
||||||
} else {
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,5 +112,6 @@ module.exports = {
|
|||||||
checkImageAvailable,
|
checkImageAvailable,
|
||||||
encryptData,
|
encryptData,
|
||||||
decryptData,
|
decryptData,
|
||||||
verifyUserId
|
verifyUserId,
|
||||||
|
baseServiceConfiguration
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,24 +8,21 @@ const docker = {
|
|||||||
network: process.env.DOCKER_NETWORK
|
network: process.env.DOCKER_NETWORK
|
||||||
}
|
}
|
||||||
async function streamEvents (stream, configuration) {
|
async function streamEvents (stream, configuration) {
|
||||||
try {
|
await new Promise((resolve, reject) => {
|
||||||
await new Promise((resolve, reject) => {
|
docker.engine.modem.followProgress(stream, onFinished, onProgress)
|
||||||
docker.engine.modem.followProgress(stream, onFinished, onProgress)
|
function onFinished (err, res) {
|
||||||
function onFinished (err, res) {
|
if (err) reject(err)
|
||||||
if (err) reject(err)
|
resolve(res)
|
||||||
resolve(res)
|
}
|
||||||
}
|
function onProgress (event) {
|
||||||
function onProgress (event) {
|
if (event.error) {
|
||||||
if (event.error) {
|
saveAppLog(event.error, configuration, true)
|
||||||
reject(event.error)
|
reject(event.error)
|
||||||
return
|
} else if (event.stream) {
|
||||||
}
|
|
||||||
saveAppLog(event.stream, configuration)
|
saveAppLog(event.stream, configuration)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
} catch (error) {
|
})
|
||||||
throw { error, type: 'app' }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { streamEvents, docker }
|
module.exports = { streamEvents, docker }
|
||||||
|
|||||||
75
api/libs/http-error/handlers.js
Normal file
75
api/libs/http-error/handlers.js
Normal 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;
|
||||||
58
api/libs/http-error/index.js
Normal file
58
api/libs/http-error/index.js
Normal 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);
|
||||||
|
}
|
||||||
6
api/libs/http-error/interfaces.js
Normal file
6
api/libs/http-error/interfaces.js
Normal 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');
|
||||||
31
api/libs/http-error/utils.js
Normal file
31
api/libs/http-error/utils.js
Normal 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;
|
||||||
239
api/libs/http-error/validation.js
Normal file
239
api/libs/http-error/validation.js
Normal 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;
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
|
const dayjs = require('dayjs')
|
||||||
|
const axios = require('axios')
|
||||||
|
|
||||||
const ApplicationLog = require('../models/Logs/Application')
|
const ApplicationLog = require('../models/Logs/Application')
|
||||||
const ServerLog = require('../models/Logs/Server')
|
const ServerLog = require('../models/Logs/Server')
|
||||||
const dayjs = require('dayjs')
|
const Settings = require('../models/Settings')
|
||||||
|
const { version } = require('../../package.json')
|
||||||
|
|
||||||
function generateTimestamp () {
|
function generateTimestamp () {
|
||||||
return `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} `
|
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) {
|
async function saveAppLog (event, configuration, isError) {
|
||||||
try {
|
try {
|
||||||
@@ -12,25 +20,12 @@ async function saveAppLog (event, configuration, isError) {
|
|||||||
const repoId = configuration.repository.id
|
const repoId = configuration.repository.id
|
||||||
const branch = configuration.repository.branch
|
const branch = configuration.repository.branch
|
||||||
if (isError) {
|
if (isError) {
|
||||||
// console.log(event, config, isError)
|
const clearedEvent = '[ERROR 😱] ' + generateTimestamp() + event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '')
|
||||||
let clearedEvent = null
|
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (event && event !== '\n') {
|
if (event && event !== '\n') {
|
||||||
const clearedEvent = '[INFO] ' + generateTimestamp() + event.replace(/(\r\n|\n|\r)/gm, '')
|
const clearedEvent = '[INFO] ' + generateTimestamp() + event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '')
|
||||||
try {
|
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
|
||||||
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -39,16 +34,14 @@ async function saveAppLog (event, configuration, isError) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveServerLog ({ event, configuration, type }) {
|
async function saveServerLog (error) {
|
||||||
if (configuration) {
|
const settings = await Settings.findOne({ applicationName: 'coolify' })
|
||||||
const deployId = configuration.general.deployId
|
const payload = { message: error.message, stack: error.stack, type: error.type || 'spaghetticode', version }
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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 = {
|
module.exports = {
|
||||||
saveAppLog,
|
saveAppLog,
|
||||||
saveServerLog
|
saveServerLog
|
||||||
|
|||||||
185
api/libs/services/plausible/index.js
Normal file
185
api/libs/services/plausible/index.js
Normal 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 }
|
||||||
@@ -2,10 +2,11 @@ const mongoose = require('mongoose')
|
|||||||
const { version } = require('../../../package.json')
|
const { version } = require('../../../package.json')
|
||||||
const logSchema = mongoose.Schema(
|
const logSchema = mongoose.Schema(
|
||||||
{
|
{
|
||||||
version: { type: String, required: true, default: version },
|
version: { type: String, default: version },
|
||||||
type: { type: String, required: true, enum: ['API', 'UPGRADE-P-1', 'UPGRADE-P-2'], default: 'API' },
|
type: { type: String, required: true },
|
||||||
event: { type: String, required: true },
|
message: { type: String, required: true },
|
||||||
seen: { type: Boolean, required: true, default: false }
|
stack: { type: String },
|
||||||
|
seen: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
{ timestamps: { createdAt: 'createdAt', updatedAt: false } }
|
{ timestamps: { createdAt: 'createdAt', updatedAt: false } }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ const mongoose = require('mongoose')
|
|||||||
const settingsSchema = mongoose.Schema(
|
const settingsSchema = mongoose.Schema(
|
||||||
{
|
{
|
||||||
applicationName: { type: String, required: true, default: 'coolify' },
|
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 }
|
{ timestamps: true }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { streamEvents, docker } = require('../libs/docker')
|
|
||||||
|
|
||||||
async function buildImage (configuration) {
|
|
||||||
let dockerFile = `
|
|
||||||
# build
|
|
||||||
FROM node:lts
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
COPY package*.json .
|
|
||||||
`
|
|
||||||
if (configuration.build.command.installation) {
|
|
||||||
dockerFile += `RUN ${configuration.build.command.installation}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
dockerFile += `COPY . .
|
|
||||||
RUN ${configuration.build.command.build}`
|
|
||||||
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, dockerFile)
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
buildImage
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
const static = require('./static')
|
|
||||||
const nodejs = require('./nodejs')
|
|
||||||
|
|
||||||
module.exports = { static, nodejs }
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
if (configuration.build.command.build) await buildImage(configuration)
|
|
||||||
|
|
||||||
let dockerFile = `# production stage
|
|
||||||
FROM node:lts
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
`
|
|
||||||
if (configuration.build.command.build) {
|
|
||||||
dockerFile += `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.build.directory} /usr/src/app`
|
|
||||||
} else {
|
|
||||||
dockerFile += 'COPY . ./'
|
|
||||||
}
|
|
||||||
if (configuration.build.command.installation) {
|
|
||||||
dockerFile += `
|
|
||||||
RUN ${configuration.build.command.installation}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
dockerFile += `
|
|
||||||
EXPOSE ${configuration.publish.port}
|
|
||||||
CMD [ "yarn", "start" ]`
|
|
||||||
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, dockerFile)
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
if (configuration.build.command.build) await buildImage(configuration)
|
|
||||||
|
|
||||||
let dockerFile = `# production stage
|
|
||||||
FROM nginx:stable-alpine
|
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
|
||||||
`
|
|
||||||
if (configuration.build.command.build) {
|
|
||||||
dockerFile += `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.build.directory} /usr/share/nginx/html`
|
|
||||||
} else {
|
|
||||||
dockerFile += 'COPY . /usr/share/nginx/html'
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerFile += `
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]`
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, dockerFile)
|
|
||||||
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,37 @@
|
|||||||
|
|
||||||
const { verifyUserId } = require('../../../libs/common')
|
|
||||||
const { setDefaultConfiguration } = require('../../../libs/applications/configuration')
|
const { setDefaultConfiguration } = require('../../../libs/applications/configuration')
|
||||||
const { docker } = require('../../../libs/docker')
|
const { docker } = require('../../../libs/docker')
|
||||||
|
const { saveServerLog } = require('../../../libs/logging')
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
fastify.post('/', async (request, reply) => {
|
fastify.post('/', async (request, reply) => {
|
||||||
if (!await verifyUserId(request.headers.authorization)) {
|
try {
|
||||||
reply.code(500).send({ error: 'Invalid request' })
|
const configuration = setDefaultConfiguration(request.body)
|
||||||
return
|
|
||||||
}
|
|
||||||
const configuration = setDefaultConfiguration(request.body)
|
|
||||||
|
|
||||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||||
let foundDomain = false
|
let foundDomain = false
|
||||||
|
|
||||||
for (const service of services) {
|
for (const service of services) {
|
||||||
const running = JSON.parse(service.Spec.Labels.configuration)
|
const running = JSON.parse(service.Spec.Labels.configuration)
|
||||||
if (running) {
|
if (running) {
|
||||||
if (
|
if (
|
||||||
running.publish.domain === configuration.publish.domain &&
|
running.publish.domain === configuration.publish.domain &&
|
||||||
running.repository.id !== configuration.repository.id
|
running.repository.id !== configuration.repository.id &&
|
||||||
) {
|
running.publish.path === configuration.publish.path
|
||||||
foundDomain = true
|
) {
|
||||||
|
foundDomain = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (fastify.config.DOMAIN === configuration.publish.domain) foundDomain = true
|
||||||
|
if (foundDomain) {
|
||||||
|
reply.code(500).send({ message: 'Domain already in use.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return { message: 'OK' }
|
||||||
|
} catch (error) {
|
||||||
|
await saveServerLog(error)
|
||||||
|
throw new Error(error)
|
||||||
}
|
}
|
||||||
if (fastify.config.DOMAIN === configuration.publish.domain) foundDomain = true
|
|
||||||
if (foundDomain) {
|
|
||||||
reply.code(500).send({ message: 'Domain already in use.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return { message: 'OK' }
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,69 @@
|
|||||||
|
|
||||||
const { verifyUserId, cleanupTmp, execShellAsync } = require('../../../../libs/common')
|
|
||||||
const Deployment = require('../../../../models/Deployment')
|
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 { queueAndBuild } = require('../../../../libs/applications')
|
||||||
const { setDefaultConfiguration } = require('../../../../libs/applications/configuration')
|
const { setDefaultConfiguration, precheckDeployment } = require('../../../../libs/applications/configuration')
|
||||||
const { docker } = require('../../../../libs/docker')
|
const { docker } = require('../../../../libs/docker')
|
||||||
|
const { saveServerLog } = require('../../../../libs/logging')
|
||||||
const cloneRepository = require('../../../../libs/applications/github/cloneRepository')
|
const cloneRepository = require('../../../../libs/applications/github/cloneRepository')
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
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) => {
|
fastify.post('/', async (request, reply) => {
|
||||||
if (!await verifyUserId(request.headers.authorization)) {
|
let configuration
|
||||||
|
try {
|
||||||
|
await verifyUserId(request.headers.authorization)
|
||||||
|
} catch (error) {
|
||||||
reply.code(500).send({ error: 'Invalid request' })
|
reply.code(500).send({ error: 'Invalid request' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const configuration = setDefaultConfiguration(request.body)
|
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||||
|
configuration = setDefaultConfiguration(request.body)
|
||||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
if (!configuration) {
|
||||||
|
throw new Error('Whaat?')
|
||||||
await cloneRepository(configuration)
|
|
||||||
|
|
||||||
let foundService = false
|
|
||||||
let foundDomain = false
|
|
||||||
let configChanged = false
|
|
||||||
let imageChanged = false
|
|
||||||
|
|
||||||
let forceUpdate = false
|
|
||||||
|
|
||||||
for (const service of services) {
|
|
||||||
const running = JSON.parse(service.Spec.Labels.configuration)
|
|
||||||
if (running) {
|
|
||||||
if (
|
|
||||||
running.publish.domain === configuration.publish.domain &&
|
|
||||||
running.repository.id !== configuration.repository.id
|
|
||||||
) {
|
|
||||||
foundDomain = true
|
|
||||||
}
|
|
||||||
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
|
|
||||||
const state = await execShellAsync(`docker stack ps ${running.build.container.name} --format '{{ json . }}'`)
|
|
||||||
const isError = state.split('\n').filter(n => n).map(s => JSON.parse(s)).filter(n => n.DesiredState !== 'Running')
|
|
||||||
if (isError.length > 0) forceUpdate = true
|
|
||||||
|
|
||||||
foundService = true
|
|
||||||
const runningWithoutContainer = JSON.parse(JSON.stringify(running))
|
|
||||||
delete runningWithoutContainer.build.container
|
|
||||||
|
|
||||||
const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration))
|
|
||||||
delete configurationWithoutContainer.build.container
|
|
||||||
|
|
||||||
// If only the configuration changed
|
|
||||||
if (JSON.stringify(runningWithoutContainer.build) !== JSON.stringify(configurationWithoutContainer.build) || JSON.stringify(runningWithoutContainer.publish) !== JSON.stringify(configurationWithoutContainer.publish)) configChanged = true
|
|
||||||
// If only the image changed
|
|
||||||
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true
|
|
||||||
// If build pack changed, forceUpdate the service
|
|
||||||
if (running.build.pack !== configuration.build.pack) forceUpdate = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
await cloneRepository(configuration)
|
||||||
if (foundDomain) {
|
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
|
||||||
cleanupTmp(configuration.general.workdir)
|
|
||||||
reply.code(500).send({ message: 'Domain already in use.' })
|
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
|
||||||
return
|
|
||||||
}
|
|
||||||
if (forceUpdate) {
|
|
||||||
imageChanged = false
|
|
||||||
configChanged = false
|
|
||||||
} else {
|
|
||||||
if (foundService && !imageChanged && !configChanged) {
|
|
||||||
cleanupTmp(configuration.general.workdir)
|
cleanupTmp(configuration.general.workdir)
|
||||||
reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
|
reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const alreadyQueued = await Deployment.find({
|
||||||
|
repoId: configuration.repository.id,
|
||||||
|
branch: configuration.repository.branch,
|
||||||
|
organization: configuration.repository.organization,
|
||||||
|
name: configuration.repository.name,
|
||||||
|
domain: configuration.publish.domain,
|
||||||
|
progress: { $in: ['queued', 'inprogress'] }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (alreadyQueued.length > 0) {
|
||||||
|
reply.code(200).send({ message: 'Already in the queue.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name, deployId: configuration.general.deployId })
|
||||||
|
await queueAndBuild(configuration, imageChanged)
|
||||||
|
} catch (error) {
|
||||||
|
const { id, organization, name, branch } = configuration.repository
|
||||||
|
const { domain } = configuration.publish
|
||||||
|
const { deployId } = configuration.general
|
||||||
|
await Deployment.findOneAndUpdate(
|
||||||
|
{ repoId: id, branch, deployId, organization, name, domain },
|
||||||
|
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
||||||
|
if (error.name) {
|
||||||
|
if (error.message && error.stack) await saveServerLog(error)
|
||||||
|
if (reply.sent) await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
|
||||||
|
}
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
cleanupTmp(configuration.general.workdir)
|
||||||
|
await purgeImagesContainers(configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
const alreadyQueued = await Deployment.find({
|
|
||||||
repoId: configuration.repository.id,
|
|
||||||
branch: configuration.repository.branch,
|
|
||||||
organization: configuration.repository.organization,
|
|
||||||
name: configuration.repository.name,
|
|
||||||
domain: configuration.publish.domain,
|
|
||||||
progress: { $in: ['queued', 'inprogress'] }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (alreadyQueued.length > 0) {
|
|
||||||
reply.code(200).send({ message: 'Already in the queue.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
queueAndBuild(configuration, services, configChanged, imageChanged)
|
|
||||||
|
|
||||||
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,25 +18,29 @@ module.exports = async function (fastify) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fastify.get('/', { schema: getLogSchema }, async (request, reply) => {
|
fastify.get('/', { schema: getLogSchema }, async (request, reply) => {
|
||||||
const { repoId, branch, page } = request.query
|
try {
|
||||||
const onePage = 5
|
const { repoId, branch, page } = request.query
|
||||||
const show = Number(page) * onePage || 5
|
const onePage = 5
|
||||||
const deploy = await Deployment.find({ repoId, branch })
|
const show = Number(page) * onePage || 5
|
||||||
.select('-_id -__v -repoId')
|
const deploy = await Deployment.find({ repoId, branch })
|
||||||
.sort({ createdAt: 'desc' })
|
.select('-_id -__v -repoId')
|
||||||
.limit(show)
|
.sort({ createdAt: 'desc' })
|
||||||
|
.limit(show)
|
||||||
|
|
||||||
const finalLogs = deploy.map(d => {
|
const finalLogs = deploy.map(d => {
|
||||||
const finalLogs = { ...d._doc }
|
const finalLogs = { ...d._doc }
|
||||||
|
|
||||||
const updatedAt = dayjs(d.updatedAt).utc()
|
const updatedAt = dayjs(d.updatedAt).utc()
|
||||||
|
|
||||||
finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000
|
finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000
|
||||||
finalLogs.since = updatedAt.fromNow()
|
finalLogs.since = updatedAt.fromNow()
|
||||||
|
|
||||||
|
return finalLogs
|
||||||
|
})
|
||||||
return finalLogs
|
return finalLogs
|
||||||
})
|
} catch (error) {
|
||||||
return finalLogs
|
throw new Error(error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.get('/:deployId', async (request, reply) => {
|
fastify.get('/:deployId', async (request, reply) => {
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
const { docker } = require('../../../libs/docker')
|
const { docker } = require('../../../libs/docker')
|
||||||
|
const { saveServerLog } = require('../../../libs/logging')
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
fastify.get('/', async (request, reply) => {
|
fastify.get('/', async (request, reply) => {
|
||||||
const { name } = request.query
|
try {
|
||||||
const service = await docker.engine.getService(`${name}_${name}`)
|
const { name } = request.query
|
||||||
const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a)
|
const service = await docker.engine.getService(`${name}_${name}`)
|
||||||
return { logs }
|
const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a)
|
||||||
|
return { logs }
|
||||||
|
} catch (error) {
|
||||||
|
await saveServerLog(error)
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
const { docker } = require('../../../libs/docker')
|
const { docker } = require('../../../libs/docker')
|
||||||
const { execShellAsync } = require('../../../libs/common')
|
const { execShellAsync, delay } = require('../../../libs/common')
|
||||||
const ApplicationLog = require('../../../models/Logs/Application')
|
const ApplicationLog = require('../../../models/Logs/Application')
|
||||||
const Deployment = require('../../../models/Deployment')
|
const Deployment = require('../../../models/Deployment')
|
||||||
|
const { purgeImagesContainers } = require('../../../libs/applications/cleanup')
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
fastify.post('/', async (request, reply) => {
|
fastify.post('/', async (request, reply) => {
|
||||||
@@ -25,6 +26,8 @@ module.exports = async function (fastify) {
|
|||||||
}
|
}
|
||||||
await execShellAsync(`docker stack rm ${found.build.container.name}`)
|
await execShellAsync(`docker stack rm ${found.build.container.name}`)
|
||||||
reply.code(200).send({ organization, name, branch })
|
reply.code(200).send({ organization, name, branch })
|
||||||
|
await delay(10000)
|
||||||
|
await purgeImagesContainers(found, true)
|
||||||
} else {
|
} else {
|
||||||
reply.code(500).send({ message: 'Nothing to do.' })
|
reply.code(500).send({ message: 'Nothing to do.' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,6 @@
|
|||||||
const { docker } = require('../../libs/docker')
|
const { docker } = require('../../libs/docker')
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
// const getConfig = {
|
|
||||||
// querystring: {
|
|
||||||
// type: 'object',
|
|
||||||
// properties: {
|
|
||||||
// repoId: { type: 'number' },
|
|
||||||
// branch: { type: 'string' }
|
|
||||||
// },
|
|
||||||
// required: ['repoId', 'branch']
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const saveConfig = {
|
|
||||||
// body: {
|
|
||||||
// type: 'object',
|
|
||||||
// properties: {
|
|
||||||
// build: {
|
|
||||||
// type: 'object',
|
|
||||||
// properties: {
|
|
||||||
// baseDir: { type: 'string' },
|
|
||||||
// installCmd: { type: 'string' },
|
|
||||||
// buildCmd: { type: 'string' }
|
|
||||||
// },
|
|
||||||
// required: ['baseDir', 'installCmd', 'buildCmd']
|
|
||||||
// },
|
|
||||||
// publish: {
|
|
||||||
// type: 'object',
|
|
||||||
// properties: {
|
|
||||||
// publishDir: { type: 'string' },
|
|
||||||
// domain: { type: 'string' },
|
|
||||||
// pathPrefix: { type: 'string' },
|
|
||||||
// port: { type: 'number' }
|
|
||||||
// },
|
|
||||||
// required: ['publishDir', 'domain', 'pathPrefix', 'port']
|
|
||||||
// },
|
|
||||||
// previewDeploy: { type: 'boolean' },
|
|
||||||
// branch: { type: 'string' },
|
|
||||||
// repoId: { type: 'number' },
|
|
||||||
// buildPack: { type: 'string' },
|
|
||||||
// fullName: { type: 'string' },
|
|
||||||
// installationId: { type: 'number' }
|
|
||||||
// },
|
|
||||||
// required: ['build', 'publish', 'previewDeploy', 'branch', 'repoId', 'buildPack', 'fullName', 'installationId']
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fastify.get("/all", async (request, reply) => {
|
|
||||||
// return await Config.find().select("-_id -__v");
|
|
||||||
// });
|
|
||||||
|
|
||||||
// fastify.get("/", { schema: getConfig }, async (request, reply) => {
|
|
||||||
// const { repoId, branch } = request.query;
|
|
||||||
// return await Config.findOne({ repoId, branch }).select("-_id -__v");
|
|
||||||
// });
|
|
||||||
|
|
||||||
fastify.post('/', async (request, reply) => {
|
fastify.post('/', async (request, reply) => {
|
||||||
const { name, organization, branch } = request.body
|
const { name, organization, branch } = request.body
|
||||||
const services = await docker.engine.listServices()
|
const services = await docker.engine.listServices()
|
||||||
@@ -79,25 +25,4 @@ module.exports = async function (fastify) {
|
|||||||
reply.code(500).send({ message: 'No configuration found.' })
|
reply.code(500).send({ message: 'No configuration found.' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// fastify.delete("/", async (request, reply) => {
|
|
||||||
// const { repoId, branch } = request.body;
|
|
||||||
|
|
||||||
// const deploys = await Deployment.find({ repoId, branch })
|
|
||||||
// const found = deploys.filter(d => d.progress !== 'done' && d.progress !== 'failed')
|
|
||||||
// if (found.length > 0) {
|
|
||||||
// throw new Error('Deployment inprogress, cannot delete now.');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const config = await Config.findOneAndDelete({ repoId, branch })
|
|
||||||
// for (const deploy of deploys) {
|
|
||||||
// await ApplicationLog.findOneAndRemove({ deployId: deploy.deployId });
|
|
||||||
// }
|
|
||||||
// const secrets = await Secret.find({ repoId, branch });
|
|
||||||
// for (const secret of secrets) {
|
|
||||||
// await Secret.findByIdAndRemove(secret._id);
|
|
||||||
// }
|
|
||||||
// await execShellAsync(`docker stack rm ${config.containerName}`);
|
|
||||||
// return { message: 'Deleted application and related configurations.' };
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,60 @@
|
|||||||
const { docker } = require('../../../libs/docker')
|
const { docker } = require('../../../libs/docker')
|
||||||
const Deployment = require('../../../models/Deployment')
|
|
||||||
const ServerLog = require('../../../models/Logs/Server')
|
const ServerLog = require('../../../models/Logs/Server')
|
||||||
|
const { saveServerLog } = require('../../../libs/logging')
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
fastify.get('/', async (request, reply) => {
|
fastify.get('/', async (request, reply) => {
|
||||||
const latestDeployments = await Deployment.aggregate([
|
try {
|
||||||
{
|
const serverLogs = await ServerLog.find()
|
||||||
$sort: { createdAt: -1 }
|
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)
|
||||||
$group:
|
let services = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'service' && r.Spec.Labels.configuration)
|
||||||
{
|
applications = applications.map(r => {
|
||||||
_id: {
|
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||||
repoId: '$repoId',
|
return {
|
||||||
branch: '$branch'
|
configuration: JSON.parse(r.Spec.Labels.configuration),
|
||||||
},
|
UpdatedAt: r.UpdatedAt
|
||||||
createdAt: { $last: '$createdAt' },
|
}
|
||||||
progress: { $first: '$progress' }
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
databases = databases.map(r => {
|
||||||
|
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||||
|
return {
|
||||||
|
configuration: JSON.parse(r.Spec.Labels.configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
services = services.map(r => {
|
||||||
|
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||||
|
return {
|
||||||
|
serviceName: r.Spec.Labels.serviceName,
|
||||||
|
configuration: JSON.parse(r.Spec.Labels.configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
applications = [...new Map(applications.map(item => [item.configuration.publish.domain + item.configuration.publish.path, item])).values()]
|
||||||
|
return {
|
||||||
|
serverLogs,
|
||||||
|
applications: {
|
||||||
|
deployed: applications
|
||||||
|
},
|
||||||
|
databases: {
|
||||||
|
deployed: databases
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
deployed: services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT' && error.errno === -2) {
|
||||||
const serverLogs = await ServerLog.find()
|
throw new Error(`Docker service unavailable at ${error.address}.`)
|
||||||
const services = await docker.engine.listServices()
|
} else {
|
||||||
|
await saveServerLog(error)
|
||||||
let applications = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' && r.Spec.Labels.configuration)
|
throw new Error(error)
|
||||||
let databases = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && 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 {}
|
|
||||||
})
|
|
||||||
databases = databases.map(r => {
|
|
||||||
const configuration = r.Spec.Labels.configuration ? JSON.parse(r.Spec.Labels.configuration) : null
|
|
||||||
r.Spec.Labels.configuration = configuration
|
|
||||||
return r
|
|
||||||
})
|
|
||||||
applications = [...new Map(applications.map(item => [item.Spec.Labels.configuration.publish.domain, item])).values()]
|
|
||||||
return {
|
|
||||||
serverLogs,
|
|
||||||
applications: {
|
|
||||||
deployed: applications
|
|
||||||
},
|
|
||||||
databases: {
|
|
||||||
deployed: databases
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const fs = require('fs').promises
|
|||||||
const cuid = require('cuid')
|
const cuid = require('cuid')
|
||||||
const { docker } = require('../../../libs/docker')
|
const { docker } = require('../../../libs/docker')
|
||||||
const { execShellAsync } = require('../../../libs/common')
|
const { execShellAsync } = require('../../../libs/common')
|
||||||
|
const { saveServerLog } = require('../../../libs/logging')
|
||||||
|
|
||||||
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
|
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
|
||||||
const generator = require('generate-password')
|
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)
|
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) {
|
if (database) {
|
||||||
const jsonEnvs = {}
|
const jsonEnvs = {}
|
||||||
for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
|
if (database.Spec.TaskTemplate.ContainerSpec.Env) {
|
||||||
const s = d.split('=')
|
for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
|
||||||
jsonEnvs[s[0]] = s[1]
|
const s = d.split('=')
|
||||||
|
jsonEnvs[s[0]] = s[1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
config: JSON.parse(database.Spec.Labels.configuration),
|
config: JSON.parse(database.Spec.Labels.configuration),
|
||||||
envs: jsonEnvs
|
envs: jsonEnvs || null
|
||||||
}
|
}
|
||||||
reply.code(200).send(payload)
|
reply.code(200).send(payload)
|
||||||
} else {
|
} else {
|
||||||
@@ -38,131 +41,147 @@ module.exports = async function (fastify) {
|
|||||||
body: {
|
body: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb'] }
|
type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb', 'clickhouse'] }
|
||||||
},
|
},
|
||||||
required: ['type']
|
required: ['type']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fastify.post('/deploy', { schema: postSchema }, async (request, reply) => {
|
fastify.post('/deploy', { schema: postSchema }, async (request, reply) => {
|
||||||
let { type, defaultDatabaseName } = request.body
|
try {
|
||||||
const passwords = generator.generateMultiple(2, {
|
let { type, defaultDatabaseName } = request.body
|
||||||
length: 24,
|
const passwords = generator.generateMultiple(2, {
|
||||||
numbers: true,
|
length: 24,
|
||||||
strict: true
|
numbers: true,
|
||||||
})
|
strict: true
|
||||||
const usernames = generator.generateMultiple(2, {
|
})
|
||||||
length: 10,
|
const usernames = generator.generateMultiple(2, {
|
||||||
numbers: true,
|
length: 10,
|
||||||
strict: true
|
numbers: true,
|
||||||
})
|
strict: true
|
||||||
// TODO: Query for existing db with the same name
|
})
|
||||||
const nickname = getUniq()
|
// TODO: Query for existing db with the same name
|
||||||
|
const nickname = getUniq()
|
||||||
|
|
||||||
if (!defaultDatabaseName) defaultDatabaseName = nickname
|
if (!defaultDatabaseName) defaultDatabaseName = nickname
|
||||||
|
|
||||||
reply.code(201).send({ message: 'Deploying.' })
|
reply.code(201).send({ message: 'Deploying.' })
|
||||||
// TODO: Persistent volume, custom inputs
|
const deployId = cuid()
|
||||||
const deployId = cuid()
|
const configuration = {
|
||||||
const configuration = {
|
general: {
|
||||||
general: {
|
workdir: `/tmp/${deployId}`,
|
||||||
workdir: `/tmp/${deployId}`,
|
deployId,
|
||||||
deployId,
|
nickname,
|
||||||
nickname,
|
type
|
||||||
type
|
},
|
||||||
},
|
database: {
|
||||||
database: {
|
usernames,
|
||||||
usernames,
|
passwords,
|
||||||
passwords,
|
defaultDatabaseName
|
||||||
defaultDatabaseName
|
},
|
||||||
},
|
deploy: {
|
||||||
deploy: {
|
name: nickname
|
||||||
name: nickname
|
}
|
||||||
}
|
}
|
||||||
}
|
await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
|
||||||
let generateEnvs = {}
|
let generateEnvs = {}
|
||||||
let image = null
|
let image = null
|
||||||
let volume = null
|
let volume = null
|
||||||
if (type === 'mongodb') {
|
let ulimits = {}
|
||||||
generateEnvs = {
|
if (type === 'mongodb') {
|
||||||
MONGODB_ROOT_PASSWORD: passwords[0],
|
generateEnvs = {
|
||||||
MONGODB_USERNAME: usernames[0],
|
MONGODB_ROOT_PASSWORD: passwords[0],
|
||||||
MONGODB_PASSWORD: passwords[1],
|
MONGODB_USERNAME: usernames[0],
|
||||||
MONGODB_DATABASE: defaultDatabaseName
|
MONGODB_PASSWORD: passwords[1],
|
||||||
}
|
MONGODB_DATABASE: defaultDatabaseName
|
||||||
image = 'bitnami/mongodb:4.4'
|
}
|
||||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb`
|
image = 'bitnami/mongodb:4.4'
|
||||||
} else if (type === 'postgresql') {
|
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb`
|
||||||
generateEnvs = {
|
} else if (type === 'postgresql') {
|
||||||
POSTGRESQL_PASSWORD: passwords[0],
|
generateEnvs = {
|
||||||
POSTGRESQL_USERNAME: usernames[0],
|
POSTGRESQL_PASSWORD: passwords[0],
|
||||||
POSTGRESQL_DATABASE: defaultDatabaseName
|
POSTGRESQL_USERNAME: usernames[0],
|
||||||
}
|
POSTGRESQL_DATABASE: defaultDatabaseName
|
||||||
image = 'bitnami/postgresql:13.2.0'
|
}
|
||||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql`
|
image = 'bitnami/postgresql:13.2.0'
|
||||||
} else if (type === 'couchdb') {
|
volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql`
|
||||||
generateEnvs = {
|
} else if (type === 'couchdb') {
|
||||||
COUCHDB_PASSWORD: passwords[0],
|
generateEnvs = {
|
||||||
COUCHDB_USER: usernames[0]
|
COUCHDB_PASSWORD: passwords[0],
|
||||||
}
|
COUCHDB_USER: usernames[0]
|
||||||
image = 'bitnami/couchdb:3'
|
}
|
||||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb`
|
image = 'bitnami/couchdb:3'
|
||||||
} else if (type === 'mysql') {
|
volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb`
|
||||||
generateEnvs = {
|
} else if (type === 'mysql') {
|
||||||
MYSQL_ROOT_PASSWORD: passwords[0],
|
generateEnvs = {
|
||||||
MYSQL_ROOT_USER: usernames[0],
|
MYSQL_ROOT_PASSWORD: passwords[0],
|
||||||
MYSQL_USER: usernames[1],
|
MYSQL_ROOT_USER: usernames[0],
|
||||||
MYSQL_PASSWORD: passwords[1],
|
MYSQL_USER: usernames[1],
|
||||||
MYSQL_DATABASE: defaultDatabaseName
|
MYSQL_PASSWORD: passwords[1],
|
||||||
}
|
MYSQL_DATABASE: defaultDatabaseName
|
||||||
image = 'bitnami/mysql:8.0'
|
}
|
||||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`
|
image = 'bitnami/mysql:8.0'
|
||||||
}
|
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`
|
||||||
|
} else if (type === 'clickhouse') {
|
||||||
const stack = {
|
image = 'yandex/clickhouse-server'
|
||||||
version: '3.8',
|
volume = `${configuration.general.deployId}-${type}-data:/var/lib/clickhouse`
|
||||||
services: {
|
ulimits = {
|
||||||
[configuration.general.deployId]: {
|
nofile: {
|
||||||
image,
|
soft: 262144,
|
||||||
networks: [`${docker.network}`],
|
hard: 262144
|
||||||
environment: generateEnvs,
|
|
||||||
volumes: [volume],
|
|
||||||
deploy: {
|
|
||||||
replicas: 1,
|
|
||||||
update_config: {
|
|
||||||
parallelism: 0,
|
|
||||||
delay: '10s',
|
|
||||||
order: 'start-first'
|
|
||||||
},
|
|
||||||
rollback_config: {
|
|
||||||
parallelism: 0,
|
|
||||||
delay: '10s',
|
|
||||||
order: 'start-first'
|
|
||||||
},
|
|
||||||
labels: [
|
|
||||||
'managedBy=coolify',
|
|
||||||
'type=database',
|
|
||||||
'configuration=' + JSON.stringify(configuration)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
networks: {
|
|
||||||
[`${docker.network}`]: {
|
const stack = {
|
||||||
external: true
|
version: '3.8',
|
||||||
}
|
services: {
|
||||||
},
|
[configuration.general.deployId]: {
|
||||||
volumes: {
|
image,
|
||||||
[`${configuration.general.deployId}-${type}-data`]: {
|
networks: [`${docker.network}`],
|
||||||
external: true
|
environment: generateEnvs,
|
||||||
|
volumes: [volume],
|
||||||
|
ulimits,
|
||||||
|
deploy: {
|
||||||
|
replicas: 1,
|
||||||
|
update_config: {
|
||||||
|
parallelism: 0,
|
||||||
|
delay: '10s',
|
||||||
|
order: 'start-first'
|
||||||
|
},
|
||||||
|
rollback_config: {
|
||||||
|
parallelism: 0,
|
||||||
|
delay: '10s',
|
||||||
|
order: 'start-first'
|
||||||
|
},
|
||||||
|
labels: [
|
||||||
|
'managedBy=coolify',
|
||||||
|
'type=database',
|
||||||
|
'configuration=' + JSON.stringify(configuration)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
[`${docker.network}`]: {
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[`${configuration.general.deployId}-${type}-data`]: {
|
||||||
|
external: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
|
||||||
|
await execShellAsync(
|
||||||
|
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
await saveServerLog(error)
|
||||||
|
throw new Error(error)
|
||||||
}
|
}
|
||||||
await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
|
|
||||||
await execShellAsync(
|
|
||||||
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.delete('/:dbName', async (request, reply) => {
|
fastify.delete('/:dbName', async (request, reply) => {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const Settings = require('../../../models/Settings')
|
|||||||
const cuid = require('cuid')
|
const cuid = require('cuid')
|
||||||
const mongoose = require('mongoose')
|
const mongoose = require('mongoose')
|
||||||
const jwt = require('jsonwebtoken')
|
const jwt = require('jsonwebtoken')
|
||||||
|
const { saveServerLog } = require('../../../libs/logging')
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
const githubCodeSchema = {
|
const githubCodeSchema = {
|
||||||
schema: {
|
schema: {
|
||||||
@@ -59,8 +61,12 @@ module.exports = async function (fastify) {
|
|||||||
avatar: avatar_url,
|
avatar: avatar_url,
|
||||||
uid
|
uid
|
||||||
})
|
})
|
||||||
|
const defaultSettings = new Settings({
|
||||||
|
_id: new mongoose.Types.ObjectId()
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
await newUser.save()
|
await newUser.save()
|
||||||
|
await defaultSettings.save()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
reply.code(500).send({ success: false, error: e })
|
reply.code(500).send({ success: false, error: e })
|
||||||
@@ -111,8 +117,8 @@ module.exports = async function (fastify) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
await saveServerLog(error)
|
||||||
reply.code(500).send({ success: false, error: error.message })
|
throw new Error(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
fastify.get('/success', async (request, reply) => {
|
fastify.get('/success', async (request, reply) => {
|
||||||
|
|||||||
14
api/routes/v1/server/index.js
Normal file
14
api/routes/v1/server/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const Server = require('../../../models/Logs/Server')
|
||||||
|
module.exports = async function (fastify) {
|
||||||
|
fastify.get('/', async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const serverLogs = await Server.find().select('-_id -__v')
|
||||||
|
// TODO: Should do better
|
||||||
|
return {
|
||||||
|
serverLogs
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
15
api/routes/v1/services/deploy.js
Normal file
15
api/routes/v1/services/deploy.js
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
27
api/routes/v1/services/index.js
Normal file
27
api/routes/v1/services/index.js
Normal 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({})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
const Settings = require('../../../models/Settings')
|
const Settings = require('../../../models/Settings')
|
||||||
|
const { saveServerLog } = require('../../../libs/logging')
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
const applicationName = 'coolify'
|
const applicationName = 'coolify'
|
||||||
const postSchema = {
|
const postSchema = {
|
||||||
body: {
|
body: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
allowRegistration: { type: 'boolean' }
|
allowRegistration: { type: 'boolean' },
|
||||||
|
sendErrors: { type: 'boolean' }
|
||||||
},
|
},
|
||||||
required: ['allowRegistration']
|
required: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +28,7 @@ module.exports = async function (fastify) {
|
|||||||
settings
|
settings
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await saveServerLog(error)
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -38,6 +42,7 @@ module.exports = async function (fastify) {
|
|||||||
).select('-_id -__v')
|
).select('-_id -__v')
|
||||||
reply.code(201).send({ settings })
|
reply.code(201).send({ settings })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await saveServerLog(error)
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ const { saveServerLog } = require('../../../libs/logging')
|
|||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
fastify.get('/', async (request, reply) => {
|
fastify.get('/', async (request, reply) => {
|
||||||
const upgradeP1 = await execShellAsync('bash ./install.sh upgrade-phase-1')
|
const upgradeP1 = await execShellAsync('bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p1.sh)"')
|
||||||
await saveServerLog({ event: upgradeP1, type: 'UPGRADE-P-1' })
|
await saveServerLog({ message: upgradeP1, type: 'UPGRADE-P-1' })
|
||||||
reply.code(200).send('I\'m trying, okay?')
|
reply.code(200).send('I\'m trying, okay?')
|
||||||
const upgradeP2 = await execShellAsync('bash ./install.sh upgrade-phase-2')
|
const upgradeP2 = await execShellAsync('docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -u root coolify bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p2.sh)"')
|
||||||
await saveServerLog({ event: upgradeP2, type: 'UPGRADE-P-2' })
|
await saveServerLog({ message: upgradeP2, type: 'UPGRADE-P-2' })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ const jwt = require('jsonwebtoken')
|
|||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
fastify.get('/', async (request, reply) => {
|
fastify.get('/', async (request, reply) => {
|
||||||
const { authorization } = request.headers
|
try {
|
||||||
if (!authorization) {
|
const { authorization } = request.headers
|
||||||
|
if (!authorization) {
|
||||||
|
reply.code(401).send({})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const token = authorization.split(' ')[1]
|
||||||
|
const verify = jwt.verify(token, fastify.config.JWT_SIGN_KEY)
|
||||||
|
const found = await User.findOne({ uid: verify.jti })
|
||||||
|
found ? reply.code(200).send({}) : reply.code(401).send({})
|
||||||
|
} catch (error) {
|
||||||
reply.code(401).send({})
|
reply.code(401).send({})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const token = authorization.split(' ')[1]
|
|
||||||
const verify = jwt.verify(token, fastify.config.JWT_SIGN_KEY)
|
|
||||||
const found = await User.findOne({ uid: verify.jti })
|
|
||||||
found ? reply.code(200).send({}) : reply.code(401).send({})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const { cleanupTmp, execShellAsync } = require('../../../libs/common')
|
const { cleanupTmp } = require('../../../libs/common')
|
||||||
|
|
||||||
const Deployment = require('../../../models/Deployment')
|
const Deployment = require('../../../models/Deployment')
|
||||||
|
const ApplicationLog = require('../../../models/Logs/Application')
|
||||||
|
const ServerLog = require('../../../models/Logs/Server')
|
||||||
|
|
||||||
const { queueAndBuild } = require('../../../libs/applications')
|
const { queueAndBuild } = require('../../../libs/applications')
|
||||||
const { setDefaultConfiguration } = require('../../../libs/applications/configuration')
|
const { setDefaultConfiguration, precheckDeployment } = require('../../../libs/applications/configuration')
|
||||||
const { docker } = require('../../../libs/docker')
|
const { docker } = require('../../../libs/docker')
|
||||||
const cloneRepository = require('../../../libs/applications/github/cloneRepository')
|
const cloneRepository = require('../../../libs/applications/github/cloneRepository')
|
||||||
|
const { purgeImagesContainers } = require('../../../libs/applications/cleanup')
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
module.exports = async function (fastify) {
|
||||||
// TODO: Add this to fastify plugin
|
|
||||||
const postSchema = {
|
const postSchema = {
|
||||||
body: {
|
body: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -33,6 +37,7 @@ module.exports = async function (fastify) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fastify.post('/', { schema: postSchema }, async (request, reply) => {
|
fastify.post('/', { schema: postSchema }, async (request, reply) => {
|
||||||
|
let configuration
|
||||||
const hmac = crypto.createHmac('sha256', fastify.config.GITHUP_APP_WEBHOOK_SECRET)
|
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 digest = Buffer.from('sha256=' + hmac.update(JSON.stringify(request.body)).digest('hex'), 'utf8')
|
||||||
const checksum = Buffer.from(request.headers['x-hub-signature-256'], 'utf8')
|
const checksum = Buffer.from(request.headers['x-hub-signature-256'], 'utf8')
|
||||||
@@ -45,98 +50,72 @@ module.exports = async function (fastify) {
|
|||||||
reply.code(500).send({ error: 'Not a push event.' })
|
reply.code(500).send({ error: 'Not a push event.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||||
|
|
||||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
configuration = services.find(r => {
|
||||||
|
if (request.body.ref.startsWith('refs')) {
|
||||||
let configuration = services.find(r => {
|
const branch = request.body.ref.split('/')[2]
|
||||||
if (request.body.ref.startsWith('refs')) {
|
if (
|
||||||
const branch = request.body.ref.split('/')[2]
|
JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
|
||||||
if (
|
JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
|
||||||
JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
|
) {
|
||||||
JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
|
return r
|
||||||
) {
|
}
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (!configuration) {
|
||||||
|
reply.code(500).send({ error: 'No configuration found.' })
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration))
|
||||||
})
|
await cloneRepository(configuration)
|
||||||
|
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
|
||||||
|
|
||||||
if (!configuration) {
|
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
|
||||||
reply.code(500).send({ error: 'No configuration found.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration))
|
|
||||||
|
|
||||||
await cloneRepository(configuration)
|
|
||||||
|
|
||||||
let foundService = false
|
|
||||||
let foundDomain = false
|
|
||||||
let configChanged = false
|
|
||||||
let imageChanged = false
|
|
||||||
|
|
||||||
let forceUpdate = false
|
|
||||||
|
|
||||||
for (const service of services) {
|
|
||||||
const running = JSON.parse(service.Spec.Labels.configuration)
|
|
||||||
if (running) {
|
|
||||||
if (
|
|
||||||
running.publish.domain === configuration.publish.domain &&
|
|
||||||
running.repository.id !== configuration.repository.id &&
|
|
||||||
running.repository.branch !== configuration.repository.branch
|
|
||||||
) {
|
|
||||||
foundDomain = true
|
|
||||||
}
|
|
||||||
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
|
|
||||||
const state = await execShellAsync(`docker stack ps ${running.build.container.name} --format '{{ json . }}'`)
|
|
||||||
const isError = state.split('\n').filter(n => n).map(s => JSON.parse(s)).filter(n => n.DesiredState !== 'Running')
|
|
||||||
if (isError.length > 0) forceUpdate = true
|
|
||||||
foundService = true
|
|
||||||
|
|
||||||
const runningWithoutContainer = JSON.parse(JSON.stringify(running))
|
|
||||||
delete runningWithoutContainer.build.container
|
|
||||||
|
|
||||||
const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration))
|
|
||||||
delete configurationWithoutContainer.build.container
|
|
||||||
|
|
||||||
if (JSON.stringify(runningWithoutContainer.build) !== JSON.stringify(configurationWithoutContainer.build) || JSON.stringify(runningWithoutContainer.publish) !== JSON.stringify(configurationWithoutContainer.publish)) configChanged = true
|
|
||||||
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (foundDomain) {
|
|
||||||
cleanupTmp(configuration.general.workdir)
|
|
||||||
reply.code(500).send({ message: 'Domain already used.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (forceUpdate) {
|
|
||||||
imageChanged = false
|
|
||||||
configChanged = false
|
|
||||||
} else {
|
|
||||||
if (foundService && !imageChanged && !configChanged) {
|
|
||||||
cleanupTmp(configuration.general.workdir)
|
cleanupTmp(configuration.general.workdir)
|
||||||
reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
|
reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const alreadyQueued = await Deployment.find({
|
||||||
|
repoId: configuration.repository.id,
|
||||||
|
branch: configuration.repository.branch,
|
||||||
|
organization: configuration.repository.organization,
|
||||||
|
name: configuration.repository.name,
|
||||||
|
domain: configuration.publish.domain,
|
||||||
|
progress: { $in: ['queued', 'inprogress'] }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (alreadyQueued.length > 0) {
|
||||||
|
reply.code(200).send({ message: 'Already in the queue.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
|
||||||
|
await queueAndBuild(configuration, imageChanged)
|
||||||
|
} catch (error) {
|
||||||
|
const { id, organization, name, branch } = configuration.repository
|
||||||
|
const { domain } = configuration.publish
|
||||||
|
const { deployId } = configuration.general
|
||||||
|
await Deployment.findOneAndUpdate(
|
||||||
|
{ repoId: id, branch, deployId, organization, name, domain },
|
||||||
|
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
||||||
|
if (error.name === 'Error') {
|
||||||
|
// Error during runtime
|
||||||
|
await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
|
||||||
|
} else {
|
||||||
|
// Error in my code
|
||||||
|
const payload = { message: error.message, stack: error.stack, type: 'spaghetticode' }
|
||||||
|
if (error.message && error.stack) await new ServerLog(payload).save()
|
||||||
|
if (reply.sent) await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
|
||||||
|
}
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
cleanupTmp(configuration.general.workdir)
|
||||||
|
await purgeImagesContainers(configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
const alreadyQueued = await Deployment.find({
|
|
||||||
repoId: configuration.repository.id,
|
|
||||||
branch: configuration.repository.branch,
|
|
||||||
organization: configuration.repository.organization,
|
|
||||||
name: configuration.repository.name,
|
|
||||||
domain: configuration.publish.domain,
|
|
||||||
progress: { $in: ['queued', 'inprogress'] }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (alreadyQueued.length > 0) {
|
|
||||||
reply.code(200).send({ message: 'Already in the queue.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
queueAndBuild(configuration, services, configChanged, imageChanged)
|
|
||||||
|
|
||||||
reply.code(201).send({ message: 'Deployment queued.' })
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
const { saveServerLog } = require('./libs/logging')
|
|
||||||
const Deployment = require('./models/Deployment')
|
|
||||||
const fastify = require('fastify')({
|
|
||||||
logger: { level: 'error' }
|
|
||||||
})
|
|
||||||
const mongoose = require('mongoose')
|
const mongoose = require('mongoose')
|
||||||
const path = require('path')
|
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')
|
const { schema } = require('./schema')
|
||||||
|
|
||||||
|
process.on('unhandledRejection', async (reason, p) => {
|
||||||
|
await saveServerLog({ message: reason.message, type: 'unhandledRejection' })
|
||||||
|
})
|
||||||
|
|
||||||
fastify.register(require('fastify-env'), {
|
fastify.register(require('fastify-env'), {
|
||||||
schema,
|
schema,
|
||||||
dotenv: true
|
dotenv: true
|
||||||
@@ -30,15 +40,6 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fastify.register(require('./app'), { prefix: '/api/v1' })
|
fastify.register(require('./app'), { prefix: '/api/v1' })
|
||||||
fastify.setErrorHandler(async (error, request, reply) => {
|
|
||||||
console.log(error)
|
|
||||||
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?' })
|
|
||||||
}
|
|
||||||
await saveServerLog({ event: error })
|
|
||||||
})
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
mongoose.connect(
|
mongoose.connect(
|
||||||
@@ -82,9 +83,27 @@ mongoose.connection.once('open', async function () {
|
|||||||
fastify.listen(3001)
|
fastify.listen(3001)
|
||||||
console.log('Coolify API is up and running in development.')
|
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.
|
// On start cleanup inprogress/queued deployments.
|
||||||
const deployments = await Deployment.find({ progress: { $in: ['queued', 'inprogress'] } })
|
try {
|
||||||
for (const deployment of deployments) {
|
await cleanupStuckedDeploymentsInDB()
|
||||||
await Deployment.findByIdAndUpdate(deployment._id, { $set: { progress: 'failed' } })
|
} catch (error) {
|
||||||
|
// Could not cleanup DB 🤔
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Doing because I do not want to prune these images. Prune skips coolify-reserve labeled images.
|
||||||
|
const basicImages = ['nginx:stable-alpine', 'node:lts', 'ubuntu:20.04', 'php:apache', 'rust:latest']
|
||||||
|
for (const image of basicImages) {
|
||||||
|
// await execShellAsync(`echo "FROM ${image}" | docker build --label coolify-reserve=true -t ${image} -`)
|
||||||
|
await execShellAsync(`docker pull ${image}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Could not pull some basic images from Docker Hub.')
|
||||||
|
console.log(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
<link rel="preload" as="image" href="/favicon.png">
|
<link rel="preload" as="image" href="/favicon.png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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="dns-prefetch" href="https://cdn.coollabs.io/" />
|
||||||
<link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" />
|
<link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" />
|
||||||
<link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" />
|
<link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.coollabs.io/css/microtip-0.2.2.min.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
73
install.sh
73
install.sh
@@ -1,43 +1,88 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
preTasks() {
|
||||||
|
echo '
|
||||||
|
##############################
|
||||||
|
#### Pulling Git Updates #####
|
||||||
|
##############################'
|
||||||
GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" git pull
|
GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" git pull
|
||||||
echo "#### Building base image."
|
|
||||||
docker build -t coolify-base -f install/Dockerfile-base .
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo '#### Ooops something not okay!'
|
echo '
|
||||||
|
####################################
|
||||||
|
#### Ooops something not okay! #####
|
||||||
|
####################################'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "#### Checking configuration."
|
echo '
|
||||||
|
##############################
|
||||||
|
#### Building Base Image #####
|
||||||
|
##############################'
|
||||||
|
docker build --label coolify-reserve=true -t coolify-base -f install/Dockerfile-base .
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo '
|
||||||
|
####################################
|
||||||
|
#### Ooops something not okay! #####
|
||||||
|
####################################'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo '
|
||||||
|
##################################
|
||||||
|
#### Checking configuration. #####
|
||||||
|
##################################'
|
||||||
docker run --rm -w /usr/src/app coolify-base node install/install.js --check
|
docker run --rm -w /usr/src/app coolify-base node install/install.js --check
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo '#### Missing configuration.'
|
echo '
|
||||||
|
##################################
|
||||||
|
#### Missing configuration ! #####
|
||||||
|
##################################'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
case "$1" in
|
case "$1" in
|
||||||
"all")
|
"all")
|
||||||
echo "#### Rebuild everything."
|
preTasks
|
||||||
|
echo '
|
||||||
|
#################################
|
||||||
|
#### Rebuilding everything. #####
|
||||||
|
#################################'
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/install.js --type all
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/install.js --type all
|
||||||
;;
|
;;
|
||||||
"coolify")
|
"coolify")
|
||||||
echo "#### Rebuild coolify."
|
preTasks
|
||||||
|
echo '
|
||||||
|
##############################
|
||||||
|
#### Rebuilding Coolify. #####
|
||||||
|
##############################'
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/install.js --type coolify
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/install.js --type coolify
|
||||||
;;
|
;;
|
||||||
"proxy")
|
"proxy")
|
||||||
echo "#### Rebuild proxy."
|
preTasks
|
||||||
|
echo '
|
||||||
|
############################
|
||||||
|
#### Rebuilding Proxy. #####
|
||||||
|
############################'
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/install.js --type proxy
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/install.js --type proxy
|
||||||
;;
|
;;
|
||||||
"upgrade-phase-1")
|
"upgrade-phase-1")
|
||||||
echo "#### Rebuild coolify from frontend request phase 1."
|
preTasks
|
||||||
|
echo '
|
||||||
|
################################
|
||||||
|
#### Upgrading Coolify P1. #####
|
||||||
|
################################'
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/install.js --type upgrade
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/install.js --type upgrade
|
||||||
;;
|
;;
|
||||||
"upgrade-phase-2")
|
"upgrade-phase-2")
|
||||||
echo "#### Rebuild coolify from frontend request phase 2."
|
echo '
|
||||||
|
################################
|
||||||
|
#### Upgrading Coolify P2. #####
|
||||||
|
################################'
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/update.js --type upgrade
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify:/data/coolify -u root -w /usr/src/app coolify-base node install/update.js --type upgrade
|
||||||
;;
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
echo "Use 'all' to build & deploy proxy+coolify, 'coolify' to build & deploy only coolify, 'proxy' to build & deploy only proxy."
|
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM coolify-base
|
FROM coolify-base
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
RUN yarn build
|
RUN pnpm build
|
||||||
CMD ["yarn", "start"]
|
CMD ["pnpm", "start"]
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
@@ -9,9 +9,10 @@ RUN apt update && apt install -y docker-ce-cli && apt clean all
|
|||||||
FROM node:14 as modules
|
FROM node:14 as modules
|
||||||
COPY --from=binaries /usr/bin/docker /usr/bin/docker
|
COPY --from=binaries /usr/bin/docker /usr/bin/docker
|
||||||
COPY --from=binaries /usr/bin/envsubst /usr/bin/envsubst
|
COPY --from=binaries /usr/bin/envsubst /usr/bin/envsubst
|
||||||
|
RUN curl -L https://pnpm.js.org/pnpm.js | node - add --global pnpm
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./package*.json .
|
COPY ./package*.json .
|
||||||
RUN yarn install
|
RUN pnpm install
|
||||||
|
|
||||||
FROM modules
|
FROM modules
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|||||||
15
install/Dockerfile-new
Normal file
15
install/Dockerfile-new
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM node:lts
|
||||||
|
LABEL coolify-preserve=true
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
RUN curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-20.10.6.tgz | tar -xzvf - docker/docker -C . --strip-components 1
|
||||||
|
RUN mv /usr/src/app/docker /usr/bin/docker
|
||||||
|
RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o /usr/bin/envsubst
|
||||||
|
RUN curl -L https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o /usr/bin/jq
|
||||||
|
RUN chmod +x /usr/bin/envsubst /usr/bin/jq /usr/bin/docker
|
||||||
|
RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm
|
||||||
|
COPY ./*package.json .
|
||||||
|
RUN pnpm install
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm build
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
|
EXPOSE 3000
|
||||||
10
install/README.md
Normal file
10
install/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Some of the files are here for backwards compatibility.
|
||||||
|
|
||||||
|
I will do things after 2 months:
|
||||||
|
|
||||||
|
- rm ./install.js and ./update.js
|
||||||
|
- rm ../install.sh
|
||||||
|
- rm ./Dockerfile-base
|
||||||
|
- rm ./obs
|
||||||
|
- rm ./check.js "No need to check env file. During installation, it is checked by the installer. If you change it between to upgrades: 🤷♂️"
|
||||||
|
- Rename Dockerfile-new to Dockerfile
|
||||||
24
install/check.js
Normal file
24
install/check.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
const fastify = require('fastify')()
|
||||||
|
const { schema } = require('../api/schema')
|
||||||
|
|
||||||
|
checkConfig().then(() => {
|
||||||
|
console.log('Config: OK')
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log('Config: NOT OK')
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
function checkConfig () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fastify.register(require('fastify-env'), {
|
||||||
|
schema,
|
||||||
|
dotenv: true
|
||||||
|
})
|
||||||
|
.ready((err) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
73
install/coolify-template-dev.yml
Normal file
73
install/coolify-template-dev.yml
Normal 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
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ version: '3.8'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
proxy:
|
proxy:
|
||||||
image: traefik:v2.3
|
image: traefik:v2.4
|
||||||
hostname: coollabs-proxy
|
hostname: coollabs-proxy
|
||||||
ports:
|
ports:
|
||||||
- target: 80
|
- target: 80
|
||||||
@@ -22,6 +22,7 @@ services:
|
|||||||
- --providers.docker.swarmMode=true
|
- --providers.docker.swarmMode=true
|
||||||
- --providers.docker.exposedbydefault=false
|
- --providers.docker.exposedbydefault=false
|
||||||
- --providers.docker.network=${DOCKER_NETWORK}
|
- --providers.docker.network=${DOCKER_NETWORK}
|
||||||
|
- --providers.docker.swarmModeRefreshSeconds=1s
|
||||||
- --entrypoints.web.address=:80
|
- --entrypoints.web.address=:80
|
||||||
- --entrypoints.websecure.address=:443
|
- --entrypoints.websecure.address=:443
|
||||||
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
|
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ program
|
|||||||
|
|
||||||
program.parse(process.argv)
|
program.parse(process.argv)
|
||||||
|
|
||||||
if (program.check) {
|
const options = program.opts()
|
||||||
|
if (options.check) {
|
||||||
checkConfig().then(() => {
|
checkConfig().then(() => {
|
||||||
console.log('Config: OK')
|
console.log('Config: OK')
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@@ -26,16 +27,18 @@ if (program.check) {
|
|||||||
console.error(`Please run as root! Current user: ${user}`)
|
console.error(`Please run as root! Current user: ${user}`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
shell.exec(`docker network create ${process.env.DOCKER_NETWORK} --driver overlay`, { silent: !program.debug })
|
shell.exec(`docker network create ${process.env.DOCKER_NETWORK} --driver overlay`, { silent: !options.debug })
|
||||||
shell.exec('docker build -t coolify -f install/Dockerfile .')
|
shell.exec('docker build -t coolify -f install/Dockerfile .')
|
||||||
if (program.type === 'all') {
|
if (options.type === 'all') {
|
||||||
shell.exec('docker stack rm coollabs-coolify', { silent: !program.debug })
|
shell.exec('docker stack rm coollabs-coolify', { silent: !options.debug })
|
||||||
} else if (program.type === 'coolify') {
|
} else if (options.type === 'coolify') {
|
||||||
shell.exec('docker service rm coollabs-coolify_coolify')
|
shell.exec('docker service rm coollabs-coolify_coolify')
|
||||||
} else if (program.type === 'proxy') {
|
} else if (options.type === 'proxy') {
|
||||||
shell.exec('docker service rm coollabs-coolify_proxy')
|
shell.exec('docker service rm coollabs-coolify_proxy')
|
||||||
}
|
}
|
||||||
if (program.type !== 'upgrade') shell.exec('set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify', { silent: !program.debug, shell: '/bin/bash' })
|
if (options.type !== 'upgrade') {
|
||||||
|
shell.exec('set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify', { silent: !options.debug, shell: '/bin/bash' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkConfig () {
|
function checkConfig () {
|
||||||
|
|||||||
4
install/obs/Dockerfile-base-new
Normal file
4
install/obs/Dockerfile-base-new
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM coolify-base-nodejs
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm install
|
||||||
6
install/obs/Dockerfile-base-nodejs
Normal file
6
install/obs/Dockerfile-base-nodejs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM node:lts
|
||||||
|
LABEL coolify-preserve=true
|
||||||
|
COPY --from=coolify-binaries /usr/bin/docker /usr/bin/docker
|
||||||
|
COPY --from=coolify-binaries /usr/bin/envsubst /usr/bin/envsubst
|
||||||
|
COPY --from=coolify-binaries /usr/bin/jq /usr/bin/jq
|
||||||
|
RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm@6
|
||||||
9
install/obs/Dockerfile-binaries
Normal file
9
install/obs/Dockerfile-binaries
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM ubuntu:20.04
|
||||||
|
LABEL coolify-preserve=true
|
||||||
|
RUN apt update && apt install -y curl gnupg2 ca-certificates
|
||||||
|
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
|
||||||
|
RUN echo 'deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable' >> /etc/apt/sources.list
|
||||||
|
RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o /usr/bin/envsubst
|
||||||
|
RUN curl -L https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o /usr/bin/jq
|
||||||
|
RUN chmod +x /usr/bin/envsubst /usr/bin/jq
|
||||||
|
RUN apt update && apt install -y docker-ce-cli && apt clean all
|
||||||
@@ -2,7 +2,6 @@ require('dotenv').config()
|
|||||||
const { program } = require('commander')
|
const { program } = require('commander')
|
||||||
const shell = require('shelljs')
|
const shell = require('shelljs')
|
||||||
const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '')
|
const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '')
|
||||||
|
|
||||||
program.version('0.0.1')
|
program.version('0.0.1')
|
||||||
program
|
program
|
||||||
.option('-d, --debug', 'Debug outputs.')
|
.option('-d, --debug', 'Debug outputs.')
|
||||||
@@ -10,12 +9,13 @@ program
|
|||||||
.option('-t, --type <type>', 'Deploy type.')
|
.option('-t, --type <type>', 'Deploy type.')
|
||||||
|
|
||||||
program.parse(process.argv)
|
program.parse(process.argv)
|
||||||
|
const options = program.opts()
|
||||||
if (user !== 'root') {
|
if (user !== 'root') {
|
||||||
console.error(`Please run as root! Current user: ${user}`)
|
console.error(`Please run as root! Current user: ${user}`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
if (program.type === 'upgrade') {
|
|
||||||
|
if (options.type === 'upgrade') {
|
||||||
shell.exec('docker service rm coollabs-coolify_coolify')
|
shell.exec('docker service rm coollabs-coolify_coolify')
|
||||||
shell.exec('set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify', { silent: !program.debug, shell: '/bin/bash' })
|
shell.exec('set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify', { silent: !options.debug, shell: '/bin/bash' })
|
||||||
}
|
}
|
||||||
|
|||||||
45
package.json
45
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "coolify",
|
"name": "coolify",
|
||||||
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
|
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.11",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "standard",
|
"lint": "standard",
|
||||||
@@ -16,43 +16,48 @@
|
|||||||
"build:svite": "svite build"
|
"build:svite": "svite build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@roxi/routify": "^2.7.3",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@zerodevx/svelte-toast": "^0.1.4",
|
"@roxi/routify": "^2.15.1",
|
||||||
"axios": "^0.21.0",
|
"@zerodevx/svelte-toast": "^0.2.2",
|
||||||
"commander": "^6.2.1",
|
"ajv": "^8.1.0",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"commander": "^7.2.0",
|
||||||
|
"compare-versions": "^3.6.0",
|
||||||
"cuid": "^2.1.8",
|
"cuid": "^2.1.8",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"dockerode": "^3.2.1",
|
"dockerode": "^3.2.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"fastify": "^3.9.1",
|
"fastify": "^3.14.2",
|
||||||
"fastify-env": "^2.1.0",
|
"fastify-env": "^2.1.0",
|
||||||
"fastify-jwt": "^2.1.3",
|
"fastify-jwt": "^2.4.0",
|
||||||
"fastify-plugin": "^3.0.0",
|
"fastify-plugin": "^3.0.0",
|
||||||
"fastify-static": "^3.3.0",
|
"fastify-static": "^4.0.1",
|
||||||
"generate-password": "^1.6.0",
|
"generate-password": "^1.6.0",
|
||||||
|
"http-errors-enhanced": "^0.7.0",
|
||||||
"js-yaml": "^4.0.0",
|
"js-yaml": "^4.0.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mongoose": "^5.11.4",
|
"mongoose": "^5.12.3",
|
||||||
"shelljs": "^0.8.4",
|
"shelljs": "^0.8.4",
|
||||||
|
"svelte-select": "^3.17.0",
|
||||||
"unique-names-generator": "^4.4.0"
|
"unique-names-generator": "^4.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mongodb-memory-server-core": "^6.9.3",
|
"mongodb-memory-server-core": "^6.9.6",
|
||||||
"nodemon": "^2.0.6",
|
"nodemon": "^2.0.7",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss": "^7.0.35",
|
"postcss": "^8.2.9",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-import": "^14.0.1",
|
||||||
"postcss-load-config": "^3.0.0",
|
"postcss-load-config": "^3.0.1",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"prettier": "1.19",
|
"prettier": "2.2.1",
|
||||||
"prettier-plugin-svelte": "^2.1.6",
|
"prettier-plugin-svelte": "^2.2.0",
|
||||||
"standard": "^16.0.3",
|
"standard": "^16.0.3",
|
||||||
"svelte": "^3.29.7",
|
"svelte": "^3.37.0",
|
||||||
"svelte-hmr": "^0.12.2",
|
"svelte-hmr": "^0.14.0",
|
||||||
"svelte-preprocess": "^4.6.1",
|
"svelte-preprocess": "^4.7.0",
|
||||||
"svite": "0.8.1",
|
"svite": "0.8.1",
|
||||||
"tailwindcss": "compat"
|
"tailwindcss": "2.1.1"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte",
|
"svelte",
|
||||||
|
|||||||
6169
pnpm-lock.yaml
generated
6169
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,12 +3,15 @@
|
|||||||
import { Router } from "@roxi/routify";
|
import { Router } from "@roxi/routify";
|
||||||
import { routes } from "../.routify/routes";
|
import { routes } from "../.routify/routes";
|
||||||
const options = {
|
const options = {
|
||||||
duration: 2000,
|
duration: 2000
|
||||||
dismissable: false
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
:global(.main) {
|
||||||
|
width: calc(100% - 4rem);
|
||||||
|
margin-left: 4rem;
|
||||||
|
}
|
||||||
:global(._toastMsg) {
|
:global(._toastMsg) {
|
||||||
@apply text-sm font-bold !important;
|
@apply text-sm font-bold !important;
|
||||||
}
|
}
|
||||||
@@ -28,7 +31,7 @@
|
|||||||
@apply bg-warmGray-700 !important;
|
@apply bg-warmGray-700 !important;
|
||||||
}
|
}
|
||||||
:global(input) {
|
: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) {
|
:global(input:hover) {
|
||||||
@apply bg-warmGray-700 !important;
|
@apply bg-warmGray-700 !important;
|
||||||
@@ -57,6 +60,22 @@
|
|||||||
:global(.h-271) {
|
:global(.h-271) {
|
||||||
min-height: 271px !important;
|
min-height: 271px !important;
|
||||||
}
|
}
|
||||||
|
:global(.repository-select-search .listItem .item),
|
||||||
|
:global(.repository-select-search .empty) {
|
||||||
|
@apply text-sm py-3 font-bold bg-warmGray-800 text-white cursor-pointer border-none hover:bg-warmGray-700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.repository-select-search .listContainer) {
|
||||||
|
@apply bg-transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.repository-select-search .clearSelect) {
|
||||||
|
@apply text-white cursor-pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.repository-select-search .selectedItem) {
|
||||||
|
@apply text-white relative cursor-pointer font-bold text-sm flex items-center !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<SvelteToast options="{options}" />
|
<SvelteToast options="{options}" />
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { application} from "@store";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 space-y-2 max-w-2xl md:mx-auto mx-6 text-center">
|
|
||||||
<label for="buildCommand">Build Command</label>
|
|
||||||
<input
|
|
||||||
id="buildCommand"
|
|
||||||
bind:value="{$application.build.command.build}"
|
|
||||||
placeholder="eg: yarn build"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for="installCommand">Install Command</label>
|
|
||||||
<input
|
|
||||||
id="installCommand"
|
|
||||||
bind:value="{$application.build.command.installation}"
|
|
||||||
placeholder="eg: yarn install"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for="baseDir">Base Directory</label>
|
|
||||||
<input id="baseDir" bind:value="{$application.build.directory}" placeholder="/" />
|
|
||||||
</div>
|
|
||||||
@@ -1,106 +1,340 @@
|
|||||||
|
<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>
|
<script>
|
||||||
import { application } from "@store";
|
import { application } from "@store";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import TooltipInfo from "../../../Tooltip/TooltipInfo.svelte";
|
||||||
|
let domainInput;
|
||||||
|
const buildpacks = {
|
||||||
|
static: {
|
||||||
|
port: {
|
||||||
|
active: false,
|
||||||
|
number: 80,
|
||||||
|
},
|
||||||
|
build: true,
|
||||||
|
},
|
||||||
|
nodejs: {
|
||||||
|
port: {
|
||||||
|
active: true,
|
||||||
|
number: 3000,
|
||||||
|
},
|
||||||
|
build: true,
|
||||||
|
},
|
||||||
|
vuejs: {
|
||||||
|
port: {
|
||||||
|
active: false,
|
||||||
|
number: 80,
|
||||||
|
},
|
||||||
|
build: true,
|
||||||
|
},
|
||||||
|
nuxtjs: {
|
||||||
|
port: {
|
||||||
|
active: true,
|
||||||
|
number: 3000,
|
||||||
|
},
|
||||||
|
build: true,
|
||||||
|
},
|
||||||
|
react: {
|
||||||
|
port: {
|
||||||
|
active: false,
|
||||||
|
number: 80,
|
||||||
|
},
|
||||||
|
build: true,
|
||||||
|
},
|
||||||
|
nextjs: {
|
||||||
|
port: {
|
||||||
|
active: true,
|
||||||
|
number: 3000,
|
||||||
|
},
|
||||||
|
build: true,
|
||||||
|
},
|
||||||
|
gatsby: {
|
||||||
|
port: {
|
||||||
|
active: true,
|
||||||
|
number: 3000,
|
||||||
|
},
|
||||||
|
build: true,
|
||||||
|
},
|
||||||
|
svelte: {
|
||||||
|
port: {
|
||||||
|
active: false,
|
||||||
|
number: 80,
|
||||||
|
},
|
||||||
|
build: true,
|
||||||
|
},
|
||||||
|
php: {
|
||||||
|
port: {
|
||||||
|
active: false,
|
||||||
|
number: 80,
|
||||||
|
},
|
||||||
|
build: false,
|
||||||
|
},
|
||||||
|
rust: {
|
||||||
|
port: {
|
||||||
|
active: true,
|
||||||
|
number: 3000,
|
||||||
|
},
|
||||||
|
build: false,
|
||||||
|
},
|
||||||
|
docker: {
|
||||||
|
port: {
|
||||||
|
active: true,
|
||||||
|
number: 3000,
|
||||||
|
},
|
||||||
|
build: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
function selectBuildPack(event) {
|
||||||
|
if (event.target.innerText === "React/Preact") {
|
||||||
|
$application.build.pack = "react";
|
||||||
|
} else {
|
||||||
|
$application.build.pack = event.target.innerText
|
||||||
|
.replace(/\./g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMount(()=> {
|
||||||
|
domainInput.focus();
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 text-sm space-y-2 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</label>
|
<div class="text-2xl font-bold border-gradient w-40">Build Packs</div>
|
||||||
<select id="buildPack" bind:value="{$application.build.pack}">
|
<div class="flex font-bold flex-wrap justify-center pt-10">
|
||||||
<option selected class="font-medium">static</option>
|
<div
|
||||||
<option class="font-medium">nodejs</option>
|
class="{$application.build.pack === 'static'
|
||||||
</select>
|
? '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
|
</div>
|
||||||
class="grid grid-cols-2 space-y-2 max-w-2xl md:mx-auto mx-6 justify-center items-center"
|
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
|
||||||
>
|
<div
|
||||||
<label for="Domain">Domain</label>
|
class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10"
|
||||||
<input
|
>
|
||||||
class:placeholder-red-500="{$application.publish.domain == null || $application.publish.domain == ''}"
|
<div class="grid grid-flow-col gap-2 items-center pb-6">
|
||||||
class:border-red-500="{$application.publish.domain == null || $application.publish.domain == ''}"
|
<div class="grid grid-flow-row">
|
||||||
id="Domain"
|
<label for="Domain" class="">Domain</label>
|
||||||
bind:value="{$application.publish.domain}"
|
|
||||||
placeholder="eg: coollabs.io (without www)"
|
|
||||||
/>
|
|
||||||
<label for="Path">Path Prefix</label>
|
|
||||||
<input
|
|
||||||
id="Path"
|
|
||||||
bind:value="{$application.publish.path}"
|
|
||||||
placeholder="/"
|
|
||||||
/>
|
|
||||||
<label for="publishDir">Publish Directory</label>
|
|
||||||
<input
|
|
||||||
id="publishDir"
|
|
||||||
bind:value="{$application.publish.directory}"
|
|
||||||
placeholder="/"
|
|
||||||
/>
|
|
||||||
{#if $application.build.pack !== "static"}
|
|
||||||
<label for="Port">Port</label>
|
|
||||||
<input
|
<input
|
||||||
id="Port"
|
bind:this={domainInput}
|
||||||
bind:value="{$application.publish.port}"
|
class="border-2"
|
||||||
placeholder="{$application.build.pack === 'static'
|
class:placeholder-red-500="{$application.publish.domain == null ||
|
||||||
? '80'
|
$application.publish.domain == ''}"
|
||||||
: '3000'}"
|
class:border-red-500="{$application.publish.domain == null ||
|
||||||
|
$application.publish.domain == ''}"
|
||||||
|
id="Domain"
|
||||||
|
bind:value="{$application.publish.domain}"
|
||||||
|
placeholder="eg: coollabs.io (without www)"
|
||||||
/>
|
/>
|
||||||
{/if}
|
</div>
|
||||||
<!-- {#if config.buildPack === "static"}
|
<div class="grid grid-flow-row">
|
||||||
<div class="text-base font-bold text-white pt-2">
|
<label for="Path"
|
||||||
Preview Deploys
|
>Path <TooltipInfo
|
||||||
</div>
|
label="{`Path to deploy your application on your domain. eg: /api means it will be deployed to -> https://${
|
||||||
<button
|
$application.publish.domain || '<yourdomain>'
|
||||||
type="button"
|
}/api`}"
|
||||||
on:click="{() =>
|
/></label
|
||||||
(config.previewDeploy = !config.previewDeploy)}"
|
|
||||||
aria-pressed="false"
|
|
||||||
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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
|
|
||||||
class:bg-green-600="{config.previewDeploy}"
|
|
||||||
class:bg-coolgray-300="{!config.previewDeploy}"
|
|
||||||
>
|
>
|
||||||
<span class="sr-only">Use setting</span>
|
<input
|
||||||
<span
|
id="Path"
|
||||||
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
bind:value="{$application.publish.path}"
|
||||||
class:translate-x-5="{config.previewDeploy}"
|
placeholder="/"
|
||||||
class:translate-x-0="{!config.previewDeploy}"
|
/>
|
||||||
>
|
</div>
|
||||||
<span
|
|
||||||
class="ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
|
|
||||||
class:opacity-0="{config.previewDeploy}"
|
|
||||||
class:opacity-100="{!config.previewDeploy}"
|
|
||||||
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="{config.previewDeploy}"
|
|
||||||
class:opacity-0="{!config.previewDeploy}"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
{/if} -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<label
|
||||||
|
for="Port"
|
||||||
|
class:text-warmGray-800="{!buildpacks[$application.build.pack].port
|
||||||
|
.active}">Port</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
disabled="{!buildpacks[$application.build.pack].port.active}"
|
||||||
|
id="Port"
|
||||||
|
class:bg-warmGray-900="{!buildpacks[$application.build.pack].port.active}"
|
||||||
|
class:text-warmGray-900="{!buildpacks[$application.build.pack].port
|
||||||
|
.active}"
|
||||||
|
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack].port
|
||||||
|
.active}"
|
||||||
|
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack].port
|
||||||
|
.active}"
|
||||||
|
class:cursor-not-allowed="{!buildpacks[$application.build.pack].port
|
||||||
|
.active}"
|
||||||
|
bind:value="{$application.publish.port}"
|
||||||
|
placeholder="{buildpacks[$application.build.pack].port.number}"
|
||||||
|
/>
|
||||||
|
<div class="grid grid-flow-col gap-2 items-center pt-6 pb-12">
|
||||||
|
<div class="grid grid-flow-row">
|
||||||
|
<label for="baseDir"
|
||||||
|
>Base Directory <TooltipInfo
|
||||||
|
label="The directory to use as base for every command (could be useful if you have a monorepo)."
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="baseDir"
|
||||||
|
bind:value="{$application.build.directory}"
|
||||||
|
placeholder="eg: sourcedir"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-flow-row">
|
||||||
|
<label for="publishDir"
|
||||||
|
>Publish Directory <TooltipInfo
|
||||||
|
label="The directory to deploy after running the build command. eg: dist, _site, public."
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="publishDir"
|
||||||
|
bind:value="{$application.publish.directory}"
|
||||||
|
placeholder="eg: dist, _site, public"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold w-40"
|
||||||
|
class:border-gradient="{buildpacks[$application.build.pack].build}"
|
||||||
|
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
|
||||||
|
>
|
||||||
|
Commands
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class=" max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10 pb-32"
|
||||||
|
>
|
||||||
|
<div class="grid grid-flow-col gap-2 items-center">
|
||||||
|
<div class="grid grid-flow-row">
|
||||||
|
<label
|
||||||
|
for="installCommand"
|
||||||
|
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
|
||||||
|
>Install Command <TooltipInfo
|
||||||
|
label="Command to run for installing dependencies. eg: yarn install."
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="mb-6"
|
||||||
|
class:bg-warmGray-900="{!buildpacks[$application.build.pack].build}"
|
||||||
|
class:text-warmGray-900="{!buildpacks[$application.build.pack].build}"
|
||||||
|
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack]
|
||||||
|
.build}"
|
||||||
|
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack]
|
||||||
|
.build}"
|
||||||
|
class:cursor-not-allowed="{!buildpacks[$application.build.pack]
|
||||||
|
.build}"
|
||||||
|
id="installCommand"
|
||||||
|
bind:value="{$application.build.command.installation}"
|
||||||
|
placeholder="eg: yarn install"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="buildCommand"
|
||||||
|
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
|
||||||
|
>Build Command <TooltipInfo
|
||||||
|
label="Command to run for building your application. If empty, no build phase initiated in the deploy process."
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="mb-6"
|
||||||
|
class:bg-warmGray-900="{!buildpacks[$application.build.pack].build}"
|
||||||
|
class:text-warmGray-900="{!buildpacks[$application.build.pack].build}"
|
||||||
|
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack]
|
||||||
|
.build}"
|
||||||
|
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack]
|
||||||
|
.build}"
|
||||||
|
class:cursor-not-allowed="{!buildpacks[$application.build.pack]
|
||||||
|
.build}"
|
||||||
|
id="buildCommand"
|
||||||
|
bind:value="{$application.build.command.build}"
|
||||||
|
placeholder="eg: yarn build"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -9,64 +9,72 @@
|
|||||||
async function saveSecret() {
|
async function saveSecret() {
|
||||||
if (secret.name && secret.value) {
|
if (secret.name && secret.value) {
|
||||||
const found = $application.publish.secrets.find(
|
const found = $application.publish.secrets.find(
|
||||||
s => s.name === secret.name,
|
s => s.name === secret.name,
|
||||||
);
|
);
|
||||||
if (!found) {
|
if (!found) {
|
||||||
$application.publish.secrets = [
|
$application.publish.secrets = [
|
||||||
...$application.publish.secrets,
|
...$application.publish.secrets,
|
||||||
{
|
{
|
||||||
name: secret.name,
|
name: secret.name,
|
||||||
value: secret.value,
|
value: secret.value,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
secret = {
|
secret = {
|
||||||
name: null,
|
name: null,
|
||||||
value: null
|
value: null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
foundSecret = found;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
foundSecret = found;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeSecret(name) {
|
async function removeSecret(name) {
|
||||||
$application.publish.secrets = [...$application.publish.secrets.filter(s => s.name !== name)]
|
foundSecret = null
|
||||||
|
$application.publish.secrets = [
|
||||||
|
...$application.publish.secrets.filter(s => s.name !== name),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<div class="text-2xl font-bold border-gradient w-24">Secrets</div>
|
||||||
<div class="space-y-2 max-w-2xl md:mx-auto mx-6 text-center">
|
<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="text-left text-base font-bold tracking-tight text-warmGray-400">
|
||||||
<div class="grid md:grid-flow-col grid-flow-row gap-2">
|
New Secret
|
||||||
<input id="secretName" bind:value="{secret.name}" placeholder="Name" />
|
</div>
|
||||||
<input id="secretValue" bind:value="{secret.value}" placeholder="Value" />
|
<div class="flex space-x-4">
|
||||||
<button
|
<input id="secretName" bind:value="{secret.name}" placeholder="Name" class="w-64 border-2 border-transparent" />
|
||||||
class="button p-1 w-20 bg-green-600 hover:bg-green-500 text-white"
|
<input id="secretValue" bind:value="{secret.value}" placeholder="Value" class="w-64 border-2 border-transparent" />
|
||||||
on:click="{saveSecret}">Save</button
|
<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>
|
</div>
|
||||||
{#if $application.publish.secrets.length > 0}
|
{#if $application.publish.secrets.length > 0}
|
||||||
{#each $application.publish.secrets as s}
|
<div class="py-4">
|
||||||
<div class="grid md:grid-flow-col grid-flow-row gap-2">
|
{#each $application.publish.secrets as s}
|
||||||
<input
|
<div class="flex space-x-4">
|
||||||
id="{s.name}"
|
<input
|
||||||
value="{s.name}"
|
id="{s.name}"
|
||||||
disabled
|
value="{s.name}"
|
||||||
class="bg-transparent border-transparent"
|
disabled
|
||||||
class:border-red-600="{foundSecret && foundSecret.name === s.name}"
|
class="border-2 bg-transparent border-transparent w-64"
|
||||||
/>
|
class:border-red-600="{foundSecret && foundSecret.name === s.name}"
|
||||||
<input
|
/>
|
||||||
id="{s.createdAt}"
|
<input
|
||||||
value="ENCRYPTED"
|
id="{s.createdAt}"
|
||||||
disabled
|
value="SAVED"
|
||||||
class="bg-transparent border-transparent"
|
disabled
|
||||||
/>
|
class="border-2 bg-transparent border-transparent w-64"
|
||||||
<button
|
/>
|
||||||
class="button w-20 bg-red-600 hover:bg-red-500 text-white"
|
<button class="icon hover:text-red-500" on:click="{() => removeSecret(s.name)}">
|
||||||
on:click="{() => removeSecret(s.name)}">Delete</button
|
<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" />
|
||||||
</div>
|
</svg>
|
||||||
{/each}
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,45 @@
|
|||||||
<script>
|
<script>
|
||||||
export let loading, branches;
|
export let loading, branches;
|
||||||
import { application } from "@store";
|
import { application, activePage } from "@store";
|
||||||
|
import Select from "svelte-select";
|
||||||
|
|
||||||
|
const selectedValue =
|
||||||
|
$activePage.application !== "new" && $application.repository.branch;
|
||||||
|
|
||||||
|
function handleSelect(event) {
|
||||||
|
$application.repository.branch = null;
|
||||||
|
setTimeout(() => {
|
||||||
|
$application.repository.branch = event.detail.value;
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="grid grid-cols-1">
|
<div class="grid grid-cols-1">
|
||||||
<label for="branch">Branch</label>
|
<label for="branch">Branch</label>
|
||||||
<select disabled>
|
<div class="repository-select-search col-span-2">
|
||||||
<option selected>Loading branches</option>
|
<Select
|
||||||
</select>
|
containerClasses="w-full border-none bg-transparent"
|
||||||
|
placeholder="Loading branches..."
|
||||||
|
isDisabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-1">
|
<div class="grid grid-cols-1">
|
||||||
<label for="branch">Branch</label>
|
<label for="branch">Branch</label>
|
||||||
<!-- svelte-ignore a11y-no-onchange -->
|
<div class="repository-select-search col-span-2">
|
||||||
<select id="branch" bind:value="{$application.repository.branch}">
|
<Select
|
||||||
<option disabled selected>Select a branch</option>
|
containerClasses="w-full border-none bg-transparent"
|
||||||
{#each branches as branch}
|
on:select="{handleSelect}"
|
||||||
<option value="{branch.name}" class="font-medium">{branch.name}</option>
|
selectedValue="{selectedValue}"
|
||||||
{/each}
|
isClearable="{false}"
|
||||||
</select>
|
items="{branches.map(b => ({ label: b.name, value: b.name }))}"
|
||||||
|
showIndicator="{$activePage.new}"
|
||||||
|
noOptionsMessage="No branches found"
|
||||||
|
placeholder="Select a branch"
|
||||||
|
isDisabled="{!$activePage.new}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import { redirect, isActive } from "@roxi/routify";
|
import { redirect, isActive } from "@roxi/routify";
|
||||||
import { fade } from "svelte/transition";
|
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 Login from "./Login.svelte";
|
||||||
import Loading from "../../Loading.svelte";
|
import Loading from "../../Loading.svelte";
|
||||||
@@ -11,14 +19,24 @@
|
|||||||
|
|
||||||
let loading = {
|
let loading = {
|
||||||
branches: false,
|
branches: false,
|
||||||
|
github: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let branches = [];
|
let branches = [];
|
||||||
let repositories = [];
|
function dashify(str, options) {
|
||||||
|
if (typeof str !== "string") return str;
|
||||||
|
return str
|
||||||
|
.trim()
|
||||||
|
.replace(/\W/g, m => (/[À-ž]/.test(m) ? m : "-"))
|
||||||
|
.replace(/^-+|-+$/g, "")
|
||||||
|
.replace(/-{2,}/g, m => (options && options.condense ? "-" : m))
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadBranches() {
|
async function loadBranches() {
|
||||||
loading.branches = true;
|
loading.branches = true;
|
||||||
const selectedRepository = repositories.find(
|
if ($activePage.new) $application.repository.branch = null;
|
||||||
|
const selectedRepository = $githubRepositories.find(
|
||||||
r => r.id === $application.repository.id,
|
r => r.id === $application.repository.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -33,7 +51,33 @@
|
|||||||
loading.branches = false;
|
loading.branches = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getGithubRepos(id, page) {
|
||||||
|
const data = await $fetch(
|
||||||
|
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadGithub() {
|
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 {
|
try {
|
||||||
const { installations } = await $fetch(
|
const { installations } = await $fetch(
|
||||||
"https://api.github.com/user/installations",
|
"https://api.github.com/user/installations",
|
||||||
@@ -43,13 +87,29 @@
|
|||||||
}
|
}
|
||||||
$application.github.installation.id = installations[0].id;
|
$application.github.installation.id = installations[0].id;
|
||||||
$application.github.app.id = installations[0].app_id;
|
$application.github.app.id = installations[0].app_id;
|
||||||
|
$githubInstallations = installations[0];
|
||||||
|
|
||||||
const data = await $fetch(
|
let page = 1;
|
||||||
`https://api.github.com/user/installations/${$application.github.installation.id}/repositories?per_page=10000`,
|
let userRepos = 0;
|
||||||
|
const data = await getGithubRepos(
|
||||||
|
$application.github.installation.id,
|
||||||
|
page,
|
||||||
);
|
);
|
||||||
|
|
||||||
repositories = data.repositories;
|
$githubRepositories = $githubRepositories.concat(data.repositories);
|
||||||
const foundRepositoryOnGithub = data.repositories.find(
|
userRepos = data.total_count;
|
||||||
|
|
||||||
|
if (userRepos > $githubRepositories.length) {
|
||||||
|
while (userRepos > $githubRepositories.length) {
|
||||||
|
page = page + 1;
|
||||||
|
const repos = await getGithubRepos(
|
||||||
|
$application.github.installation.id,
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
$githubRepositories = $githubRepositories.concat(repos.repositories);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const foundRepositoryOnGithub = $githubRepositories.find(
|
||||||
r =>
|
r =>
|
||||||
r.full_name ===
|
r.full_name ===
|
||||||
`${$application.repository.organization}/${$application.repository.name}`,
|
`${$application.repository.organization}/${$application.repository.name}`,
|
||||||
@@ -61,15 +121,17 @@
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
|
} finally {
|
||||||
|
loading.github = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function modifyGithubAppConfig() {
|
function modifyGithubAppConfig() {
|
||||||
const left = screen.width / 2 - 1020 / 2;
|
const left = screen.width / 2 - 1020 / 2;
|
||||||
const top = screen.height / 2 - 618 / 2;
|
const top = screen.height / 2 - 618 / 2;
|
||||||
const newWindow = open(
|
const newWindow = open(
|
||||||
`https://github.com/apps/${
|
`https://github.com/apps/${dashify(
|
||||||
import.meta.env.VITE_GITHUB_APP_NAME
|
import.meta.env.VITE_GITHUB_APP_NAME,
|
||||||
}/installations/new`,
|
)}/installations/new`,
|
||||||
"Install App",
|
"Install App",
|
||||||
"resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=" +
|
"resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=" +
|
||||||
top +
|
top +
|
||||||
@@ -80,7 +142,8 @@
|
|||||||
const timer = setInterval(async () => {
|
const timer = setInterval(async () => {
|
||||||
if (newWindow.closed) {
|
if (newWindow.closed) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
if (!$isActive("/application/new")) {
|
loading.github = true;
|
||||||
|
if (!$activePage.new) {
|
||||||
try {
|
try {
|
||||||
const config = await $fetch(`/api/v1/config`, {
|
const config = await $fetch(`/api/v1/config`, {
|
||||||
body: {
|
body: {
|
||||||
@@ -97,37 +160,104 @@
|
|||||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||||
}
|
}
|
||||||
branches = [];
|
branches = [];
|
||||||
repositories = [];
|
$githubRepositories = [];
|
||||||
await loadGithub();
|
await loadGithub();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if !$activePage.new}
|
||||||
|
<div class="min-h-full text-white">
|
||||||
|
<div
|
||||||
|
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
|
||||||
|
>
|
||||||
|
{$application.publish.domain
|
||||||
|
? `${$application.publish.domain}${
|
||||||
|
$application.publish.path !== "/" ? $application.publish.path : ""
|
||||||
|
}`
|
||||||
|
: "example.com"}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
class="icon mx-2"
|
||||||
|
href="{'https://' +
|
||||||
|
$application.publish.domain +
|
||||||
|
$application.publish.path}"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||||
|
></path>
|
||||||
|
</svg></a
|
||||||
|
>
|
||||||
|
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
class="icon"
|
||||||
|
href="{`https://github.com/${$application.repository.organization}/${$application.repository.name}`}"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><path
|
||||||
|
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
||||||
|
></path></svg
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if $activePage.new}
|
||||||
|
<div class="min-h-full text-white">
|
||||||
|
<div
|
||||||
|
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
|
||||||
|
>
|
||||||
|
New Application
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div in:fade="{{ duration: 100 }}">
|
<div in:fade="{{ duration: 100 }}">
|
||||||
{#if !$session.githubAppToken}
|
{#if !$session.githubAppToken}
|
||||||
<Login />
|
<Login />
|
||||||
{:else}
|
{:else}
|
||||||
{#await loadGithub()}
|
{#await loadGithub()}
|
||||||
<Loading />
|
<Loading github githubLoadingText="Loading repositories..." />
|
||||||
{:then}
|
{:then}
|
||||||
<div
|
{#if loading.github}
|
||||||
class="text-center space-y-2 max-w-4xl mx-auto px-6"
|
<Loading github githubLoadingText="Loading repositories..." />
|
||||||
in:fade="{{ duration: 100 }}"
|
{:else}
|
||||||
>
|
<div
|
||||||
<Repositories
|
class="space-y-2 max-w-4xl mx-auto px-6"
|
||||||
bind:repositories
|
in:fade="{{ duration: 100 }}"
|
||||||
on:loadBranches="{loadBranches}"
|
>
|
||||||
on:modifyGithubAppConfig="{modifyGithubAppConfig}"
|
<Repositories
|
||||||
/>
|
on:loadBranches="{loadBranches}"
|
||||||
{#if $application.repository.organization !== "new"}
|
on:modifyGithubAppConfig="{modifyGithubAppConfig}"
|
||||||
<Branches loading="{loading.branches}" branches="{branches}" />
|
/>
|
||||||
{/if}
|
{#if $application.repository.organization}
|
||||||
|
<Branches loading="{loading.branches}" branches="{branches}" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if $application.repository.branch}
|
{#if $application.repository.branch}
|
||||||
<Tabs />
|
<Tabs />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,36 +1,44 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import { isActive } from "@roxi/routify";
|
import { application, githubRepositories, activePage } from "@store";
|
||||||
import { application } from "@store";
|
import Select from "svelte-select";
|
||||||
export let repositories;
|
function handleSelect(event) {
|
||||||
|
$application.build.pack = 'static'
|
||||||
|
$application.repository.id = parseInt(event.detail.value, 10);
|
||||||
|
dispatch("loadBranches");
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = $githubRepositories.map(repo => ({
|
||||||
|
label: `${repo.owner.login}/${repo.name}`,
|
||||||
|
value: repo.id.toString(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectedValue =
|
||||||
|
!$activePage.new &&
|
||||||
|
`${$application.repository.organization}/${$application.repository.name}`;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const loadBranches = () => dispatch("loadBranches");
|
|
||||||
const modifyGithubAppConfig = () => dispatch("modifyGithubAppConfig");
|
const modifyGithubAppConfig = () => dispatch("modifyGithubAppConfig");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid grid-cols-1">
|
<div class="grid grid-cols-1 pt-4">
|
||||||
{#if repositories.length !== 0}
|
{#if $githubRepositories.length !== 0}
|
||||||
<label for="repository">Organization / Repository</label>
|
<label for="repository">Organization / Repository</label>
|
||||||
<div class="grid grid-cols-3">
|
<div class="grid grid-cols-3 ">
|
||||||
<!-- svelte-ignore a11y-no-onchange -->
|
<div class="repository-select-search col-span-2">
|
||||||
<select
|
<Select
|
||||||
id="repository"
|
isFocused="true"
|
||||||
class:cursor-not-allowed="{!$isActive('/application/new')}"
|
containerClasses="w-full border-none bg-transparent"
|
||||||
class="col-span-2"
|
on:select="{handleSelect}"
|
||||||
bind:value="{$application.repository.id}"
|
selectedValue="{selectedValue}"
|
||||||
on:change="{loadBranches}"
|
isClearable="{false}"
|
||||||
disabled="{!$isActive('/application/new')}"
|
items="{items}"
|
||||||
>
|
showIndicator="{$activePage.new}"
|
||||||
<option selected disabled>Select a repository</option>
|
noOptionsMessage="No Repositories found"
|
||||||
{#each repositories as repo}
|
placeholder="Select a Repository"
|
||||||
<option value="{repo.id}" class="font-medium">
|
isDisabled="{!$activePage.new}"
|
||||||
{repo.owner.login}
|
/>
|
||||||
/
|
</div>
|
||||||
{repo.name}
|
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
|
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
|
||||||
on:click="{modifyGithubAppConfig}">Configure on Github</button
|
on:click="{modifyGithubAppConfig}">Configure on Github</button
|
||||||
@@ -38,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
|
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white py-2"
|
||||||
on:click="{modifyGithubAppConfig}">Add repositories on Github</button
|
on:click="{modifyGithubAppConfig}">Add repositories on Github</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,44 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { redirect, isActive } from "@roxi/routify";
|
import { redirect } from "@roxi/routify";
|
||||||
import { application, fetch, deployments } from "@store";
|
|
||||||
import General from "./ActiveTab/General.svelte";
|
|
||||||
import BuildStep from "./ActiveTab/BuildStep.svelte";
|
|
||||||
import Secrets from "./ActiveTab/Secrets.svelte";
|
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
import { toast } from "@zerodevx/svelte-toast";
|
||||||
|
import templates from "../../../utils/templates";
|
||||||
|
import { application, fetch, deployments, activePage } from "@store";
|
||||||
|
import General from "./ActiveTab/General.svelte";
|
||||||
|
import Secrets from "./ActiveTab/Secrets.svelte";
|
||||||
|
import Loading from "../../Loading.svelte";
|
||||||
|
|
||||||
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 {
|
|
||||||
$deployments.applications.deployed.filter(d => {
|
|
||||||
const conf = d?.Spec?.Labels.application;
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let activeTab = {
|
let activeTab = {
|
||||||
general: true,
|
general: true,
|
||||||
buildStep: false,
|
buildStep: false,
|
||||||
@@ -54,44 +23,131 @@
|
|||||||
activeTab[tab] = true;
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="block text-center py-4">
|
{#await load()}
|
||||||
<nav
|
<Loading github githubLoadingText="Scanning repository..." />
|
||||||
class="flex space-x-4 justify-center font-bold text-md text-white"
|
{:then}
|
||||||
aria-label="Tabs"
|
<div class="block text-center py-8">
|
||||||
>
|
<nav
|
||||||
<div
|
class="flex space-x-4 justify-center font-bold text-md text-white"
|
||||||
on:click="{() => activateTab('general')}"
|
aria-label="Tabs"
|
||||||
class:text-green-500="{activeTab.general}"
|
|
||||||
class="px-3 py-2 cursor-pointer hover:text-green-500"
|
|
||||||
>
|
>
|
||||||
General
|
<div
|
||||||
</div>
|
on:click="{() => activateTab('general')}"
|
||||||
<div
|
class:text-green-500="{activeTab.general}"
|
||||||
on:click="{() => activateTab('buildStep')}"
|
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
|
||||||
class:text-green-500="{activeTab.buildStep}"
|
>
|
||||||
class="px-3 py-2 cursor-pointer hover:text-green-500"
|
General
|
||||||
>
|
</div>
|
||||||
Build Step
|
<div
|
||||||
</div>
|
on:click="{() => activateTab('secrets')}"
|
||||||
<div
|
class:text-green-500="{activeTab.secrets}"
|
||||||
on:click="{() => activateTab('secrets')}"
|
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
|
||||||
class:text-green-500="{activeTab.secrets}"
|
>
|
||||||
class="px-3 py-2 cursor-pointer hover:text-green-500"
|
Secrets
|
||||||
>
|
</div>
|
||||||
Secrets
|
</nav>
|
||||||
</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>
|
||||||
</div>
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="h-full">
|
||||||
|
{#if activeTab.general}
|
||||||
|
<General />
|
||||||
|
{:else if activeTab.secrets}
|
||||||
|
<Secrets />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
195
src/components/Application/Navbar.svelte
Normal file
195
src/components/Application/Navbar.svelte
Normal 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>
|
||||||
@@ -3,6 +3,10 @@
|
|||||||
import { isActive, redirect } from "@roxi/routify/runtime";
|
import { isActive, redirect } from "@roxi/routify/runtime";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
import { toast } from "@zerodevx/svelte-toast";
|
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 type;
|
||||||
let defaultDatabaseName;
|
let defaultDatabaseName;
|
||||||
@@ -15,7 +19,7 @@
|
|||||||
defaultDatabaseName,
|
defaultDatabaseName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
$dbInprogress = true
|
$dbInprogress = true;
|
||||||
toast.push("Database deployment queued.");
|
toast.push("Database deployment queued.");
|
||||||
$redirect(`/dashboard/databases`);
|
$redirect(`/dashboard/databases`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -30,48 +34,65 @@
|
|||||||
>
|
>
|
||||||
{#if $isActive("/database/new")}
|
{#if $isActive("/database/new")}
|
||||||
<div class="flex justify-center space-x-4 font-bold pb-6">
|
<div class="flex justify-center space-x-4 font-bold pb-6">
|
||||||
<button
|
<div
|
||||||
class="button bg-gray-500 p-2 text-white hover:bg-green-600 cursor-pointer w-32"
|
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')}"
|
on:click="{() => (type = 'mongodb')}"
|
||||||
class:bg-green-600="{type === 'mongodb'}"
|
|
||||||
>
|
>
|
||||||
MongoDB
|
<div class="flex items-center justify-center my-2">
|
||||||
</button>
|
<MongoDb customClass="w-6" />
|
||||||
<button
|
</div>
|
||||||
class="button bg-gray-500 p-2 text-white hover:bg-blue-600 cursor-pointer w-32"
|
<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')}"
|
on:click="{() => (type = 'postgresql')}"
|
||||||
class:bg-blue-600="{type === 'postgresql'}"
|
|
||||||
>
|
>
|
||||||
PostgreSQL
|
<div class="flex items-center justify-center my-2">
|
||||||
</button>
|
<Postgresql customClass="w-12" />
|
||||||
<button
|
</div>
|
||||||
class="button bg-gray-500 p-2 text-white hover:bg-orange-600 cursor-pointer w-32"
|
<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')}"
|
on:click="{() => (type = 'mysql')}"
|
||||||
class:bg-orange-600="{type === 'mysql'}"
|
|
||||||
>
|
>
|
||||||
MySQL
|
<div class="flex items-center justify-center">
|
||||||
</button>
|
<Mysql customClass="w-10" />
|
||||||
<button
|
</div>
|
||||||
class="button bg-gray-500 p-2 text-white hover:bg-red-600 cursor-pointer w-32"
|
<div class="text-white">MySQL</div>
|
||||||
on:click="{() => (type = 'couchdb')}"
|
</div>
|
||||||
class:bg-red-600="{type === 'couchdb'}"
|
|
||||||
>
|
<!-- <button
|
||||||
Couchdb
|
class="button bg-gray-500 p-2 text-white hover:bg-yellow-500 cursor-pointer w-32"
|
||||||
</button>
|
on:click="{() => (type = 'clickhouse')}"
|
||||||
|
class:bg-yellow-500="{type === 'clickhouse'}"
|
||||||
|
>
|
||||||
|
Clickhouse
|
||||||
|
</button> -->
|
||||||
</div>
|
</div>
|
||||||
{#if type}
|
{#if type}
|
||||||
<div>
|
<div class="flex justify-center space-x-4 items-center">
|
||||||
<div
|
<label for="defaultDB">Default database</label>
|
||||||
class="grid grid-rows-1 justify-center items-center text-center pb-5"
|
<input
|
||||||
>
|
id="defaultDB"
|
||||||
<label for="defaultDB">Default database</label>
|
class="w-64"
|
||||||
<input
|
placeholder="random"
|
||||||
id="defaultDB"
|
bind:value="{defaultDatabaseName}"
|
||||||
class="w-64"
|
/>
|
||||||
placeholder="random"
|
|
||||||
bind:value="{defaultDatabaseName}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
class:bg-green-600="{type === 'mongodb'}"
|
class:bg-green-600="{type === 'mongodb'}"
|
||||||
class:hover:bg-green-500="{type === 'mongodb'}"
|
class:hover:bg-green-500="{type === 'mongodb'}"
|
||||||
@@ -81,6 +102,8 @@
|
|||||||
class:hover:bg-orange-500="{type === 'mysql'}"
|
class:hover:bg-orange-500="{type === 'mysql'}"
|
||||||
class:bg-red-600="{type === 'couchdb'}"
|
class:bg-red-600="{type === 'couchdb'}"
|
||||||
class:hover:bg-red-500="{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"
|
class="button p-2 w-32 text-white"
|
||||||
on:click="{deploy}">Deploy</button
|
on:click="{deploy}">Deploy</button
|
||||||
>
|
>
|
||||||
|
|||||||
9
src/components/Databases/SVGs/Clickhouse.svelte
Normal file
9
src/components/Databases/SVGs/Clickhouse.svelte
Normal 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
|
||||||
|
>
|
||||||
@@ -42,11 +42,38 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
export let github = false;
|
||||||
|
export let githubLoadingText = "Loading GitHub...";
|
||||||
export let fullscreen = true;
|
export let fullscreen = true;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if fullscreen}
|
{#if fullscreen}
|
||||||
<div class="fixed top-0 flex flex-wrap content-center h-full w-full">
|
{#if github}
|
||||||
<span class="loader"></span>
|
<div class="fixed left-0 top-0 flex flex-wrap content-center h-full w-full">
|
||||||
</div>
|
<div class="main flex justify-center items-center">
|
||||||
|
<div class="w-64">
|
||||||
|
<svg
|
||||||
|
class=" w-28 animate-bounce mx-auto"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><path
|
||||||
|
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
<div class="text-xl font-bold text-center">
|
||||||
|
{githubLoadingText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="main fixed left-0 top-0 flex flex-wrap content-center h-full">
|
||||||
|
<span class=" loader"></span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
54
src/components/PasswordField.svelte
Normal file
54
src/components/PasswordField.svelte
Normal 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>
|
||||||
82
src/components/Services/Plausible.svelte
Normal file
82
src/components/Services/Plausible.svelte
Normal 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}
|
||||||
14
src/components/Tooltip/Tooltip.svelte
Normal file
14
src/components/Tooltip/Tooltip.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
export let position = "bottom";
|
||||||
|
export let label;
|
||||||
|
export let size = "fit";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
aria-label="{label}"
|
||||||
|
data-microtip-position="{position}"
|
||||||
|
data-microtip-size="{size}"
|
||||||
|
role="tooltip"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
25
src/components/Tooltip/TooltipInfo.svelte
Normal file
25
src/components/Tooltip/TooltipInfo.svelte
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script>
|
||||||
|
export let position = "top";
|
||||||
|
export let label;
|
||||||
|
export let size = "large";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="absolute px-1 py-1"
|
||||||
|
aria-label="{label}"
|
||||||
|
data-microtip-position="{position}"
|
||||||
|
data-microtip-size="{size}"
|
||||||
|
role="tooltip"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 text-warmGray-600 hover:text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
@@ -15,4 +15,39 @@ body {
|
|||||||
--toastBackground: rgba(41, 37, 36, 0.8);
|
--toastBackground: rgba(41, 37, 36, 0.8);
|
||||||
--toastProgressBackground: transparent;
|
--toastProgressBackground: transparent;
|
||||||
--toastFont: 'Inter';
|
--toastFont: 'Inter';
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-gradient {
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
border-image: linear-gradient(0.25turn, rgba(255, 249, 34), rgba(255, 0, 128), rgba(56, 2, 155, 0));
|
||||||
|
border-image-slice: 1;
|
||||||
|
}
|
||||||
|
.border-gradient-full {
|
||||||
|
border: 4px solid transparent;
|
||||||
|
border-image: linear-gradient(0.25turn, rgba(255, 249, 34), rgba(255, 0, 128), rgba(56, 2, 155, 0));
|
||||||
|
border-image-slice: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[aria-label][role~="tooltip"]::after {
|
||||||
|
background: rgba(41, 37, 36, 0.9);
|
||||||
|
color: white;
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role~="tooltip"][data-microtip-position|="bottom"]::before {
|
||||||
|
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role~="tooltip"][data-microtip-position|="top"]::before {
|
||||||
|
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
|
||||||
|
}
|
||||||
|
[role~="tooltip"][data-microtip-position="right"]::before {
|
||||||
|
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role~="tooltip"][data-microtip-position="left"]::before {
|
||||||
|
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
|
||||||
}
|
}
|
||||||
@@ -2,33 +2,80 @@
|
|||||||
.min-w-4rem {
|
.min-w-4rem {
|
||||||
min-width: 4rem;
|
min-width: 4rem;
|
||||||
}
|
}
|
||||||
.main {
|
|
||||||
width: calc(100% - 4rem);
|
|
||||||
margin-left: 4rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { url, goto, route, isActive, redirect } from "@roxi/routify/runtime";
|
import { goto, route, isChangingPage } from "@roxi/routify/runtime";
|
||||||
import {
|
import { loggedIn, session, fetch, deployments, activePage } from "@store";
|
||||||
loggedIn,
|
|
||||||
session,
|
|
||||||
fetch,
|
|
||||||
deployments,
|
|
||||||
application,
|
|
||||||
initConf,
|
|
||||||
} from "@store";
|
|
||||||
import { toast } from "@zerodevx/svelte-toast";
|
import { toast } from "@zerodevx/svelte-toast";
|
||||||
import packageJson from "../../package.json";
|
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
import compareVersions from "compare-versions";
|
||||||
|
import packageJson from "../../package.json";
|
||||||
|
import Tooltip from "../components/Tooltip/Tooltip.svelte";
|
||||||
|
|
||||||
let upgradeAvailable = false;
|
let upgradeAvailable = false;
|
||||||
let upgradeDisabled = false;
|
let upgradeDisabled = false;
|
||||||
let upgradeDone = false;
|
let upgradeDone = false;
|
||||||
let latest = {};
|
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 () => {
|
onMount(async () => {
|
||||||
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() {
|
async function verifyToken() {
|
||||||
if ($session.token) {
|
if ($session.token) {
|
||||||
try {
|
try {
|
||||||
@@ -74,184 +121,233 @@
|
|||||||
}
|
}
|
||||||
async function checkUpgrade() {
|
async function checkUpgrade() {
|
||||||
latest = await window
|
latest = await window
|
||||||
.fetch(
|
.fetch(`https://get.coollabs.io/version.json`, {
|
||||||
"https://raw.githubusercontent.com/coollabsio/coolify/main/package.json",
|
cache: "no-cache",
|
||||||
{ cache: "no-cache" },
|
})
|
||||||
)
|
|
||||||
.then(r => r.json());
|
.then(r => r.json());
|
||||||
if (
|
|
||||||
latest.version.split(".").join("") >
|
return compareVersions(
|
||||||
packageJson.version.split(".").join("")
|
latest.coolify[branch].version,
|
||||||
) {
|
packageJson.version,
|
||||||
return true;
|
) === 1
|
||||||
}
|
? true
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await verifyToken() then notUsed}
|
{#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"}
|
{#if $route.path !== "/index"}
|
||||||
<nav
|
<nav
|
||||||
class="w-16 bg-warmGray-800 text-white top-0 left-0 fixed min-w-4rem min-h-screen"
|
class="w-16 bg-warmGray-800 text-white top-0 left-0 fixed min-w-4rem min-h-screen"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col w-full h-screen items-center space-y-4 transition-all duration-100"
|
class="flex flex-col w-full h-screen items-center transition-all duration-100"
|
||||||
class:border-green-500="{$isActive('/dashboard/applications')}"
|
class:border-green-500="{$activePage.mainmenu === 'applications'}"
|
||||||
class:border-purple-500="{$isActive('/dashboard/databases')}"
|
class:border-purple-500="{$activePage.mainmenu === 'databases'}"
|
||||||
>
|
>
|
||||||
<img class="w-10 pt-4 pb-4" src="/favicon.png" alt="coolLabs logo" />
|
<img class="w-10 pt-4 pb-4" src="/favicon.png" alt="coolLabs logo" />
|
||||||
<div
|
<Tooltip position="right" label="Applications">
|
||||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 my-4 transition-all duration-100 cursor-pointer"
|
<div
|
||||||
on:click="{() => $goto('/dashboard/applications')}"
|
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 mt-4 transition-all duration-100 cursor-pointer"
|
||||||
class:text-green-500="{$isActive('/dashboard/applications') ||
|
on:click="{() => $goto('/dashboard/applications')}"
|
||||||
$isActive('/application')}"
|
class:text-green-500="{$activePage.mainmenu === 'applications'}"
|
||||||
class:bg-warmGray-700="{$isActive('/dashboard/applications') ||
|
class:bg-warmGray-700="{$activePage.mainmenu === 'applications'}"
|
||||||
$isActive('/application')}"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-8"
|
|
||||||
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"
|
|
||||||
><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect
|
|
||||||
x="9"
|
|
||||||
y="9"
|
|
||||||
width="6"
|
|
||||||
height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line
|
|
||||||
x1="15"
|
|
||||||
y1="1"
|
|
||||||
x2="15"
|
|
||||||
y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line
|
|
||||||
x1="15"
|
|
||||||
y1="20"
|
|
||||||
x2="15"
|
|
||||||
y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line
|
|
||||||
x1="20"
|
|
||||||
y1="14"
|
|
||||||
x2="23"
|
|
||||||
y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line
|
|
||||||
x1="1"
|
|
||||||
y1="14"
|
|
||||||
x2="4"
|
|
||||||
y2="14"></line></svg
|
|
||||||
>
|
>
|
||||||
</div>
|
<svg
|
||||||
<div
|
class="w-8"
|
||||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-purple-500 my-4 transition-all duration-100 cursor-pointer"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
on:click="{() => $goto('/dashboard/databases')}"
|
viewBox="0 0 24 24"
|
||||||
class:text-purple-500="{$isActive('/dashboard/databases') ||
|
fill="none"
|
||||||
$isActive('/database')}"
|
stroke="currentColor"
|
||||||
class:bg-warmGray-700="{$isActive('/dashboard/databases') ||
|
stroke-width="2"
|
||||||
$isActive('/database')}"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-8"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
><rect x="4" y="4" width="16" height="16" rx="2" ry="2"
|
||||||
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
|
></rect><rect x="9" y="9" width="6" height="6"></rect><line
|
||||||
></path>
|
x1="9"
|
||||||
</svg>
|
y1="1"
|
||||||
</div>
|
x2="9"
|
||||||
|
y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line
|
||||||
|
x1="9"
|
||||||
|
y1="20"
|
||||||
|
x2="9"
|
||||||
|
y2="23"></line><line x1="15" y1="20" x2="15" y2="23"
|
||||||
|
></line><line x1="20" y1="9" x2="23" y2="9"></line><line
|
||||||
|
x1="20"
|
||||||
|
y1="14"
|
||||||
|
x2="23"
|
||||||
|
y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line
|
||||||
|
x1="1"
|
||||||
|
y1="14"
|
||||||
|
x2="4"
|
||||||
|
y2="14"></line></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip position="right" label="Databases">
|
||||||
|
<div
|
||||||
|
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="{$activePage.mainmenu === 'databases'}"
|
||||||
|
class:bg-warmGray-700="{$activePage.mainmenu === 'databases'}"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-8"
|
||||||
|
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="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
|
||||||
|
></path>
|
||||||
|
</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>
|
<div class="flex-1"></div>
|
||||||
<button
|
<Tooltip position="right" label="Settings">
|
||||||
title="Settings"
|
<button
|
||||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-yellow-500 my-4 transition-all duration-100 cursor-pointer"
|
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:text-yellow-500="{$activePage.mainmenu === 'settings'}"
|
||||||
class:bg-warmGray-700="{$isActive('/settings')}"
|
class:bg-warmGray-700="{$activePage.mainmenu === 'settings'}"
|
||||||
on:click="{() => $goto('/settings')}"
|
on:click="{() => $goto('/settings')}"
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-8"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
|
class="w-8"
|
||||||
|
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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip position="right" label="Logout">
|
||||||
|
<button
|
||||||
|
class="p-2 hover:bg-warmGray-700 rounded hover:text-red-500 my-4 transition-all duration-100 cursor-pointer"
|
||||||
|
on:click="{logout}"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-7"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"
|
||||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
></path><polyline points="16 17 21 12 16 7"></polyline><line
|
||||||
></path>
|
x1="21"
|
||||||
<path
|
y1="12"
|
||||||
stroke-linecap="round"
|
x2="9"
|
||||||
stroke-linejoin="round"
|
y2="12"></line></svg
|
||||||
stroke-width="2"
|
>
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
</button>
|
||||||
</svg>
|
</Tooltip>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-red-500 my-4 transition-all duration-100 cursor-pointer"
|
|
||||||
on:click="{logout}"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-7"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline
|
|
||||||
points="16 17 21 12 16 7"></polyline><line
|
|
||||||
x1="21"
|
|
||||||
y1="12"
|
|
||||||
x2="9"
|
|
||||||
y2="12"></line></svg
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer text-xs font-bold text-warmGray-400 py-2 hover:bg-warmGray-700 w-full text-center"
|
class="cursor-pointer text-xs font-bold text-warmGray-400 py-2 hover:bg-warmGray-700 w-full text-center"
|
||||||
>
|
>
|
||||||
v{packageJson.version}
|
{packageJson.version}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{/if}
|
{/if}
|
||||||
{#if upgradeAvailable}
|
{#if upgradeAvailable}
|
||||||
<footer
|
<footer
|
||||||
class="absolute top-0 right-0 p-2 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 class="flex items-center">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
{#if !upgradeDisabled}
|
{#if !upgradeDisabled}
|
||||||
<button
|
<button
|
||||||
class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 font-bold text-xs rounded px-2 py-2"
|
class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-xs font-bold rounded px-2 py-2"
|
||||||
disabled="{upgradeDisabled}"
|
disabled="{upgradeDisabled}"
|
||||||
on:click="{upgrade}"
|
on:click="{upgrade}"
|
||||||
>New version available. <br>Click here to upgrade!</button
|
>New version available, <br />click here to upgrade!</button
|
||||||
>
|
>
|
||||||
{:else if upgradeDone}
|
{:else if upgradeDone}
|
||||||
<button
|
<button
|
||||||
use:reloadInAMin
|
use:reloadInAMin
|
||||||
class="font-bold text-xs rounded px-2 cursor-not-allowed"
|
class="font-bold text-xs rounded px-2 cursor-not-allowed"
|
||||||
disabled="{upgradeDisabled}"
|
disabled="{upgradeDisabled}"
|
||||||
>Upgrade done. 🎉 Automatically reloading in 30s.</button
|
>Upgrade done. 🎉 Automatically reloading in 30s.</button
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="opacity-50 tracking-tight font-bold text-xs rounded px-2 cursor-not-allowed"
|
class="opacity-50 tracking-tight font-bold text-xs rounded px-2 cursor-not-allowed"
|
||||||
disabled="{upgradeDisabled}">Upgrading. It could take a while, please wait...</button
|
disabled="{upgradeDisabled}"
|
||||||
>
|
>Upgrading. It could take a while, please wait...</button
|
||||||
{/if}
|
>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
{/if}
|
{/if}
|
||||||
<main class:main={$route.path !== "/index"}>
|
<main class:main="{$route.path !== '/index'}">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
{:catch test}
|
{:catch test}
|
||||||
{$goto("/index")}
|
{$goto("/index")}
|
||||||
|
|||||||
@@ -1,62 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { redirect, isActive } from "@roxi/routify";
|
|
||||||
import { application, fetch, initialApplication, initConf } from "@store";
|
|
||||||
import { toast } from "@zerodevx/svelte-toast";
|
|
||||||
import Configuration from "../../../../../components/Application/Configuration/Configuration.svelte";
|
import Configuration from "../../../../../components/Application/Configuration/Configuration.svelte";
|
||||||
import Loading from "../../../../../components/Loading.svelte";
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
$application = { ...config };
|
|
||||||
$initConf = JSON.parse(JSON.stringify($application));
|
|
||||||
} catch (error) {
|
|
||||||
toast.push("Configuration not found.");
|
|
||||||
$redirect("/dashboard/applications");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</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"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
class="text-green-500 hover:underline cursor-pointer px-2"
|
|
||||||
href="{'https://' +
|
|
||||||
$application.publish.domain +
|
|
||||||
$application.publish.path}">{$application.publish.domain}</a
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
class="icon"
|
|
||||||
href="{`https://github.com/${$application.repository.organization}/${$application.repository.name}`}"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
><path
|
|
||||||
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
|
||||||
></path></svg
|
|
||||||
></a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Configuration />
|
<Configuration />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { params } from "@roxi/routify";
|
import { params, redirect } from "@roxi/routify";
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
import { fetch } from "@store";
|
import { fetch } from "@store";
|
||||||
@@ -15,13 +15,18 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadLogs() {
|
async function loadLogs() {
|
||||||
const { events, progress } = await $fetch(
|
try {
|
||||||
|
const { events, progress } = await $fetch(
|
||||||
`/api/v1/application/deploy/logs/${$params.deployId}`,
|
`/api/v1/application/deploy/logs/${$params.deployId}`,
|
||||||
);
|
);
|
||||||
logs = [...events];
|
logs = [...events];
|
||||||
if (progress === "done" || progress === "failed") {
|
if (progress === "done" || progress === "failed") {
|
||||||
clearInterval(loadLogsInterval);
|
clearInterval(loadLogsInterval);
|
||||||
}
|
}
|
||||||
|
} catch(error) {
|
||||||
|
$redirect('/dashboard')
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
clearInterval(loadLogsInterval);
|
clearInterval(loadLogsInterval);
|
||||||
@@ -38,12 +43,12 @@
|
|||||||
<Loading />
|
<Loading />
|
||||||
{:then}
|
{:then}
|
||||||
<div
|
<div
|
||||||
class="text-center space-y-2 max-w-7xl mx-auto px-6"
|
class="text-center px-6"
|
||||||
in:fade="{{ duration: 100 }}"
|
in:fade="{{ duration: 100 }}"
|
||||||
>
|
>
|
||||||
<div class="max-w-4xl mx-auto" in:fade="{{ duration: 100 }}">
|
<div in:fade="{{ duration: 100 }}">
|
||||||
<pre
|
<pre
|
||||||
class="text-left font-mono text-xs font-medium tracking-tighter rounded-lg bg-warmGray-800 p-4 whitespace-pre-wrap">
|
class="leading-4 text-left text-sm font-semibold tracking-tighter rounded-lg bg-black p-6 whitespace-pre-wrap">
|
||||||
{#if logs.length > 0}
|
{#if logs.length > 0}
|
||||||
{#each logs as log}
|
{#each logs as log}
|
||||||
{log + '\n'}
|
{log + '\n'}
|
||||||
|
|||||||
@@ -59,17 +59,17 @@
|
|||||||
<Loading />
|
<Loading />
|
||||||
{:then}
|
{:then}
|
||||||
<div
|
<div
|
||||||
class="text-center space-y-2 max-w-7xl mx-auto px-6"
|
class="text-center px-6"
|
||||||
in:fade="{{ duration: 100 }}"
|
in:fade="{{ duration: 100 }}"
|
||||||
>
|
>
|
||||||
<div class="flex pt-2 space-x-4 w-full">
|
<div class="flex pt-2 space-x-4 w-full">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="font-bold text-left pb-2 text-xl">Application logs</div>
|
<div class="font-bold text-left pb-2 text-xl">Application logs</div>
|
||||||
{#if logs.length === 0}
|
{#if logs.length === 0}
|
||||||
<div class="text-xs">Waiting for the logs...</div>
|
<div class="text-xs font-semibold tracking-tighter">Waiting for the logs...</div>
|
||||||
{:else}
|
{:else}
|
||||||
<pre
|
<pre
|
||||||
class="text-left font-mono text-xs font-medium rounded bg-warmGray-800 text-white p-4 whitespace-pre-wrap w-full">
|
class="leading-4 text-left text-sm font-semibold tracking-tighter rounded-lg bg-black p-6 whitespace-pre-wrap w-full">
|
||||||
{#each logs as log}
|
{#each logs as log}
|
||||||
{log + '\n'}
|
{log + '\n'}
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import { application } from "@store";
|
import { application } from "@store";
|
||||||
import Configuration from "../../../../../components/Application/Configuration/Configuration.svelte";
|
import Configuration from "../../../../../components/Application/Configuration/Configuration.svelte";
|
||||||
</script>
|
</script>
|
||||||
@@ -11,7 +10,7 @@
|
|||||||
Overview of
|
Overview of
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-green-500 hover:underline cursor-pointer px-2"
|
class="hover:underline cursor-pointer px-2"
|
||||||
href="{'https://' +
|
href="{'https://' +
|
||||||
$application.publish.domain +
|
$application.publish.domain +
|
||||||
$application.publish.path}">{$application.publish.domain}</a
|
$application.publish.path}">{$application.publish.domain}</a
|
||||||
|
|||||||
@@ -1,215 +1,75 @@
|
|||||||
<script>
|
<script>
|
||||||
import { params, goto, redirect, isActive } from "@roxi/routify";
|
import { params, redirect } from "@roxi/routify";
|
||||||
import { application, fetch, initialApplication, initConf } from "@store";
|
import {
|
||||||
|
application,
|
||||||
|
fetch,
|
||||||
|
initialApplication,
|
||||||
|
initConf,
|
||||||
|
deployments,
|
||||||
|
activePage,
|
||||||
|
} from "@store";
|
||||||
import { onDestroy } from "svelte";
|
import { onDestroy } from "svelte";
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import Loading from "../../components/Loading.svelte";
|
import Loading from "../../components/Loading.svelte";
|
||||||
import { toast } from "@zerodevx/svelte-toast";
|
import { toast } from "@zerodevx/svelte-toast";
|
||||||
|
import Navbar from "../../components/Application/Navbar.svelte";
|
||||||
|
|
||||||
$application.repository.organization = $params.organization;
|
$application.repository.organization = $params.organization;
|
||||||
$application.repository.name = $params.name;
|
$application.repository.name = $params.name;
|
||||||
$application.repository.branch = $params.branch;
|
$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() {
|
async function loadConfiguration() {
|
||||||
if (!$isActive("/application/new")) {
|
if (!$activePage.new) {
|
||||||
try {
|
if ($deployments.length === 0) {
|
||||||
const config = await $fetch(`/api/v1/config`, {
|
await setConfiguration();
|
||||||
body: {
|
} else {
|
||||||
name: $application.repository.name,
|
const found = $deployments.applications.deployed.find(app => {
|
||||||
organization: $application.repository.organization,
|
const { organization, name, branch } = app.configuration;
|
||||||
branch: $application.repository.branch,
|
if (
|
||||||
},
|
organization === $application.repository.organization &&
|
||||||
|
name === $application.repository.name &&
|
||||||
|
branch === $application.repository.branch
|
||||||
|
) {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$application = { ...config };
|
if (found) {
|
||||||
$initConf = JSON.parse(JSON.stringify($application));
|
$application = { ...found.configuration };
|
||||||
} catch (error) {
|
$initConf = JSON.parse(JSON.stringify($application));
|
||||||
toast.push("Configuration not found.");
|
} else {
|
||||||
$redirect("/dashboard/applications");
|
await setConfiguration();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
$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(() => {
|
onDestroy(() => {
|
||||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function deploy() {
|
|
||||||
try {
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
{#await loadConfiguration()}
|
{#await loadConfiguration()}
|
||||||
<Loading />
|
<Loading />
|
||||||
{:then}
|
{:then}
|
||||||
<nav
|
<Navbar />
|
||||||
class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4"
|
<div class="text-white">
|
||||||
>
|
<slot />
|
||||||
<button
|
</div>
|
||||||
title="Deploy"
|
|
||||||
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>
|
|
||||||
<button
|
|
||||||
title="Delete"
|
|
||||||
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>
|
|
||||||
<div class="border border-warmGray-700 h-8"></div>
|
|
||||||
<button
|
|
||||||
title="Logs"
|
|
||||||
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>
|
|
||||||
<button
|
|
||||||
title="Configuration"
|
|
||||||
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>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="text-white">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@@ -2,12 +2,4 @@
|
|||||||
import Configuration from "../../components/Application/Configuration/Configuration.svelte";
|
import Configuration from "../../components/Application/Configuration/Configuration.svelte";
|
||||||
</script>
|
</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"
|
|
||||||
>
|
|
||||||
New Application
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Configuration />
|
<Configuration />
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { fetch, deployments } from "@store";
|
import { fetch, deployments } from "@store";
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import { goto, isActive } from "@roxi/routify/runtime";
|
|
||||||
import { toast } from "@zerodevx/svelte-toast";
|
import { toast } from "@zerodevx/svelte-toast";
|
||||||
let loadDashboardInterval = null;
|
let loadDashboardInterval = null;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user