mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-18 12:33:06 +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
|
||||
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
|
||||
yarn-error.log
|
||||
api/development/console.log
|
||||
.pnpm-debug.log
|
||||
.pnpm-debug.log
|
||||
.pnpm-store
|
||||
@@ -11,4 +11,4 @@
|
||||
"svelteBracketNewLine": true,
|
||||
"svelteAllowShorthand": true,
|
||||
"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
|
||||
- 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!
|
||||
An open-source, hassle-free, self-hostable Heroku & Netlify alternative.
|
||||
|
||||
# Upcoming features
|
||||
- Backups & monitoring.
|
||||
- User analytics with privacy in mind.
|
||||
- And much more (see [Roadmap](https://github.com/coollabsio/coolify/projects/1)).
|
||||
## Demo
|
||||
|
||||
[Small video](https://cdn.coollabs.io/assets/coolify/video/coolify.webm)
|
||||
|
||||
# FAQ
|
||||
Q: What 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:
|
||||
- Email: Read-only
|
||||
## Installation
|
||||
|
||||
Subscribe to events:
|
||||
- Push -> Check!
|
||||
Installation is automated with the following command:
|
||||
|
||||
### Installation
|
||||
- Clone this repository: `git clone git@github.com:coollabsio/coolify.git`
|
||||
- Set `.env` (see `.env.template`)
|
||||
- Installation: `bash install.sh all`
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"
|
||||
```
|
||||
|
||||
## Updating process
|
||||
### Update everything (proxy+coolify)
|
||||
- `bash install.sh all`
|
||||
|
||||
## Features
|
||||
You can deploy any of the following applications, databases and services easily.
|
||||
|
||||
### Update coolify only
|
||||
- `bash install.sh coolify`
|
||||
(constantly growing lists)
|
||||
|
||||
### Update proxy only
|
||||
- `bash install.sh proxy`
|
||||
### Applications
|
||||
With Github integration
|
||||
|
||||
- Static sites
|
||||
- NodeJS
|
||||
- VueJS
|
||||
- NuxtJS
|
||||
- React/Preact
|
||||
- NextJS
|
||||
- Gatsby
|
||||
- Svelte
|
||||
- PHP
|
||||
- Rust
|
||||
- or any custom dockerfile
|
||||
|
||||
### Databases
|
||||
- MongoDB
|
||||
- MySQL
|
||||
- PostgreSQL
|
||||
- CouchDB
|
||||
|
||||
### Services
|
||||
- [Plausible Analytics](https://plausible.io)
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
# Contact
|
||||
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
|
||||
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
|
||||
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
|
||||
- Discord: [Invitation](https://discord.com/invite/bvS3WhR)
|
||||
|
||||
## Roadmap
|
||||
[See the Roadmap here](https://github.com/coollabsio/coolify/projects/1)
|
||||
|
||||
## License
|
||||
|
||||
# License
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Please see the [LICENSE](/LICENSE) file in our repository for the full text.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = async function (fastify, opts) {
|
||||
// Private routes
|
||||
fastify.register(async function (server) {
|
||||
if (process.env.NODE_ENV === 'production') server.register(require('./plugins/authentication'))
|
||||
server.register(require('./plugins/authentication'))
|
||||
server.register(require('./routes/v1/upgrade'), { prefix: '/upgrade' })
|
||||
server.register(require('./routes/v1/settings'), { prefix: '/settings' })
|
||||
server.register(require('./routes/v1/dashboard'), { prefix: '/dashboard' })
|
||||
@@ -12,6 +12,9 @@ module.exports = async function (fastify, opts) {
|
||||
server.register(require('./routes/v1/application/deploy'), { prefix: '/application/deploy' })
|
||||
server.register(require('./routes/v1/application/deploy/logs'), { prefix: '/application/deploy/logs' })
|
||||
server.register(require('./routes/v1/databases'), { prefix: '/databases' })
|
||||
server.register(require('./routes/v1/services'), { prefix: '/services' })
|
||||
server.register(require('./routes/v1/services/deploy'), { prefix: '/services/deploy' })
|
||||
server.register(require('./routes/v1/server'), { prefix: '/server' })
|
||||
})
|
||||
// Public routes
|
||||
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 Deployment = require('../../../models/Deployment')
|
||||
|
||||
@@ -9,26 +9,20 @@ module.exports = async function (configuration) {
|
||||
|
||||
const execute = packs[configuration.build.pack]
|
||||
if (execute) {
|
||||
await Deployment.findOneAndUpdate(
|
||||
{ repoId: id, branch, deployId, organization, name, domain },
|
||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'inprogress' })
|
||||
await saveAppLog('### Building application.', configuration)
|
||||
await execute(configuration)
|
||||
await saveAppLog('### Building done.', configuration)
|
||||
} else {
|
||||
try {
|
||||
await Deployment.findOneAndUpdate(
|
||||
{ repoId: id, branch, deployId, organization, name, domain },
|
||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'inprogress' })
|
||||
await saveAppLog('### Building application.', configuration)
|
||||
|
||||
await execute(configuration)
|
||||
|
||||
await saveAppLog('### Building done.', configuration)
|
||||
} catch (error) {
|
||||
await Deployment.findOneAndUpdate(
|
||||
{ repoId: id, branch, deployId, organization, name, domain },
|
||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
||||
if (error.stack) throw { error: error.stack, type: 'server' }
|
||||
throw { error, type: 'app' }
|
||||
} catch (error) {
|
||||
// Hmm.
|
||||
}
|
||||
} else {
|
||||
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' }
|
||||
throw new Error('No buildpack found.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,35 @@
|
||||
const { docker } = require('../../docker')
|
||||
const { execShellAsync, delay } = require('../../common')
|
||||
const { execShellAsync } = require('../../common')
|
||||
const Deployment = require('../../../models/Deployment')
|
||||
|
||||
async function purgeOldThings () {
|
||||
try {
|
||||
await docker.engine.pruneImages()
|
||||
await docker.engine.pruneContainers()
|
||||
} catch (error) {
|
||||
throw { error, type: 'server' }
|
||||
async function purgeImagesContainers (configuration, deleteAll = false) {
|
||||
const { name, tag } = configuration.build.container
|
||||
await execShellAsync('docker container prune -f')
|
||||
if (deleteAll) {
|
||||
const IDsToDelete = (await execShellAsync(`docker images ls --filter=reference='${name}' --format '{{json .ID }}'`)).trim().replace(/"/g, '').split('\n')
|
||||
if (IDsToDelete.length > 0) await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`)
|
||||
} else {
|
||||
const IDsToDelete = (await execShellAsync(`docker images ls --filter=reference='${name}' --filter=before='${name}:${tag}' --format '{{json .ID }}'`)).trim().replace(/"/g, '').split('\n')
|
||||
if (IDsToDelete.length > 1) await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`)
|
||||
}
|
||||
await execShellAsync('docker image prune -f')
|
||||
}
|
||||
|
||||
async function cleanup (configuration) {
|
||||
const { id } = configuration.repository
|
||||
const deployId = configuration.general.deployId
|
||||
try {
|
||||
// Cleanup stucked deployments.
|
||||
const deployments = await Deployment.find({ repoId: id, deployId: { $ne: deployId }, progress: { $in: ['queued', 'inprogress'] } })
|
||||
for (const deployment of deployments) {
|
||||
await Deployment.findByIdAndUpdate(deployment._id, { $set: { progress: 'failed' } })
|
||||
}
|
||||
} catch (error) {
|
||||
throw { error, type: 'server' }
|
||||
}
|
||||
async function cleanupStuckedDeploymentsInDB () {
|
||||
// Cleanup stucked deployments.
|
||||
await Deployment.updateMany(
|
||||
{ progress: { $in: ['queued', 'inprogress'] } },
|
||||
{ progress: 'failed' }
|
||||
)
|
||||
}
|
||||
|
||||
async function deleteSameDeployments (configuration) {
|
||||
try {
|
||||
await (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application').map(async s => {
|
||||
const running = JSON.parse(s.Spec.Labels.configuration)
|
||||
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
|
||||
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
throw { error, type: 'server' }
|
||||
}
|
||||
await (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application').map(async s => {
|
||||
const running = JSON.parse(s.Spec.Labels.configuration)
|
||||
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
|
||||
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { cleanup, deleteSameDeployments, purgeOldThings }
|
||||
module.exports = { cleanupStuckedDeploymentsInDB, deleteSameDeployments, purgeImagesContainers }
|
||||
|
||||
@@ -1,47 +1,54 @@
|
||||
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
|
||||
const cuid = require('cuid')
|
||||
const { execShellAsync } = require('../common')
|
||||
const crypto = require('crypto')
|
||||
const { docker } = require('../docker')
|
||||
const { execShellAsync, baseServiceConfiguration } = require('../common')
|
||||
|
||||
function getUniq () {
|
||||
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 })
|
||||
}
|
||||
|
||||
function setDefaultConfiguration (configuration) {
|
||||
try {
|
||||
const nickname = getUniq()
|
||||
const deployId = cuid()
|
||||
const nickname = getUniq()
|
||||
const deployId = cuid()
|
||||
|
||||
const shaBase = JSON.stringify({ repository: configuration.repository })
|
||||
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex')
|
||||
const shaBase = JSON.stringify({ repository: configuration.repository })
|
||||
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex')
|
||||
|
||||
configuration.build.container.name = sha256.slice(0, 15)
|
||||
configuration.build.container.name = sha256.slice(0, 15)
|
||||
|
||||
configuration.general.nickname = nickname
|
||||
configuration.general.deployId = deployId
|
||||
configuration.general.workdir = `/tmp/${deployId}`
|
||||
configuration.general.nickname = nickname
|
||||
configuration.general.deployId = deployId
|
||||
configuration.general.workdir = `/tmp/${deployId}`
|
||||
|
||||
if (!configuration.publish.path) configuration.publish.path = '/'
|
||||
if (!configuration.publish.port) configuration.publish.port = configuration.build.pack === 'static' ? 80 : 3000
|
||||
|
||||
if (configuration.build.pack === 'static') {
|
||||
if (!configuration.build.command.installation) configuration.build.command.installation = 'yarn install'
|
||||
if (!configuration.build.directory) configuration.build.directory = '/'
|
||||
if (!configuration.publish.path) configuration.publish.path = '/'
|
||||
if (!configuration.publish.port) {
|
||||
if (configuration.build.pack === 'nodejs' && configuration.build.pack === 'vuejs' && configuration.build.pack === 'nuxtjs' && configuration.build.pack === 'rust' && configuration.build.pack === 'nextjs') {
|
||||
configuration.publish.port = 3000
|
||||
} else {
|
||||
configuration.publish.port = 80
|
||||
}
|
||||
|
||||
if (configuration.build.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.
|
||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||
const found = services.find(s => {
|
||||
const config = JSON.parse(s.Spec.Labels.configuration)
|
||||
if (config.repository.id === configuration.repository.id && config.repository.branch === configuration.repository.branch) {
|
||||
@@ -51,12 +58,56 @@ async function updateServiceLabels (configuration, services) {
|
||||
})
|
||||
if (found) {
|
||||
const { ID } = found
|
||||
try {
|
||||
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration }
|
||||
execShellAsync(`docker service update --label-add configuration='${JSON.stringify(Labels)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${configuration.build.container.tag}' ${ID}`)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration }
|
||||
await execShellAsync(`docker service update --label-add configuration='${JSON.stringify(Labels)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${configuration.build.container.tag}' ${ID}`)
|
||||
}
|
||||
}
|
||||
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
|
||||
module.exports = async function (configuration) {
|
||||
const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby']
|
||||
try {
|
||||
// TODO: Do it better.
|
||||
await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules')
|
||||
await fs.writeFile(
|
||||
`${configuration.general.workdir}/nginx.conf`,
|
||||
`user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
access_log off;
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/index.html $uri/ /index.html =404;
|
||||
}
|
||||
|
||||
error_page 404 /50x.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
`
|
||||
)
|
||||
// TODO: Write full .dockerignore for all deployments!!
|
||||
if (configuration.build.pack === 'php') {
|
||||
await fs.writeFile(`${configuration.general.workdir}/.htaccess`, `
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.+)$ index.php [QSA,L]
|
||||
`)
|
||||
}
|
||||
// await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules')
|
||||
if (staticDeployments.includes(configuration.build.pack)) {
|
||||
await fs.writeFile(
|
||||
`${configuration.general.workdir}/nginx.conf`,
|
||||
`user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
access_log off;
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/index.html $uri/ /index.html =404;
|
||||
}
|
||||
|
||||
error_page 404 /50x.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
throw { error, type: 'server' }
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,76 @@
|
||||
const yaml = require('js-yaml')
|
||||
const fs = require('fs').promises
|
||||
const { execShellAsync } = require('../../common')
|
||||
const { docker } = require('../../docker')
|
||||
const { saveAppLog } = require('../../logging')
|
||||
const { deleteSameDeployments } = require('../cleanup')
|
||||
const fs = require('fs').promises
|
||||
|
||||
module.exports = async function (configuration, configChanged, imageChanged) {
|
||||
try {
|
||||
const generateEnvs = {}
|
||||
for (const secret of configuration.publish.secrets) {
|
||||
generateEnvs[secret.name] = secret.value
|
||||
}
|
||||
const containerName = configuration.build.container.name
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[containerName]: {
|
||||
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvs,
|
||||
deploy: {
|
||||
replicas: 1,
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 1,
|
||||
window: '120s'
|
||||
},
|
||||
update_config: {
|
||||
parallelism: 1,
|
||||
delay: '10s',
|
||||
order: 'start-first'
|
||||
},
|
||||
rollback_config: {
|
||||
parallelism: 1,
|
||||
delay: '10s',
|
||||
order: 'start-first'
|
||||
},
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=application',
|
||||
'configuration=' + JSON.stringify(configuration),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' +
|
||||
module.exports = async function (configuration, imageChanged) {
|
||||
const generateEnvs = {}
|
||||
for (const secret of configuration.publish.secrets) {
|
||||
generateEnvs[secret.name] = secret.value
|
||||
}
|
||||
const containerName = configuration.build.container.name
|
||||
|
||||
// Only save SHA256 of it in the configuration label
|
||||
const baseServiceConfiguration = configuration.baseServiceConfiguration
|
||||
delete configuration.baseServiceConfiguration
|
||||
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[containerName]: {
|
||||
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvs,
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=application',
|
||||
'configuration=' + JSON.stringify(configuration),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' +
|
||||
configuration.build.container.name +
|
||||
`.loadbalancer.server.port=${configuration.publish.port}`,
|
||||
'traefik.http.routers.' +
|
||||
'traefik.http.routers.' +
|
||||
configuration.build.container.name +
|
||||
'.entrypoints=websecure',
|
||||
'traefik.http.routers.' +
|
||||
'traefik.http.routers.' +
|
||||
configuration.build.container.name +
|
||||
'.rule=Host(`' +
|
||||
configuration.publish.domain +
|
||||
'`) && PathPrefix(`' +
|
||||
configuration.publish.path +
|
||||
'`)',
|
||||
'traefik.http.routers.' +
|
||||
'traefik.http.routers.' +
|
||||
configuration.build.container.name +
|
||||
'.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' +
|
||||
'traefik.http.routers.' +
|
||||
configuration.build.container.name +
|
||||
'.middlewares=global-compress'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
await saveAppLog('### Publishing.', configuration)
|
||||
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
|
||||
if (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 axios = require('axios')
|
||||
const { execShellAsync, cleanupTmp } = require('../../common')
|
||||
const { execShellAsync } = require('../../common')
|
||||
|
||||
module.exports = async function (configuration) {
|
||||
const { workdir } = configuration.general
|
||||
const { organization, name, branch } = configuration.repository
|
||||
const github = configuration.github
|
||||
|
||||
const githubPrivateKey = process.env.GITHUB_APP_PRIVATE_KEY.replace(/\\n/g, '\n').replace(/"/g, '')
|
||||
|
||||
const payload = {
|
||||
iat: Math.round(new Date().getTime() / 1000),
|
||||
exp: Math.round(new Date().getTime() / 1000 + 60),
|
||||
iss: parseInt(github.app.id)
|
||||
}
|
||||
|
||||
try {
|
||||
const { workdir } = configuration.general
|
||||
const { organization, name, branch } = configuration.repository
|
||||
const github = configuration.github
|
||||
if (!github.installation.id || !github.app.id) {
|
||||
throw new Error('Github installation ID is invalid.')
|
||||
}
|
||||
const githubPrivateKey = process.env.GITHUB_APP_PRIVATE_KEY.replace(/\\n/g, '\n').replace(/"/g, '')
|
||||
|
||||
const payload = {
|
||||
iat: Math.round(new Date().getTime() / 1000),
|
||||
exp: Math.round(new Date().getTime() / 1000 + 60),
|
||||
iss: parseInt(github.app.id)
|
||||
}
|
||||
|
||||
const jwtToken = jwt.sign(payload, githubPrivateKey, {
|
||||
algorithm: 'RS256'
|
||||
})
|
||||
@@ -29,7 +31,7 @@ module.exports = async function (configuration) {
|
||||
}
|
||||
})
|
||||
await execShellAsync(
|
||||
`mkdir -p ${workdir} && git clone -q -b ${branch} https://x-access-token:${accessToken.data.token}@github.com/${organization}/${name}.git ${workdir}/`
|
||||
`mkdir -p ${workdir} && git clone -q -b ${branch} https://x-access-token:${accessToken.data.token}@github.com/${organization}/${name}.git ${workdir}/`
|
||||
)
|
||||
configuration.build.container.tag = (
|
||||
await execShellAsync(`cd ${configuration.general.workdir}/ && git rev-parse HEAD`)
|
||||
@@ -37,8 +39,6 @@ module.exports = async function (configuration) {
|
||||
.replace('\n', '')
|
||||
.slice(0, 7)
|
||||
} catch (error) {
|
||||
cleanupTmp(workdir)
|
||||
if (error.stack) console.log(error.stack)
|
||||
throw { error, type: 'server' }
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,27 @@
|
||||
const dayjs = require('dayjs')
|
||||
|
||||
const { saveServerLog } = require('../logging')
|
||||
const { cleanupTmp } = require('../common')
|
||||
|
||||
const { saveAppLog } = require('../logging')
|
||||
const copyFiles = require('./deploy/copyFiles')
|
||||
const buildContainer = require('./build/container')
|
||||
const deploy = require('./deploy/deploy')
|
||||
const Deployment = require('../../models/Deployment')
|
||||
const { cleanup, purgeOldThings } = require('./cleanup')
|
||||
const { updateServiceLabels } = require('./configuration')
|
||||
|
||||
async function queueAndBuild (configuration, services, configChanged, imageChanged) {
|
||||
async function queueAndBuild (configuration, imageChanged) {
|
||||
const { id, organization, name, branch } = configuration.repository
|
||||
const { domain } = configuration.publish
|
||||
const { deployId, nickname, workdir } = configuration.general
|
||||
try {
|
||||
await new Deployment({
|
||||
repoId: id, branch, deployId, domain, organization, name, nickname
|
||||
}).save()
|
||||
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration)
|
||||
await copyFiles(configuration)
|
||||
await buildContainer(configuration)
|
||||
await deploy(configuration, configChanged, imageChanged)
|
||||
await Deployment.findOneAndUpdate(
|
||||
{ repoId: id, branch, deployId, organization, name, domain },
|
||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' })
|
||||
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 })
|
||||
}
|
||||
}
|
||||
const { deployId, nickname } = configuration.general
|
||||
await new Deployment({
|
||||
repoId: id, branch, deployId, domain, organization, name, nickname
|
||||
}).save()
|
||||
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration)
|
||||
await copyFiles(configuration)
|
||||
await buildContainer(configuration)
|
||||
await deploy(configuration, imageChanged)
|
||||
await Deployment.findOneAndUpdate(
|
||||
{ repoId: id, branch, deployId, organization, name, domain },
|
||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' })
|
||||
await updateServiceLabels(configuration)
|
||||
}
|
||||
|
||||
module.exports = { queueAndBuild }
|
||||
|
||||
@@ -6,6 +6,24 @@ const User = require('../models/User')
|
||||
const algorithm = 'aes-256-cbc'
|
||||
const key = process.env.SECRETS_ENCRYPTION_KEY
|
||||
|
||||
const baseServiceConfiguration = {
|
||||
replicas: 1,
|
||||
restart_policy: {
|
||||
condition: 'any',
|
||||
max_attempts: 6
|
||||
},
|
||||
update_config: {
|
||||
parallelism: 1,
|
||||
delay: '10s',
|
||||
order: 'start-first'
|
||||
},
|
||||
rollback_config: {
|
||||
parallelism: 1,
|
||||
delay: '10s',
|
||||
order: 'start-first',
|
||||
failure_action: 'rollback'
|
||||
}
|
||||
}
|
||||
function delay (t) {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(function () {
|
||||
@@ -15,12 +33,16 @@ function delay (t) {
|
||||
}
|
||||
|
||||
async function verifyUserId (authorization) {
|
||||
const token = authorization.split(' ')[1]
|
||||
const verify = jsonwebtoken.verify(token, process.env.JWT_SIGN_KEY)
|
||||
const found = await User.findOne({ uid: verify.jti })
|
||||
if (found) {
|
||||
return true
|
||||
} else {
|
||||
try {
|
||||
const token = authorization.split(' ')[1]
|
||||
const verify = jsonwebtoken.verify(token, process.env.JWT_SIGN_KEY)
|
||||
const found = await User.findOne({ uid: verify.jti })
|
||||
if (found) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -90,5 +112,6 @@ module.exports = {
|
||||
checkImageAvailable,
|
||||
encryptData,
|
||||
decryptData,
|
||||
verifyUserId
|
||||
verifyUserId,
|
||||
baseServiceConfiguration
|
||||
}
|
||||
|
||||
@@ -8,24 +8,21 @@ const docker = {
|
||||
network: process.env.DOCKER_NETWORK
|
||||
}
|
||||
async function streamEvents (stream, configuration) {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.engine.modem.followProgress(stream, onFinished, onProgress)
|
||||
function onFinished (err, res) {
|
||||
if (err) reject(err)
|
||||
resolve(res)
|
||||
}
|
||||
function onProgress (event) {
|
||||
if (event.error) {
|
||||
reject(event.error)
|
||||
return
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.engine.modem.followProgress(stream, onFinished, onProgress)
|
||||
function onFinished (err, res) {
|
||||
if (err) reject(err)
|
||||
resolve(res)
|
||||
}
|
||||
function onProgress (event) {
|
||||
if (event.error) {
|
||||
saveAppLog(event.error, configuration, true)
|
||||
reject(event.error)
|
||||
} else if (event.stream) {
|
||||
saveAppLog(event.stream, configuration)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
throw { error, type: 'app' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { streamEvents, docker }
|
||||
|
||||
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 ServerLog = require('../models/Logs/Server')
|
||||
const dayjs = require('dayjs')
|
||||
const Settings = require('../models/Settings')
|
||||
const { version } = require('../../package.json')
|
||||
|
||||
function generateTimestamp () {
|
||||
return `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} `
|
||||
}
|
||||
const patterns = [
|
||||
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
||||
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
|
||||
].join('|')
|
||||
|
||||
async function saveAppLog (event, configuration, isError) {
|
||||
try {
|
||||
@@ -12,25 +20,12 @@ async function saveAppLog (event, configuration, isError) {
|
||||
const repoId = configuration.repository.id
|
||||
const branch = configuration.repository.branch
|
||||
if (isError) {
|
||||
// console.log(event, config, isError)
|
||||
let clearedEvent = null
|
||||
|
||||
if (event.error) clearedEvent = '[ERROR] ' + generateTimestamp() + event.error.replace(/(\r\n|\n|\r)/gm, '')
|
||||
else if (event) clearedEvent = '[ERROR] ' + generateTimestamp() + event.replace(/(\r\n|\n|\r)/gm, '')
|
||||
|
||||
try {
|
||||
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
const clearedEvent = '[ERROR 😱] ' + generateTimestamp() + event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '')
|
||||
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
|
||||
} else {
|
||||
if (event && event !== '\n') {
|
||||
const clearedEvent = '[INFO] ' + generateTimestamp() + event.replace(/(\r\n|\n|\r)/gm, '')
|
||||
try {
|
||||
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
const clearedEvent = '[INFO] ' + generateTimestamp() + event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '')
|
||||
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -39,16 +34,14 @@ async function saveAppLog (event, configuration, isError) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveServerLog ({ event, configuration, type }) {
|
||||
if (configuration) {
|
||||
const deployId = configuration.general.deployId
|
||||
const repoId = configuration.repository.id
|
||||
const branch = configuration.repository.branch
|
||||
await new ApplicationLog({ repoId, branch, deployId, event: `[SERVER ERROR 😖]: ${event}` }).save()
|
||||
}
|
||||
await new ServerLog({ event, type }).save()
|
||||
}
|
||||
async function saveServerLog (error) {
|
||||
const settings = await Settings.findOne({ applicationName: 'coolify' })
|
||||
const payload = { message: error.message, stack: error.stack, type: error.type || 'spaghetticode', version }
|
||||
|
||||
const found = await ServerLog.find(payload)
|
||||
if (found.length === 0 && error.message) await new ServerLog(payload).save()
|
||||
if (settings && settings.sendErrors && process.env.NODE_ENV === 'production') await axios.post('https://errors.coollabs.io/api/error', payload)
|
||||
}
|
||||
module.exports = {
|
||||
saveAppLog,
|
||||
saveServerLog
|
||||
|
||||
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 logSchema = mongoose.Schema(
|
||||
{
|
||||
version: { type: String, required: true, default: version },
|
||||
type: { type: String, required: true, enum: ['API', 'UPGRADE-P-1', 'UPGRADE-P-2'], default: 'API' },
|
||||
event: { type: String, required: true },
|
||||
seen: { type: Boolean, required: true, default: false }
|
||||
version: { type: String, default: version },
|
||||
type: { type: String, required: true },
|
||||
message: { type: String, required: true },
|
||||
stack: { type: String },
|
||||
seen: { type: Boolean, default: false }
|
||||
},
|
||||
{ timestamps: { createdAt: 'createdAt', updatedAt: false } }
|
||||
)
|
||||
|
||||
@@ -3,7 +3,8 @@ const mongoose = require('mongoose')
|
||||
const settingsSchema = mongoose.Schema(
|
||||
{
|
||||
applicationName: { type: String, required: true, default: 'coolify' },
|
||||
allowRegistration: { type: Boolean, required: true, default: false }
|
||||
allowRegistration: { type: Boolean, required: true, default: false },
|
||||
sendErrors: { type: Boolean, required: true, default: true }
|
||||
},
|
||||
{ timestamps: true }
|
||||
)
|
||||
|
||||
@@ -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 { docker } = require('../../../libs/docker')
|
||||
const { saveServerLog } = require('../../../libs/logging')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
fastify.post('/', async (request, reply) => {
|
||||
if (!await verifyUserId(request.headers.authorization)) {
|
||||
reply.code(500).send({ error: 'Invalid request' })
|
||||
return
|
||||
}
|
||||
const configuration = setDefaultConfiguration(request.body)
|
||||
try {
|
||||
const configuration = setDefaultConfiguration(request.body)
|
||||
|
||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||
let foundDomain = false
|
||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||
let foundDomain = false
|
||||
|
||||
for (const service of services) {
|
||||
const running = JSON.parse(service.Spec.Labels.configuration)
|
||||
if (running) {
|
||||
if (
|
||||
running.publish.domain === configuration.publish.domain &&
|
||||
running.repository.id !== configuration.repository.id
|
||||
) {
|
||||
foundDomain = true
|
||||
for (const service of services) {
|
||||
const running = JSON.parse(service.Spec.Labels.configuration)
|
||||
if (running) {
|
||||
if (
|
||||
running.publish.domain === configuration.publish.domain &&
|
||||
running.repository.id !== configuration.repository.id &&
|
||||
running.publish.path === configuration.publish.path
|
||||
) {
|
||||
foundDomain = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fastify.config.DOMAIN === configuration.publish.domain) foundDomain = true
|
||||
if (foundDomain) {
|
||||
reply.code(500).send({ message: 'Domain already in use.' })
|
||||
return
|
||||
}
|
||||
return { message: 'OK' }
|
||||
} catch (error) {
|
||||
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 ApplicationLog = require('../../../../models/Logs/Application')
|
||||
const { verifyUserId, cleanupTmp } = require('../../../../libs/common')
|
||||
const { purgeImagesContainers } = require('../../../../libs/applications/cleanup')
|
||||
const { queueAndBuild } = require('../../../../libs/applications')
|
||||
const { setDefaultConfiguration } = require('../../../../libs/applications/configuration')
|
||||
const { setDefaultConfiguration, precheckDeployment } = require('../../../../libs/applications/configuration')
|
||||
const { docker } = require('../../../../libs/docker')
|
||||
const { saveServerLog } = require('../../../../libs/logging')
|
||||
const cloneRepository = require('../../../../libs/applications/github/cloneRepository')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
// const postSchema = {
|
||||
// body: {
|
||||
// type: "object",
|
||||
// properties: {
|
||||
// ref: { type: "string" },
|
||||
// repository: {
|
||||
// type: "object",
|
||||
// properties: {
|
||||
// id: { type: "number" },
|
||||
// full_name: { type: "string" },
|
||||
// },
|
||||
// required: ["id", "full_name"],
|
||||
// },
|
||||
// installation: {
|
||||
// type: "object",
|
||||
// properties: {
|
||||
// id: { type: "number" },
|
||||
// },
|
||||
// required: ["id"],
|
||||
// },
|
||||
// },
|
||||
// required: ["ref", "repository", "installation"],
|
||||
// },
|
||||
// };
|
||||
fastify.post('/', async (request, reply) => {
|
||||
if (!await verifyUserId(request.headers.authorization)) {
|
||||
let configuration
|
||||
try {
|
||||
await verifyUserId(request.headers.authorization)
|
||||
} catch (error) {
|
||||
reply.code(500).send({ error: 'Invalid request' })
|
||||
return
|
||||
}
|
||||
|
||||
const configuration = setDefaultConfiguration(request.body)
|
||||
|
||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||
|
||||
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
|
||||
}
|
||||
try {
|
||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||
configuration = setDefaultConfiguration(request.body)
|
||||
if (!configuration) {
|
||||
throw new Error('Whaat?')
|
||||
}
|
||||
}
|
||||
if (foundDomain) {
|
||||
cleanupTmp(configuration.general.workdir)
|
||||
reply.code(500).send({ message: 'Domain already in use.' })
|
||||
return
|
||||
}
|
||||
if (forceUpdate) {
|
||||
imageChanged = false
|
||||
configChanged = false
|
||||
} else {
|
||||
if (foundService && !imageChanged && !configChanged) {
|
||||
await cloneRepository(configuration)
|
||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
|
||||
|
||||
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
|
||||
cleanupTmp(configuration.general.workdir)
|
||||
reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
|
||||
return
|
||||
}
|
||||
|
||||
const alreadyQueued = await Deployment.find({
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain,
|
||||
progress: { $in: ['queued', 'inprogress'] }
|
||||
})
|
||||
|
||||
if (alreadyQueued.length > 0) {
|
||||
reply.code(200).send({ message: 'Already in the queue.' })
|
||||
return
|
||||
}
|
||||
|
||||
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name, deployId: configuration.general.deployId })
|
||||
await queueAndBuild(configuration, imageChanged)
|
||||
} catch (error) {
|
||||
const { id, organization, name, branch } = configuration.repository
|
||||
const { domain } = configuration.publish
|
||||
const { deployId } = configuration.general
|
||||
await Deployment.findOneAndUpdate(
|
||||
{ repoId: id, branch, deployId, organization, name, domain },
|
||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
||||
if (error.name) {
|
||||
if (error.message && error.stack) await saveServerLog(error)
|
||||
if (reply.sent) await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
|
||||
}
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
cleanupTmp(configuration.general.workdir)
|
||||
await purgeImagesContainers(configuration)
|
||||
}
|
||||
|
||||
const alreadyQueued = await Deployment.find({
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain,
|
||||
progress: { $in: ['queued', 'inprogress'] }
|
||||
})
|
||||
|
||||
if (alreadyQueued.length > 0) {
|
||||
reply.code(200).send({ message: 'Already in the queue.' })
|
||||
return
|
||||
}
|
||||
|
||||
queueAndBuild(configuration, services, configChanged, imageChanged)
|
||||
|
||||
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,25 +18,29 @@ module.exports = async function (fastify) {
|
||||
}
|
||||
}
|
||||
fastify.get('/', { schema: getLogSchema }, async (request, reply) => {
|
||||
const { repoId, branch, page } = request.query
|
||||
const onePage = 5
|
||||
const show = Number(page) * onePage || 5
|
||||
const deploy = await Deployment.find({ repoId, branch })
|
||||
.select('-_id -__v -repoId')
|
||||
.sort({ createdAt: 'desc' })
|
||||
.limit(show)
|
||||
try {
|
||||
const { repoId, branch, page } = request.query
|
||||
const onePage = 5
|
||||
const show = Number(page) * onePage || 5
|
||||
const deploy = await Deployment.find({ repoId, branch })
|
||||
.select('-_id -__v -repoId')
|
||||
.sort({ createdAt: 'desc' })
|
||||
.limit(show)
|
||||
|
||||
const finalLogs = deploy.map(d => {
|
||||
const finalLogs = { ...d._doc }
|
||||
const finalLogs = deploy.map(d => {
|
||||
const finalLogs = { ...d._doc }
|
||||
|
||||
const updatedAt = dayjs(d.updatedAt).utc()
|
||||
const updatedAt = dayjs(d.updatedAt).utc()
|
||||
|
||||
finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000
|
||||
finalLogs.since = updatedAt.fromNow()
|
||||
finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000
|
||||
finalLogs.since = updatedAt.fromNow()
|
||||
|
||||
return finalLogs
|
||||
})
|
||||
return finalLogs
|
||||
})
|
||||
return finalLogs
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
}
|
||||
})
|
||||
|
||||
fastify.get('/:deployId', async (request, reply) => {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
const { docker } = require('../../../libs/docker')
|
||||
const { saveServerLog } = require('../../../libs/logging')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
fastify.get('/', async (request, reply) => {
|
||||
const { name } = request.query
|
||||
const service = await docker.engine.getService(`${name}_${name}`)
|
||||
const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a)
|
||||
return { logs }
|
||||
try {
|
||||
const { name } = request.query
|
||||
const service = await docker.engine.getService(`${name}_${name}`)
|
||||
const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a)
|
||||
return { logs }
|
||||
} catch (error) {
|
||||
await saveServerLog(error)
|
||||
throw new Error(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { docker } = require('../../../libs/docker')
|
||||
const { execShellAsync } = require('../../../libs/common')
|
||||
const { execShellAsync, delay } = require('../../../libs/common')
|
||||
const ApplicationLog = require('../../../models/Logs/Application')
|
||||
const Deployment = require('../../../models/Deployment')
|
||||
const { purgeImagesContainers } = require('../../../libs/applications/cleanup')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
fastify.post('/', async (request, reply) => {
|
||||
@@ -25,6 +26,8 @@ module.exports = async function (fastify) {
|
||||
}
|
||||
await execShellAsync(`docker stack rm ${found.build.container.name}`)
|
||||
reply.code(200).send({ organization, name, branch })
|
||||
await delay(10000)
|
||||
await purgeImagesContainers(found, true)
|
||||
} else {
|
||||
reply.code(500).send({ message: 'Nothing to do.' })
|
||||
}
|
||||
|
||||
@@ -1,60 +1,6 @@
|
||||
const { docker } = require('../../libs/docker')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
// const getConfig = {
|
||||
// querystring: {
|
||||
// type: 'object',
|
||||
// properties: {
|
||||
// repoId: { type: 'number' },
|
||||
// branch: { type: 'string' }
|
||||
// },
|
||||
// required: ['repoId', 'branch']
|
||||
// }
|
||||
// }
|
||||
|
||||
// const saveConfig = {
|
||||
// body: {
|
||||
// type: 'object',
|
||||
// properties: {
|
||||
// build: {
|
||||
// type: 'object',
|
||||
// properties: {
|
||||
// baseDir: { type: 'string' },
|
||||
// installCmd: { type: 'string' },
|
||||
// buildCmd: { type: 'string' }
|
||||
// },
|
||||
// required: ['baseDir', 'installCmd', 'buildCmd']
|
||||
// },
|
||||
// publish: {
|
||||
// type: 'object',
|
||||
// properties: {
|
||||
// publishDir: { type: 'string' },
|
||||
// domain: { type: 'string' },
|
||||
// pathPrefix: { type: 'string' },
|
||||
// port: { type: 'number' }
|
||||
// },
|
||||
// required: ['publishDir', 'domain', 'pathPrefix', 'port']
|
||||
// },
|
||||
// previewDeploy: { type: 'boolean' },
|
||||
// branch: { type: 'string' },
|
||||
// repoId: { type: 'number' },
|
||||
// buildPack: { type: 'string' },
|
||||
// fullName: { type: 'string' },
|
||||
// installationId: { type: 'number' }
|
||||
// },
|
||||
// required: ['build', 'publish', 'previewDeploy', 'branch', 'repoId', 'buildPack', 'fullName', 'installationId']
|
||||
// }
|
||||
// }
|
||||
|
||||
// fastify.get("/all", async (request, reply) => {
|
||||
// return await Config.find().select("-_id -__v");
|
||||
// });
|
||||
|
||||
// fastify.get("/", { schema: getConfig }, async (request, reply) => {
|
||||
// const { repoId, branch } = request.query;
|
||||
// return await Config.findOne({ repoId, branch }).select("-_id -__v");
|
||||
// });
|
||||
|
||||
fastify.post('/', async (request, reply) => {
|
||||
const { name, organization, branch } = request.body
|
||||
const services = await docker.engine.listServices()
|
||||
@@ -79,25 +25,4 @@ module.exports = async function (fastify) {
|
||||
reply.code(500).send({ message: 'No configuration found.' })
|
||||
}
|
||||
})
|
||||
|
||||
// fastify.delete("/", async (request, reply) => {
|
||||
// const { repoId, branch } = request.body;
|
||||
|
||||
// const deploys = await Deployment.find({ repoId, branch })
|
||||
// const found = deploys.filter(d => d.progress !== 'done' && d.progress !== 'failed')
|
||||
// if (found.length > 0) {
|
||||
// throw new Error('Deployment inprogress, cannot delete now.');
|
||||
// }
|
||||
|
||||
// const config = await Config.findOneAndDelete({ repoId, branch })
|
||||
// for (const deploy of deploys) {
|
||||
// await ApplicationLog.findOneAndRemove({ deployId: deploy.deployId });
|
||||
// }
|
||||
// const secrets = await Secret.find({ repoId, branch });
|
||||
// for (const secret of secrets) {
|
||||
// await Secret.findByIdAndRemove(secret._id);
|
||||
// }
|
||||
// await execShellAsync(`docker stack rm ${config.containerName}`);
|
||||
// return { message: 'Deleted application and related configurations.' };
|
||||
// });
|
||||
}
|
||||
|
||||
@@ -1,54 +1,60 @@
|
||||
const { docker } = require('../../../libs/docker')
|
||||
const Deployment = require('../../../models/Deployment')
|
||||
const ServerLog = require('../../../models/Logs/Server')
|
||||
const { saveServerLog } = require('../../../libs/logging')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
fastify.get('/', async (request, reply) => {
|
||||
const latestDeployments = await Deployment.aggregate([
|
||||
{
|
||||
$sort: { createdAt: -1 }
|
||||
},
|
||||
{
|
||||
$group:
|
||||
{
|
||||
_id: {
|
||||
repoId: '$repoId',
|
||||
branch: '$branch'
|
||||
},
|
||||
createdAt: { $last: '$createdAt' },
|
||||
progress: { $first: '$progress' }
|
||||
try {
|
||||
const serverLogs = await ServerLog.find()
|
||||
const dockerServices = await docker.engine.listServices()
|
||||
let applications = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' && r.Spec.Labels.configuration)
|
||||
let databases = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && r.Spec.Labels.configuration)
|
||||
let services = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'service' && r.Spec.Labels.configuration)
|
||||
applications = applications.map(r => {
|
||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||
return {
|
||||
configuration: JSON.parse(r.Spec.Labels.configuration),
|
||||
UpdatedAt: r.UpdatedAt
|
||||
}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
databases = databases.map(r => {
|
||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||
return {
|
||||
configuration: JSON.parse(r.Spec.Labels.configuration)
|
||||
}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
services = services.map(r => {
|
||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||
return {
|
||||
serviceName: r.Spec.Labels.serviceName,
|
||||
configuration: JSON.parse(r.Spec.Labels.configuration)
|
||||
}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
applications = [...new Map(applications.map(item => [item.configuration.publish.domain + item.configuration.publish.path, item])).values()]
|
||||
return {
|
||||
serverLogs,
|
||||
applications: {
|
||||
deployed: applications
|
||||
},
|
||||
databases: {
|
||||
deployed: databases
|
||||
},
|
||||
services: {
|
||||
deployed: services
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const serverLogs = await ServerLog.find()
|
||||
const services = await docker.engine.listServices()
|
||||
|
||||
let applications = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' && r.Spec.Labels.configuration)
|
||||
let databases = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && r.Spec.Labels.configuration)
|
||||
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
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT' && error.errno === -2) {
|
||||
throw new Error(`Docker service unavailable at ${error.address}.`)
|
||||
} else {
|
||||
await saveServerLog(error)
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ const fs = require('fs').promises
|
||||
const cuid = require('cuid')
|
||||
const { docker } = require('../../../libs/docker')
|
||||
const { execShellAsync } = require('../../../libs/common')
|
||||
const { saveServerLog } = require('../../../libs/logging')
|
||||
|
||||
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
|
||||
const generator = require('generate-password')
|
||||
@@ -17,13 +18,15 @@ module.exports = async function (fastify) {
|
||||
const database = (await docker.engine.listServices()).find(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && JSON.parse(r.Spec.Labels.configuration).general.deployId === deployId)
|
||||
if (database) {
|
||||
const jsonEnvs = {}
|
||||
for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
|
||||
const s = d.split('=')
|
||||
jsonEnvs[s[0]] = s[1]
|
||||
if (database.Spec.TaskTemplate.ContainerSpec.Env) {
|
||||
for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
|
||||
const s = d.split('=')
|
||||
jsonEnvs[s[0]] = s[1]
|
||||
}
|
||||
}
|
||||
const payload = {
|
||||
config: JSON.parse(database.Spec.Labels.configuration),
|
||||
envs: jsonEnvs
|
||||
envs: jsonEnvs || null
|
||||
}
|
||||
reply.code(200).send(payload)
|
||||
} else {
|
||||
@@ -38,131 +41,147 @@ module.exports = async function (fastify) {
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb'] }
|
||||
type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb', 'clickhouse'] }
|
||||
},
|
||||
required: ['type']
|
||||
}
|
||||
}
|
||||
|
||||
fastify.post('/deploy', { schema: postSchema }, async (request, reply) => {
|
||||
let { type, defaultDatabaseName } = request.body
|
||||
const passwords = generator.generateMultiple(2, {
|
||||
length: 24,
|
||||
numbers: true,
|
||||
strict: true
|
||||
})
|
||||
const usernames = generator.generateMultiple(2, {
|
||||
length: 10,
|
||||
numbers: true,
|
||||
strict: true
|
||||
})
|
||||
// TODO: Query for existing db with the same name
|
||||
const nickname = getUniq()
|
||||
try {
|
||||
let { type, defaultDatabaseName } = request.body
|
||||
const passwords = generator.generateMultiple(2, {
|
||||
length: 24,
|
||||
numbers: true,
|
||||
strict: true
|
||||
})
|
||||
const usernames = generator.generateMultiple(2, {
|
||||
length: 10,
|
||||
numbers: true,
|
||||
strict: true
|
||||
})
|
||||
// TODO: Query for existing db with the same name
|
||||
const nickname = getUniq()
|
||||
|
||||
if (!defaultDatabaseName) defaultDatabaseName = nickname
|
||||
if (!defaultDatabaseName) defaultDatabaseName = nickname
|
||||
|
||||
reply.code(201).send({ message: 'Deploying.' })
|
||||
// TODO: Persistent volume, custom inputs
|
||||
const deployId = cuid()
|
||||
const configuration = {
|
||||
general: {
|
||||
workdir: `/tmp/${deployId}`,
|
||||
deployId,
|
||||
nickname,
|
||||
type
|
||||
},
|
||||
database: {
|
||||
usernames,
|
||||
passwords,
|
||||
defaultDatabaseName
|
||||
},
|
||||
deploy: {
|
||||
name: nickname
|
||||
reply.code(201).send({ message: 'Deploying.' })
|
||||
const deployId = cuid()
|
||||
const configuration = {
|
||||
general: {
|
||||
workdir: `/tmp/${deployId}`,
|
||||
deployId,
|
||||
nickname,
|
||||
type
|
||||
},
|
||||
database: {
|
||||
usernames,
|
||||
passwords,
|
||||
defaultDatabaseName
|
||||
},
|
||||
deploy: {
|
||||
name: nickname
|
||||
}
|
||||
}
|
||||
}
|
||||
let generateEnvs = {}
|
||||
let image = null
|
||||
let volume = null
|
||||
if (type === 'mongodb') {
|
||||
generateEnvs = {
|
||||
MONGODB_ROOT_PASSWORD: passwords[0],
|
||||
MONGODB_USERNAME: usernames[0],
|
||||
MONGODB_PASSWORD: passwords[1],
|
||||
MONGODB_DATABASE: defaultDatabaseName
|
||||
}
|
||||
image = 'bitnami/mongodb:4.4'
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb`
|
||||
} else if (type === 'postgresql') {
|
||||
generateEnvs = {
|
||||
POSTGRESQL_PASSWORD: passwords[0],
|
||||
POSTGRESQL_USERNAME: usernames[0],
|
||||
POSTGRESQL_DATABASE: defaultDatabaseName
|
||||
}
|
||||
image = 'bitnami/postgresql:13.2.0'
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql`
|
||||
} else if (type === 'couchdb') {
|
||||
generateEnvs = {
|
||||
COUCHDB_PASSWORD: passwords[0],
|
||||
COUCHDB_USER: usernames[0]
|
||||
}
|
||||
image = 'bitnami/couchdb:3'
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb`
|
||||
} else if (type === 'mysql') {
|
||||
generateEnvs = {
|
||||
MYSQL_ROOT_PASSWORD: passwords[0],
|
||||
MYSQL_ROOT_USER: usernames[0],
|
||||
MYSQL_USER: usernames[1],
|
||||
MYSQL_PASSWORD: passwords[1],
|
||||
MYSQL_DATABASE: defaultDatabaseName
|
||||
}
|
||||
image = 'bitnami/mysql:8.0'
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`
|
||||
}
|
||||
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[configuration.general.deployId]: {
|
||||
image,
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvs,
|
||||
volumes: [volume],
|
||||
deploy: {
|
||||
replicas: 1,
|
||||
update_config: {
|
||||
parallelism: 0,
|
||||
delay: '10s',
|
||||
order: 'start-first'
|
||||
},
|
||||
rollback_config: {
|
||||
parallelism: 0,
|
||||
delay: '10s',
|
||||
order: 'start-first'
|
||||
},
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=database',
|
||||
'configuration=' + JSON.stringify(configuration)
|
||||
]
|
||||
await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
|
||||
let generateEnvs = {}
|
||||
let image = null
|
||||
let volume = null
|
||||
let ulimits = {}
|
||||
if (type === 'mongodb') {
|
||||
generateEnvs = {
|
||||
MONGODB_ROOT_PASSWORD: passwords[0],
|
||||
MONGODB_USERNAME: usernames[0],
|
||||
MONGODB_PASSWORD: passwords[1],
|
||||
MONGODB_DATABASE: defaultDatabaseName
|
||||
}
|
||||
image = 'bitnami/mongodb:4.4'
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb`
|
||||
} else if (type === 'postgresql') {
|
||||
generateEnvs = {
|
||||
POSTGRESQL_PASSWORD: passwords[0],
|
||||
POSTGRESQL_USERNAME: usernames[0],
|
||||
POSTGRESQL_DATABASE: defaultDatabaseName
|
||||
}
|
||||
image = 'bitnami/postgresql:13.2.0'
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql`
|
||||
} else if (type === 'couchdb') {
|
||||
generateEnvs = {
|
||||
COUCHDB_PASSWORD: passwords[0],
|
||||
COUCHDB_USER: usernames[0]
|
||||
}
|
||||
image = 'bitnami/couchdb:3'
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb`
|
||||
} else if (type === 'mysql') {
|
||||
generateEnvs = {
|
||||
MYSQL_ROOT_PASSWORD: passwords[0],
|
||||
MYSQL_ROOT_USER: usernames[0],
|
||||
MYSQL_USER: usernames[1],
|
||||
MYSQL_PASSWORD: passwords[1],
|
||||
MYSQL_DATABASE: defaultDatabaseName
|
||||
}
|
||||
image = 'bitnami/mysql:8.0'
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`
|
||||
} else if (type === 'clickhouse') {
|
||||
image = 'yandex/clickhouse-server'
|
||||
volume = `${configuration.general.deployId}-${type}-data:/var/lib/clickhouse`
|
||||
ulimits = {
|
||||
nofile: {
|
||||
soft: 262144,
|
||||
hard: 262144
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[`${configuration.general.deployId}-${type}-data`]: {
|
||||
external: true
|
||||
}
|
||||
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[configuration.general.deployId]: {
|
||||
image,
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvs,
|
||||
volumes: [volume],
|
||||
ulimits,
|
||||
deploy: {
|
||||
replicas: 1,
|
||||
update_config: {
|
||||
parallelism: 0,
|
||||
delay: '10s',
|
||||
order: 'start-first'
|
||||
},
|
||||
rollback_config: {
|
||||
parallelism: 0,
|
||||
delay: '10s',
|
||||
order: 'start-first'
|
||||
},
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=database',
|
||||
'configuration=' + JSON.stringify(configuration)
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[`${configuration.general.deployId}-${type}-data`]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
}
|
||||
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
|
||||
await execShellAsync(
|
||||
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
|
||||
)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
await saveServerLog(error)
|
||||
throw new Error(error)
|
||||
}
|
||||
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) => {
|
||||
|
||||
@@ -4,6 +4,8 @@ const Settings = require('../../../models/Settings')
|
||||
const cuid = require('cuid')
|
||||
const mongoose = require('mongoose')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { saveServerLog } = require('../../../libs/logging')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
const githubCodeSchema = {
|
||||
schema: {
|
||||
@@ -59,8 +61,12 @@ module.exports = async function (fastify) {
|
||||
avatar: avatar_url,
|
||||
uid
|
||||
})
|
||||
const defaultSettings = new Settings({
|
||||
_id: new mongoose.Types.ObjectId()
|
||||
})
|
||||
try {
|
||||
await newUser.save()
|
||||
await defaultSettings.save()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
reply.code(500).send({ success: false, error: e })
|
||||
@@ -111,8 +117,8 @@ module.exports = async function (fastify) {
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
reply.code(500).send({ success: false, error: error.message })
|
||||
await saveServerLog(error)
|
||||
throw new Error(error)
|
||||
}
|
||||
})
|
||||
fastify.get('/success', async (request, reply) => {
|
||||
|
||||
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 { saveServerLog } = require('../../../libs/logging')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
const applicationName = 'coolify'
|
||||
const postSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowRegistration: { type: 'boolean' }
|
||||
allowRegistration: { type: 'boolean' },
|
||||
sendErrors: { type: 'boolean' }
|
||||
},
|
||||
required: ['allowRegistration']
|
||||
required: []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +28,7 @@ module.exports = async function (fastify) {
|
||||
settings
|
||||
}
|
||||
} catch (error) {
|
||||
await saveServerLog(error)
|
||||
throw new Error(error)
|
||||
}
|
||||
})
|
||||
@@ -38,6 +42,7 @@ module.exports = async function (fastify) {
|
||||
).select('-_id -__v')
|
||||
reply.code(201).send({ settings })
|
||||
} catch (error) {
|
||||
await saveServerLog(error)
|
||||
throw new Error(error)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,10 +3,10 @@ const { saveServerLog } = require('../../../libs/logging')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
fastify.get('/', async (request, reply) => {
|
||||
const upgradeP1 = await execShellAsync('bash ./install.sh upgrade-phase-1')
|
||||
await saveServerLog({ event: upgradeP1, type: 'UPGRADE-P-1' })
|
||||
const upgradeP1 = await execShellAsync('bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p1.sh)"')
|
||||
await saveServerLog({ message: upgradeP1, type: 'UPGRADE-P-1' })
|
||||
reply.code(200).send('I\'m trying, okay?')
|
||||
const upgradeP2 = await execShellAsync('bash ./install.sh upgrade-phase-2')
|
||||
await saveServerLog({ event: upgradeP2, type: 'UPGRADE-P-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({ message: upgradeP2, type: 'UPGRADE-P-2' })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ const jwt = require('jsonwebtoken')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
fastify.get('/', async (request, reply) => {
|
||||
const { authorization } = request.headers
|
||||
if (!authorization) {
|
||||
try {
|
||||
const { authorization } = request.headers
|
||||
if (!authorization) {
|
||||
reply.code(401).send({})
|
||||
return
|
||||
}
|
||||
const token = authorization.split(' ')[1]
|
||||
const verify = jwt.verify(token, fastify.config.JWT_SIGN_KEY)
|
||||
const found = await User.findOne({ uid: verify.jti })
|
||||
found ? reply.code(200).send({}) : reply.code(401).send({})
|
||||
} catch (error) {
|
||||
reply.code(401).send({})
|
||||
return
|
||||
}
|
||||
const token = authorization.split(' ')[1]
|
||||
const verify = jwt.verify(token, fastify.config.JWT_SIGN_KEY)
|
||||
const found = await User.findOne({ uid: verify.jti })
|
||||
found ? reply.code(200).send({}) : reply.code(401).send({})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
const crypto = require('crypto')
|
||||
const { cleanupTmp, execShellAsync } = require('../../../libs/common')
|
||||
const { cleanupTmp } = require('../../../libs/common')
|
||||
|
||||
const Deployment = require('../../../models/Deployment')
|
||||
const ApplicationLog = require('../../../models/Logs/Application')
|
||||
const ServerLog = require('../../../models/Logs/Server')
|
||||
|
||||
const { queueAndBuild } = require('../../../libs/applications')
|
||||
const { setDefaultConfiguration } = require('../../../libs/applications/configuration')
|
||||
const { setDefaultConfiguration, precheckDeployment } = require('../../../libs/applications/configuration')
|
||||
const { docker } = require('../../../libs/docker')
|
||||
const cloneRepository = require('../../../libs/applications/github/cloneRepository')
|
||||
const { purgeImagesContainers } = require('../../../libs/applications/cleanup')
|
||||
|
||||
module.exports = async function (fastify) {
|
||||
// TODO: Add this to fastify plugin
|
||||
const postSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
@@ -33,6 +37,7 @@ module.exports = async function (fastify) {
|
||||
}
|
||||
}
|
||||
fastify.post('/', { schema: postSchema }, async (request, reply) => {
|
||||
let configuration
|
||||
const hmac = crypto.createHmac('sha256', fastify.config.GITHUP_APP_WEBHOOK_SECRET)
|
||||
const digest = Buffer.from('sha256=' + hmac.update(JSON.stringify(request.body)).digest('hex'), 'utf8')
|
||||
const checksum = Buffer.from(request.headers['x-hub-signature-256'], 'utf8')
|
||||
@@ -45,98 +50,72 @@ module.exports = async function (fastify) {
|
||||
reply.code(500).send({ error: 'Not a push event.' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||
|
||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||
|
||||
let configuration = services.find(r => {
|
||||
if (request.body.ref.startsWith('refs')) {
|
||||
const branch = request.body.ref.split('/')[2]
|
||||
if (
|
||||
JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
|
||||
JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
|
||||
) {
|
||||
return r
|
||||
configuration = services.find(r => {
|
||||
if (request.body.ref.startsWith('refs')) {
|
||||
const branch = request.body.ref.split('/')[2]
|
||||
if (
|
||||
JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
|
||||
JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
|
||||
) {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
if (!configuration) {
|
||||
reply.code(500).send({ error: 'No configuration found.' })
|
||||
return
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration))
|
||||
await cloneRepository(configuration)
|
||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
|
||||
|
||||
if (!configuration) {
|
||||
reply.code(500).send({ error: 'No configuration found.' })
|
||||
return
|
||||
}
|
||||
|
||||
configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration))
|
||||
|
||||
await cloneRepository(configuration)
|
||||
|
||||
let foundService = false
|
||||
let foundDomain = false
|
||||
let configChanged = false
|
||||
let imageChanged = false
|
||||
|
||||
let forceUpdate = false
|
||||
|
||||
for (const service of services) {
|
||||
const running = JSON.parse(service.Spec.Labels.configuration)
|
||||
if (running) {
|
||||
if (
|
||||
running.publish.domain === configuration.publish.domain &&
|
||||
running.repository.id !== configuration.repository.id &&
|
||||
running.repository.branch !== configuration.repository.branch
|
||||
) {
|
||||
foundDomain = true
|
||||
}
|
||||
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
|
||||
const state = await execShellAsync(`docker stack ps ${running.build.container.name} --format '{{ json . }}'`)
|
||||
const isError = state.split('\n').filter(n => n).map(s => JSON.parse(s)).filter(n => n.DesiredState !== 'Running')
|
||||
if (isError.length > 0) forceUpdate = true
|
||||
foundService = true
|
||||
|
||||
const runningWithoutContainer = JSON.parse(JSON.stringify(running))
|
||||
delete runningWithoutContainer.build.container
|
||||
|
||||
const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration))
|
||||
delete configurationWithoutContainer.build.container
|
||||
|
||||
if (JSON.stringify(runningWithoutContainer.build) !== JSON.stringify(configurationWithoutContainer.build) || JSON.stringify(runningWithoutContainer.publish) !== JSON.stringify(configurationWithoutContainer.publish)) configChanged = true
|
||||
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundDomain) {
|
||||
cleanupTmp(configuration.general.workdir)
|
||||
reply.code(500).send({ message: 'Domain already used.' })
|
||||
return
|
||||
}
|
||||
if (forceUpdate) {
|
||||
imageChanged = false
|
||||
configChanged = false
|
||||
} else {
|
||||
if (foundService && !imageChanged && !configChanged) {
|
||||
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
|
||||
cleanupTmp(configuration.general.workdir)
|
||||
reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
|
||||
return
|
||||
}
|
||||
const alreadyQueued = await Deployment.find({
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain,
|
||||
progress: { $in: ['queued', 'inprogress'] }
|
||||
})
|
||||
|
||||
if (alreadyQueued.length > 0) {
|
||||
reply.code(200).send({ message: 'Already in the queue.' })
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
const fs = require('fs')
|
||||
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 path = require('path')
|
||||
const { saveServerLog } = require('./libs/logging')
|
||||
const { execShellAsync } = require('./libs/common')
|
||||
const { cleanupStuckedDeploymentsInDB } = require('./libs/applications/cleanup')
|
||||
const fastify = require('fastify')({
|
||||
trustProxy: true,
|
||||
logger: {
|
||||
level: 'error'
|
||||
}
|
||||
})
|
||||
fastify.register(require('../api/libs/http-error'))
|
||||
|
||||
const { schema } = require('./schema')
|
||||
|
||||
process.on('unhandledRejection', async (reason, p) => {
|
||||
await saveServerLog({ message: reason.message, type: 'unhandledRejection' })
|
||||
})
|
||||
|
||||
fastify.register(require('fastify-env'), {
|
||||
schema,
|
||||
dotenv: true
|
||||
@@ -30,15 +40,6 @@ if (process.env.NODE_ENV === 'production') {
|
||||
}
|
||||
|
||||
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') {
|
||||
mongoose.connect(
|
||||
@@ -82,9 +83,27 @@ mongoose.connection.once('open', async function () {
|
||||
fastify.listen(3001)
|
||||
console.log('Coolify API is up and running in development.')
|
||||
}
|
||||
try {
|
||||
// Always cleanup server logs
|
||||
await mongoose.connection.db.dropCollection('logs-servers')
|
||||
} catch (error) {
|
||||
// Could not cleanup logs-servers collection
|
||||
}
|
||||
// On start cleanup inprogress/queued deployments.
|
||||
const deployments = await Deployment.find({ progress: { $in: ['queued', 'inprogress'] } })
|
||||
for (const deployment of deployments) {
|
||||
await Deployment.findByIdAndUpdate(deployment._id, { $set: { progress: 'failed' } })
|
||||
try {
|
||||
await cleanupStuckedDeploymentsInDB()
|
||||
} catch (error) {
|
||||
// Could not cleanup DB 🤔
|
||||
}
|
||||
try {
|
||||
// Doing because I do not want to prune these images. Prune skips coolify-reserve labeled images.
|
||||
const basicImages = ['nginx:stable-alpine', 'node:lts', 'ubuntu:20.04', 'php:apache', 'rust:latest']
|
||||
for (const image of basicImages) {
|
||||
// await execShellAsync(`echo "FROM ${image}" | docker build --label coolify-reserve=true -t ${image} -`)
|
||||
await execShellAsync(`docker pull ${image}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Could not pull some basic images from Docker Hub.')
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<link rel="preload" as="image" href="/favicon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>coolify: Heroku & Netlify alternative</title>
|
||||
<title>Coolify</title>
|
||||
<link rel="dns-prefetch" href="https://cdn.coollabs.io/" />
|
||||
<link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" />
|
||||
<link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" />
|
||||
<link rel="stylesheet" href="https://cdn.coollabs.io/css/microtip-0.2.2.min.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
73
install.sh
73
install.sh
@@ -1,43 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
preTasks() {
|
||||
echo '
|
||||
##############################
|
||||
#### Pulling Git Updates #####
|
||||
##############################'
|
||||
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
|
||||
echo '#### Ooops something not okay!'
|
||||
echo '
|
||||
####################################
|
||||
#### Ooops something not okay! #####
|
||||
####################################'
|
||||
exit 1
|
||||
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
|
||||
if [ $? -ne 0 ]; then
|
||||
echo '#### Missing configuration.'
|
||||
echo '
|
||||
##################################
|
||||
#### Missing configuration ! #####
|
||||
##################################'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
}
|
||||
case "$1" in
|
||||
"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
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
"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
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Use 'all' to build & deploy proxy+coolify, 'coolify' to build & deploy only coolify, 'proxy' to build & deploy only proxy."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
esac
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM coolify-base
|
||||
WORKDIR /usr/src/app
|
||||
RUN yarn build
|
||||
CMD ["yarn", "start"]
|
||||
RUN pnpm build
|
||||
CMD ["pnpm", "start"]
|
||||
EXPOSE 3000
|
||||
@@ -9,9 +9,10 @@ RUN apt update && apt install -y docker-ce-cli && apt clean all
|
||||
FROM node:14 as modules
|
||||
COPY --from=binaries /usr/bin/docker /usr/bin/docker
|
||||
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
|
||||
COPY ./package*.json .
|
||||
RUN yarn install
|
||||
RUN pnpm install
|
||||
|
||||
FROM modules
|
||||
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:
|
||||
proxy:
|
||||
image: traefik:v2.3
|
||||
image: traefik:v2.4
|
||||
hostname: coollabs-proxy
|
||||
ports:
|
||||
- target: 80
|
||||
@@ -22,6 +22,7 @@ services:
|
||||
- --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
|
||||
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
|
||||
|
||||
@@ -13,7 +13,8 @@ program
|
||||
|
||||
program.parse(process.argv)
|
||||
|
||||
if (program.check) {
|
||||
const options = program.opts()
|
||||
if (options.check) {
|
||||
checkConfig().then(() => {
|
||||
console.log('Config: OK')
|
||||
}).catch((err) => {
|
||||
@@ -26,16 +27,18 @@ if (program.check) {
|
||||
console.error(`Please run as root! Current user: ${user}`)
|
||||
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 .')
|
||||
if (program.type === 'all') {
|
||||
shell.exec('docker stack rm coollabs-coolify', { silent: !program.debug })
|
||||
} else if (program.type === 'coolify') {
|
||||
if (options.type === 'all') {
|
||||
shell.exec('docker stack rm coollabs-coolify', { silent: !options.debug })
|
||||
} else if (options.type === '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')
|
||||
}
|
||||
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 () {
|
||||
|
||||
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 shell = require('shelljs')
|
||||
const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '')
|
||||
|
||||
program.version('0.0.1')
|
||||
program
|
||||
.option('-d, --debug', 'Debug outputs.')
|
||||
@@ -10,12 +9,13 @@ program
|
||||
.option('-t, --type <type>', 'Deploy type.')
|
||||
|
||||
program.parse(process.argv)
|
||||
|
||||
const options = program.opts()
|
||||
if (user !== 'root') {
|
||||
console.error(`Please run as root! Current user: ${user}`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (program.type === 'upgrade') {
|
||||
|
||||
if (options.type === 'upgrade') {
|
||||
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",
|
||||
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.11",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"lint": "standard",
|
||||
@@ -16,43 +16,48 @@
|
||||
"build:svite": "svite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@roxi/routify": "^2.7.3",
|
||||
"@zerodevx/svelte-toast": "^0.1.4",
|
||||
"axios": "^0.21.0",
|
||||
"commander": "^6.2.1",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@roxi/routify": "^2.15.1",
|
||||
"@zerodevx/svelte-toast": "^0.2.2",
|
||||
"ajv": "^8.1.0",
|
||||
"axios": "^0.21.1",
|
||||
"commander": "^7.2.0",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cuid": "^2.1.8",
|
||||
"dayjs": "^1.10.4",
|
||||
"deepmerge": "^4.2.2",
|
||||
"dockerode": "^3.2.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"fastify": "^3.9.1",
|
||||
"fastify": "^3.14.2",
|
||||
"fastify-env": "^2.1.0",
|
||||
"fastify-jwt": "^2.1.3",
|
||||
"fastify-jwt": "^2.4.0",
|
||||
"fastify-plugin": "^3.0.0",
|
||||
"fastify-static": "^3.3.0",
|
||||
"fastify-static": "^4.0.1",
|
||||
"generate-password": "^1.6.0",
|
||||
"http-errors-enhanced": "^0.7.0",
|
||||
"js-yaml": "^4.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^5.11.4",
|
||||
"mongoose": "^5.12.3",
|
||||
"shelljs": "^0.8.4",
|
||||
"svelte-select": "^3.17.0",
|
||||
"unique-names-generator": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mongodb-memory-server-core": "^6.9.3",
|
||||
"nodemon": "^2.0.6",
|
||||
"mongodb-memory-server-core": "^6.9.6",
|
||||
"nodemon": "^2.0.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^7.0.35",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-load-config": "^3.0.0",
|
||||
"postcss": "^8.2.9",
|
||||
"postcss-import": "^14.0.1",
|
||||
"postcss-load-config": "^3.0.1",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "1.19",
|
||||
"prettier-plugin-svelte": "^2.1.6",
|
||||
"prettier": "2.2.1",
|
||||
"prettier-plugin-svelte": "^2.2.0",
|
||||
"standard": "^16.0.3",
|
||||
"svelte": "^3.29.7",
|
||||
"svelte-hmr": "^0.12.2",
|
||||
"svelte-preprocess": "^4.6.1",
|
||||
"svelte": "^3.37.0",
|
||||
"svelte-hmr": "^0.14.0",
|
||||
"svelte-preprocess": "^4.7.0",
|
||||
"svite": "0.8.1",
|
||||
"tailwindcss": "compat"
|
||||
"tailwindcss": "2.1.1"
|
||||
},
|
||||
"keywords": [
|
||||
"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 { routes } from "../.routify/routes";
|
||||
const options = {
|
||||
duration: 2000,
|
||||
dismissable: false
|
||||
duration: 2000
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="postcss">
|
||||
:global(.main) {
|
||||
width: calc(100% - 4rem);
|
||||
margin-left: 4rem;
|
||||
}
|
||||
:global(._toastMsg) {
|
||||
@apply text-sm font-bold !important;
|
||||
}
|
||||
@@ -28,7 +31,7 @@
|
||||
@apply bg-warmGray-700 !important;
|
||||
}
|
||||
:global(input) {
|
||||
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none !important;
|
||||
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
|
||||
}
|
||||
:global(input:hover) {
|
||||
@apply bg-warmGray-700 !important;
|
||||
@@ -57,6 +60,22 @@
|
||||
:global(.h-271) {
|
||||
min-height: 271px !important;
|
||||
}
|
||||
:global(.repository-select-search .listItem .item),
|
||||
:global(.repository-select-search .empty) {
|
||||
@apply text-sm py-3 font-bold bg-warmGray-800 text-white cursor-pointer border-none hover:bg-warmGray-700 !important;
|
||||
}
|
||||
|
||||
:global(.repository-select-search .listContainer) {
|
||||
@apply bg-transparent !important;
|
||||
}
|
||||
|
||||
:global(.repository-select-search .clearSelect) {
|
||||
@apply text-white cursor-pointer !important;
|
||||
}
|
||||
|
||||
:global(.repository-select-search .selectedItem) {
|
||||
@apply text-white relative cursor-pointer font-bold text-sm flex items-center !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<SvelteToast options="{options}" />
|
||||
|
||||
@@ -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>
|
||||
import { application } from "@store";
|
||||
import { onMount } from "svelte";
|
||||
import TooltipInfo from "../../../Tooltip/TooltipInfo.svelte";
|
||||
let domainInput;
|
||||
const buildpacks = {
|
||||
static: {
|
||||
port: {
|
||||
active: false,
|
||||
number: 80,
|
||||
},
|
||||
build: true,
|
||||
},
|
||||
nodejs: {
|
||||
port: {
|
||||
active: true,
|
||||
number: 3000,
|
||||
},
|
||||
build: true,
|
||||
},
|
||||
vuejs: {
|
||||
port: {
|
||||
active: false,
|
||||
number: 80,
|
||||
},
|
||||
build: true,
|
||||
},
|
||||
nuxtjs: {
|
||||
port: {
|
||||
active: true,
|
||||
number: 3000,
|
||||
},
|
||||
build: true,
|
||||
},
|
||||
react: {
|
||||
port: {
|
||||
active: false,
|
||||
number: 80,
|
||||
},
|
||||
build: true,
|
||||
},
|
||||
nextjs: {
|
||||
port: {
|
||||
active: true,
|
||||
number: 3000,
|
||||
},
|
||||
build: true,
|
||||
},
|
||||
gatsby: {
|
||||
port: {
|
||||
active: true,
|
||||
number: 3000,
|
||||
},
|
||||
build: true,
|
||||
},
|
||||
svelte: {
|
||||
port: {
|
||||
active: false,
|
||||
number: 80,
|
||||
},
|
||||
build: true,
|
||||
},
|
||||
php: {
|
||||
port: {
|
||||
active: false,
|
||||
number: 80,
|
||||
},
|
||||
build: false,
|
||||
},
|
||||
rust: {
|
||||
port: {
|
||||
active: true,
|
||||
number: 3000,
|
||||
},
|
||||
build: false,
|
||||
},
|
||||
docker: {
|
||||
port: {
|
||||
active: true,
|
||||
number: 3000,
|
||||
},
|
||||
build: false,
|
||||
},
|
||||
};
|
||||
function selectBuildPack(event) {
|
||||
if (event.target.innerText === "React/Preact") {
|
||||
$application.build.pack = "react";
|
||||
} else {
|
||||
$application.build.pack = event.target.innerText
|
||||
.replace(/\./g, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
onMount(()=> {
|
||||
domainInput.focus();
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="grid grid-cols-1 text-sm space-y-2 max-w-2xl md:mx-auto mx-6 pb-6 auto-cols-max"
|
||||
>
|
||||
<label for="buildPack">Build Pack</label>
|
||||
<select id="buildPack" bind:value="{$application.build.pack}">
|
||||
<option selected class="font-medium">static</option>
|
||||
<option class="font-medium">nodejs</option>
|
||||
</select>
|
||||
<div
|
||||
class="grid grid-cols-1 text-sm max-w-4xl md:mx-auto mx-6 pb-16 auto-cols-max "
|
||||
>
|
||||
<div class="text-2xl font-bold border-gradient w-40">Build Packs</div>
|
||||
<div class="flex font-bold flex-wrap justify-center pt-10">
|
||||
<div
|
||||
class="{$application.build.pack === 'static'
|
||||
? 'buildpack bg-red-500'
|
||||
: 'buildpack hover:border-red-500'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
Static
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'nodejs'
|
||||
? 'buildpack bg-emerald-600'
|
||||
: 'buildpack hover:border-emerald-600'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
NodeJS
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'vuejs'
|
||||
? 'buildpack bg-green-500'
|
||||
: 'buildpack hover:border-green-500'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
VueJS
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'nuxtjs'
|
||||
? 'buildpack bg-green-500'
|
||||
: 'buildpack hover:border-green-500'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
NuxtJS
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'react'
|
||||
? 'buildpack bg-gradient-to-r from-blue-500 to-purple-500'
|
||||
: 'buildpack hover:border-blue-500'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
React/Preact
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'nextjs'
|
||||
? 'buildpack bg-blue-500'
|
||||
: 'buildpack hover:border-blue-500'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
NextJS
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'gatsby'
|
||||
? 'buildpack bg-blue-500'
|
||||
: 'buildpack hover:border-blue-500'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
Gatsby
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'svelte'
|
||||
? 'buildpack bg-orange-600'
|
||||
: 'buildpack hover:border-orange-600'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
Svelte
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'php'
|
||||
? 'buildpack bg-indigo-500'
|
||||
: 'buildpack hover:border-indigo-500'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
PHP
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'rust'
|
||||
? 'buildpack bg-pink-500'
|
||||
: 'buildpack hover:border-pink-500'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
Rust
|
||||
</div>
|
||||
<div
|
||||
class="{$application.build.pack === 'docker'
|
||||
? 'buildpack bg-purple-500'
|
||||
: 'buildpack hover:border-purple-500'}"
|
||||
on:click="{selectBuildPack}"
|
||||
>
|
||||
Docker
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-2 space-y-2 max-w-2xl md:mx-auto mx-6 justify-center items-center"
|
||||
>
|
||||
<label for="Domain">Domain</label>
|
||||
<input
|
||||
class:placeholder-red-500="{$application.publish.domain == null || $application.publish.domain == ''}"
|
||||
class:border-red-500="{$application.publish.domain == null || $application.publish.domain == ''}"
|
||||
id="Domain"
|
||||
bind:value="{$application.publish.domain}"
|
||||
placeholder="eg: coollabs.io (without www)"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
|
||||
<div
|
||||
class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10"
|
||||
>
|
||||
<div class="grid grid-flow-col gap-2 items-center pb-6">
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="Domain" class="">Domain</label>
|
||||
<input
|
||||
id="Port"
|
||||
bind:value="{$application.publish.port}"
|
||||
placeholder="{$application.build.pack === 'static'
|
||||
? '80'
|
||||
: '3000'}"
|
||||
bind:this={domainInput}
|
||||
class="border-2"
|
||||
class:placeholder-red-500="{$application.publish.domain == null ||
|
||||
$application.publish.domain == ''}"
|
||||
class:border-red-500="{$application.publish.domain == null ||
|
||||
$application.publish.domain == ''}"
|
||||
id="Domain"
|
||||
bind:value="{$application.publish.domain}"
|
||||
placeholder="eg: coollabs.io (without www)"
|
||||
/>
|
||||
{/if}
|
||||
<!-- {#if config.buildPack === "static"}
|
||||
<div class="text-base font-bold text-white pt-2">
|
||||
Preview Deploys
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
on:click="{() =>
|
||||
(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}"
|
||||
</div>
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="Path"
|
||||
>Path <TooltipInfo
|
||||
label="{`Path to deploy your application on your domain. eg: /api means it will be deployed to -> https://${
|
||||
$application.publish.domain || '<yourdomain>'
|
||||
}/api`}"
|
||||
/></label
|
||||
>
|
||||
<span class="sr-only">Use setting</span>
|
||||
<span
|
||||
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
class:translate-x-5="{config.previewDeploy}"
|
||||
class:translate-x-0="{!config.previewDeploy}"
|
||||
>
|
||||
<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} -->
|
||||
<input
|
||||
id="Path"
|
||||
bind:value="{$application.publish.path}"
|
||||
placeholder="/"
|
||||
/>
|
||||
</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() {
|
||||
if (secret.name && secret.value) {
|
||||
const found = $application.publish.secrets.find(
|
||||
s => s.name === secret.name,
|
||||
);
|
||||
if (!found) {
|
||||
$application.publish.secrets = [
|
||||
...$application.publish.secrets,
|
||||
{
|
||||
name: secret.name,
|
||||
value: secret.value,
|
||||
},
|
||||
];
|
||||
secret = {
|
||||
name: null,
|
||||
value: null
|
||||
s => s.name === secret.name,
|
||||
);
|
||||
if (!found) {
|
||||
$application.publish.secrets = [
|
||||
...$application.publish.secrets,
|
||||
{
|
||||
name: secret.name,
|
||||
value: secret.value,
|
||||
},
|
||||
];
|
||||
secret = {
|
||||
name: null,
|
||||
value: null,
|
||||
};
|
||||
} else {
|
||||
foundSecret = found;
|
||||
}
|
||||
} else {
|
||||
foundSecret = found;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<div class="space-y-2 max-w-2xl md:mx-auto mx-6 text-center">
|
||||
<div class="text-left text-base font-bold tracking-tight text-warmGray-400">New Secret</div>
|
||||
<div class="grid md:grid-flow-col grid-flow-row gap-2">
|
||||
<input id="secretName" bind:value="{secret.name}" placeholder="Name" />
|
||||
<input id="secretValue" bind:value="{secret.value}" placeholder="Value" />
|
||||
<button
|
||||
class="button p-1 w-20 bg-green-600 hover:bg-green-500 text-white"
|
||||
on:click="{saveSecret}">Save</button
|
||||
>
|
||||
<div class="text-2xl font-bold border-gradient w-24">Secrets</div>
|
||||
<div class="max-w-xl mx-auto text-center pt-4">
|
||||
<div class="text-left text-base font-bold tracking-tight text-warmGray-400">
|
||||
New Secret
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<input id="secretName" bind:value="{secret.name}" placeholder="Name" class="w-64 border-2 border-transparent" />
|
||||
<input id="secretValue" bind:value="{secret.value}" placeholder="Value" class="w-64 border-2 border-transparent" />
|
||||
<button class="icon hover:text-green-500" on:click="{saveSecret}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{#if $application.publish.secrets.length > 0}
|
||||
{#each $application.publish.secrets as s}
|
||||
<div class="grid md:grid-flow-col grid-flow-row gap-2">
|
||||
<input
|
||||
id="{s.name}"
|
||||
value="{s.name}"
|
||||
disabled
|
||||
class="bg-transparent border-transparent"
|
||||
class:border-red-600="{foundSecret && foundSecret.name === s.name}"
|
||||
/>
|
||||
<input
|
||||
id="{s.createdAt}"
|
||||
value="ENCRYPTED"
|
||||
disabled
|
||||
class="bg-transparent border-transparent"
|
||||
/>
|
||||
<button
|
||||
class="button w-20 bg-red-600 hover:bg-red-500 text-white"
|
||||
on:click="{() => removeSecret(s.name)}">Delete</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="py-4">
|
||||
{#each $application.publish.secrets as s}
|
||||
<div class="flex space-x-4">
|
||||
<input
|
||||
id="{s.name}"
|
||||
value="{s.name}"
|
||||
disabled
|
||||
class="border-2 bg-transparent border-transparent w-64"
|
||||
class:border-red-600="{foundSecret && foundSecret.name === s.name}"
|
||||
/>
|
||||
<input
|
||||
id="{s.createdAt}"
|
||||
value="SAVED"
|
||||
disabled
|
||||
class="border-2 bg-transparent border-transparent w-64"
|
||||
/>
|
||||
<button class="icon hover:text-red-500" on:click="{() => removeSecret(s.name)}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,45 @@
|
||||
<script>
|
||||
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>
|
||||
|
||||
{#if loading}
|
||||
<div class="grid grid-cols-1">
|
||||
<label for="branch">Branch</label>
|
||||
<select disabled>
|
||||
<option selected>Loading branches</option>
|
||||
</select>
|
||||
<div class="repository-select-search col-span-2">
|
||||
<Select
|
||||
containerClasses="w-full border-none bg-transparent"
|
||||
placeholder="Loading branches..."
|
||||
isDisabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1">
|
||||
<label for="branch">Branch</label>
|
||||
<!-- svelte-ignore a11y-no-onchange -->
|
||||
<select id="branch" bind:value="{$application.repository.branch}">
|
||||
<option disabled selected>Select a branch</option>
|
||||
{#each branches as branch}
|
||||
<option value="{branch.name}" class="font-medium">{branch.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class="repository-select-search col-span-2">
|
||||
<Select
|
||||
containerClasses="w-full border-none bg-transparent"
|
||||
on:select="{handleSelect}"
|
||||
selectedValue="{selectedValue}"
|
||||
isClearable="{false}"
|
||||
items="{branches.map(b => ({ label: b.name, value: b.name }))}"
|
||||
showIndicator="{$activePage.new}"
|
||||
noOptionsMessage="No branches found"
|
||||
placeholder="Select a branch"
|
||||
isDisabled="{!$activePage.new}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<script>
|
||||
import { redirect, isActive } from "@roxi/routify";
|
||||
import { fade } from "svelte/transition";
|
||||
import { session, application, fetch, initialApplication } from "@store";
|
||||
import {
|
||||
session,
|
||||
application,
|
||||
fetch,
|
||||
initialApplication,
|
||||
githubRepositories,
|
||||
githubInstallations,
|
||||
activePage,
|
||||
} from "@store";
|
||||
|
||||
import Login from "./Login.svelte";
|
||||
import Loading from "../../Loading.svelte";
|
||||
@@ -11,14 +19,24 @@
|
||||
|
||||
let loading = {
|
||||
branches: false,
|
||||
github: false,
|
||||
};
|
||||
|
||||
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() {
|
||||
loading.branches = true;
|
||||
const selectedRepository = repositories.find(
|
||||
if ($activePage.new) $application.repository.branch = null;
|
||||
const selectedRepository = $githubRepositories.find(
|
||||
r => r.id === $application.repository.id,
|
||||
);
|
||||
|
||||
@@ -33,7 +51,33 @@
|
||||
loading.branches = false;
|
||||
}
|
||||
|
||||
async function getGithubRepos(id, page) {
|
||||
const data = await $fetch(
|
||||
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadGithub() {
|
||||
if ($githubRepositories.length > 0) {
|
||||
$application.github.installation.id = $githubInstallations.id;
|
||||
$application.github.app.id = $githubInstallations.app_id;
|
||||
const foundRepositoryOnGithub = $githubRepositories.find(
|
||||
r =>
|
||||
r.full_name ===
|
||||
`${$application.repository.organization}/${$application.repository.name}`,
|
||||
);
|
||||
|
||||
if (foundRepositoryOnGithub) {
|
||||
$application.repository.id = foundRepositoryOnGithub.id;
|
||||
$application.repository.organization = foundRepositoryOnGithub.owner.login;
|
||||
$application.repository.name = foundRepositoryOnGithub.name;
|
||||
// await loadBranches();
|
||||
}
|
||||
return;
|
||||
}
|
||||
loading.github = true;
|
||||
try {
|
||||
const { installations } = await $fetch(
|
||||
"https://api.github.com/user/installations",
|
||||
@@ -43,13 +87,29 @@
|
||||
}
|
||||
$application.github.installation.id = installations[0].id;
|
||||
$application.github.app.id = installations[0].app_id;
|
||||
$githubInstallations = installations[0];
|
||||
|
||||
const data = await $fetch(
|
||||
`https://api.github.com/user/installations/${$application.github.installation.id}/repositories?per_page=10000`,
|
||||
let page = 1;
|
||||
let userRepos = 0;
|
||||
const data = await getGithubRepos(
|
||||
$application.github.installation.id,
|
||||
page,
|
||||
);
|
||||
|
||||
repositories = data.repositories;
|
||||
const foundRepositoryOnGithub = data.repositories.find(
|
||||
$githubRepositories = $githubRepositories.concat(data.repositories);
|
||||
userRepos = data.total_count;
|
||||
|
||||
if (userRepos > $githubRepositories.length) {
|
||||
while (userRepos > $githubRepositories.length) {
|
||||
page = page + 1;
|
||||
const repos = await getGithubRepos(
|
||||
$application.github.installation.id,
|
||||
page,
|
||||
);
|
||||
$githubRepositories = $githubRepositories.concat(repos.repositories);
|
||||
}
|
||||
}
|
||||
const foundRepositoryOnGithub = $githubRepositories.find(
|
||||
r =>
|
||||
r.full_name ===
|
||||
`${$application.repository.organization}/${$application.repository.name}`,
|
||||
@@ -61,15 +121,17 @@
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
loading.github = false;
|
||||
}
|
||||
}
|
||||
function modifyGithubAppConfig() {
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
const top = screen.height / 2 - 618 / 2;
|
||||
const newWindow = open(
|
||||
`https://github.com/apps/${
|
||||
import.meta.env.VITE_GITHUB_APP_NAME
|
||||
}/installations/new`,
|
||||
`https://github.com/apps/${dashify(
|
||||
import.meta.env.VITE_GITHUB_APP_NAME,
|
||||
)}/installations/new`,
|
||||
"Install App",
|
||||
"resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=" +
|
||||
top +
|
||||
@@ -80,7 +142,8 @@
|
||||
const timer = setInterval(async () => {
|
||||
if (newWindow.closed) {
|
||||
clearInterval(timer);
|
||||
if (!$isActive("/application/new")) {
|
||||
loading.github = true;
|
||||
if (!$activePage.new) {
|
||||
try {
|
||||
const config = await $fetch(`/api/v1/config`, {
|
||||
body: {
|
||||
@@ -97,37 +160,104 @@
|
||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||
}
|
||||
branches = [];
|
||||
repositories = [];
|
||||
$githubRepositories = [];
|
||||
await loadGithub();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !$activePage.new}
|
||||
<div class="min-h-full text-white">
|
||||
<div
|
||||
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
|
||||
>
|
||||
{$application.publish.domain
|
||||
? `${$application.publish.domain}${
|
||||
$application.publish.path !== "/" ? $application.publish.path : ""
|
||||
}`
|
||||
: "example.com"}
|
||||
<a
|
||||
target="_blank"
|
||||
class="icon mx-2"
|
||||
href="{'https://' +
|
||||
$application.publish.domain +
|
||||
$application.publish.path}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
></path>
|
||||
</svg></a
|
||||
>
|
||||
|
||||
<a
|
||||
target="_blank"
|
||||
class="icon"
|
||||
href="{`https://github.com/${$application.repository.organization}/${$application.repository.name}`}"
|
||||
>
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path
|
||||
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
||||
></path></svg
|
||||
></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $activePage.new}
|
||||
<div class="min-h-full text-white">
|
||||
<div
|
||||
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
|
||||
>
|
||||
New Application
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div in:fade="{{ duration: 100 }}">
|
||||
{#if !$session.githubAppToken}
|
||||
<Login />
|
||||
{:else}
|
||||
{#await loadGithub()}
|
||||
<Loading />
|
||||
<Loading github githubLoadingText="Loading repositories..." />
|
||||
{:then}
|
||||
<div
|
||||
class="text-center space-y-2 max-w-4xl mx-auto px-6"
|
||||
in:fade="{{ duration: 100 }}"
|
||||
>
|
||||
<Repositories
|
||||
bind:repositories
|
||||
on:loadBranches="{loadBranches}"
|
||||
on:modifyGithubAppConfig="{modifyGithubAppConfig}"
|
||||
/>
|
||||
{#if $application.repository.organization !== "new"}
|
||||
<Branches loading="{loading.branches}" branches="{branches}" />
|
||||
{/if}
|
||||
{#if loading.github}
|
||||
<Loading github githubLoadingText="Loading repositories..." />
|
||||
{:else}
|
||||
<div
|
||||
class="space-y-2 max-w-4xl mx-auto px-6"
|
||||
in:fade="{{ duration: 100 }}"
|
||||
>
|
||||
<Repositories
|
||||
on:loadBranches="{loadBranches}"
|
||||
on:modifyGithubAppConfig="{modifyGithubAppConfig}"
|
||||
/>
|
||||
{#if $application.repository.organization}
|
||||
<Branches loading="{loading.branches}" branches="{branches}" />
|
||||
{/if}
|
||||
|
||||
{#if $application.repository.branch}
|
||||
<Tabs />
|
||||
{/if}
|
||||
</div>
|
||||
{#if $application.repository.branch}
|
||||
<Tabs />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { isActive } from "@roxi/routify";
|
||||
import { application } from "@store";
|
||||
export let repositories;
|
||||
import { application, githubRepositories, activePage } from "@store";
|
||||
import Select from "svelte-select";
|
||||
function handleSelect(event) {
|
||||
$application.build.pack = 'static'
|
||||
$application.repository.id = parseInt(event.detail.value, 10);
|
||||
dispatch("loadBranches");
|
||||
}
|
||||
|
||||
let items = $githubRepositories.map(repo => ({
|
||||
label: `${repo.owner.login}/${repo.name}`,
|
||||
value: repo.id.toString(),
|
||||
}));
|
||||
|
||||
const selectedValue =
|
||||
!$activePage.new &&
|
||||
`${$application.repository.organization}/${$application.repository.name}`;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const loadBranches = () => dispatch("loadBranches");
|
||||
const modifyGithubAppConfig = () => dispatch("modifyGithubAppConfig");
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1">
|
||||
{#if repositories.length !== 0}
|
||||
<div class="grid grid-cols-1 pt-4">
|
||||
{#if $githubRepositories.length !== 0}
|
||||
<label for="repository">Organization / Repository</label>
|
||||
<div class="grid grid-cols-3">
|
||||
<!-- svelte-ignore a11y-no-onchange -->
|
||||
<select
|
||||
id="repository"
|
||||
class:cursor-not-allowed="{!$isActive('/application/new')}"
|
||||
class="col-span-2"
|
||||
bind:value="{$application.repository.id}"
|
||||
on:change="{loadBranches}"
|
||||
disabled="{!$isActive('/application/new')}"
|
||||
>
|
||||
<option selected disabled>Select a repository</option>
|
||||
{#each repositories as repo}
|
||||
<option value="{repo.id}" class="font-medium">
|
||||
{repo.owner.login}
|
||||
/
|
||||
{repo.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<div class="grid grid-cols-3 ">
|
||||
<div class="repository-select-search col-span-2">
|
||||
<Select
|
||||
isFocused="true"
|
||||
containerClasses="w-full border-none bg-transparent"
|
||||
on:select="{handleSelect}"
|
||||
selectedValue="{selectedValue}"
|
||||
isClearable="{false}"
|
||||
items="{items}"
|
||||
showIndicator="{$activePage.new}"
|
||||
noOptionsMessage="No Repositories found"
|
||||
placeholder="Select a Repository"
|
||||
isDisabled="{!$activePage.new}"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
|
||||
on:click="{modifyGithubAppConfig}">Configure on Github</button
|
||||
@@ -38,7 +46,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<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
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -1,44 +1,13 @@
|
||||
<script>
|
||||
import { redirect, isActive } 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 { redirect } from "@roxi/routify";
|
||||
import { onMount } from "svelte";
|
||||
import { toast } from "@zerodevx/svelte-toast";
|
||||
import templates from "../../../utils/templates";
|
||||
import { application, fetch, deployments, activePage } from "@store";
|
||||
import General from "./ActiveTab/General.svelte";
|
||||
import Secrets from "./ActiveTab/Secrets.svelte";
|
||||
import Loading from "../../Loading.svelte";
|
||||
|
||||
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 = {
|
||||
general: true,
|
||||
buildStep: false,
|
||||
@@ -54,44 +23,131 @@
|
||||
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>
|
||||
|
||||
<div class="block text-center py-4">
|
||||
<nav
|
||||
class="flex space-x-4 justify-center font-bold text-md text-white"
|
||||
aria-label="Tabs"
|
||||
>
|
||||
<div
|
||||
on:click="{() => activateTab('general')}"
|
||||
class:text-green-500="{activeTab.general}"
|
||||
class="px-3 py-2 cursor-pointer hover:text-green-500"
|
||||
{#await load()}
|
||||
<Loading github githubLoadingText="Scanning repository..." />
|
||||
{:then}
|
||||
<div class="block text-center py-8">
|
||||
<nav
|
||||
class="flex space-x-4 justify-center font-bold text-md text-white"
|
||||
aria-label="Tabs"
|
||||
>
|
||||
General
|
||||
</div>
|
||||
<div
|
||||
on:click="{() => activateTab('buildStep')}"
|
||||
class:text-green-500="{activeTab.buildStep}"
|
||||
class="px-3 py-2 cursor-pointer hover:text-green-500"
|
||||
>
|
||||
Build Step
|
||||
</div>
|
||||
<div
|
||||
on:click="{() => activateTab('secrets')}"
|
||||
class:text-green-500="{activeTab.secrets}"
|
||||
class="px-3 py-2 cursor-pointer hover:text-green-500"
|
||||
>
|
||||
Secrets
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="h-full">
|
||||
{#if activeTab.general}
|
||||
<General />
|
||||
{:else if activeTab.buildStep}
|
||||
<BuildStep />
|
||||
{:else if activeTab.secrets}
|
||||
<Secrets />
|
||||
{/if}
|
||||
<div
|
||||
on:click="{() => activateTab('general')}"
|
||||
class:text-green-500="{activeTab.general}"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
|
||||
>
|
||||
General
|
||||
</div>
|
||||
<div
|
||||
on:click="{() => activateTab('secrets')}"
|
||||
class:text-green-500="{activeTab.secrets}"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
|
||||
>
|
||||
Secrets
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<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 { fade } from "svelte/transition";
|
||||
import { toast } from "@zerodevx/svelte-toast";
|
||||
import MongoDb from "../SVGs/MongoDb.svelte";
|
||||
import Postgresql from "../SVGs/Postgresql.svelte";
|
||||
import Mysql from "../SVGs/Mysql.svelte";
|
||||
import CouchDb from "../SVGs/CouchDb.svelte";
|
||||
|
||||
let type;
|
||||
let defaultDatabaseName;
|
||||
@@ -15,7 +19,7 @@
|
||||
defaultDatabaseName,
|
||||
},
|
||||
});
|
||||
$dbInprogress = true
|
||||
$dbInprogress = true;
|
||||
toast.push("Database deployment queued.");
|
||||
$redirect(`/dashboard/databases`);
|
||||
} catch (error) {
|
||||
@@ -30,48 +34,65 @@
|
||||
>
|
||||
{#if $isActive("/database/new")}
|
||||
<div class="flex justify-center space-x-4 font-bold pb-6">
|
||||
<button
|
||||
class="button bg-gray-500 p-2 text-white hover:bg-green-600 cursor-pointer w-32"
|
||||
<div
|
||||
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-green-600 p-2 rounded bg-warmGray-800 w-32"
|
||||
class:border-green-600="{type === 'mongodb'}"
|
||||
on:click="{() => (type = 'mongodb')}"
|
||||
class:bg-green-600="{type === 'mongodb'}"
|
||||
>
|
||||
MongoDB
|
||||
</button>
|
||||
<button
|
||||
class="button bg-gray-500 p-2 text-white hover:bg-blue-600 cursor-pointer w-32"
|
||||
<div class="flex items-center justify-center my-2">
|
||||
<MongoDb customClass="w-6" />
|
||||
</div>
|
||||
<div class="text-white">MongoDB</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-red-600 p-2 rounded bg-warmGray-800 w-32"
|
||||
class:border-red-600="{type === 'couchdb'}"
|
||||
on:click="{() => (type = 'couchdb')}"
|
||||
>
|
||||
<div class="flex items-center justify-center my-2">
|
||||
<CouchDb customClass="w-12 text-red-600 fill-current" />
|
||||
</div>
|
||||
<div class="text-white">Couchdb</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-600 p-2 rounded bg-warmGray-800 w-32"
|
||||
class:border-blue-600="{type === 'postgresql'}"
|
||||
on:click="{() => (type = 'postgresql')}"
|
||||
class:bg-blue-600="{type === 'postgresql'}"
|
||||
>
|
||||
PostgreSQL
|
||||
</button>
|
||||
<button
|
||||
class="button bg-gray-500 p-2 text-white hover:bg-orange-600 cursor-pointer w-32"
|
||||
<div class="flex items-center justify-center my-2">
|
||||
<Postgresql customClass="w-12" />
|
||||
</div>
|
||||
<div class="text-white">PostgreSQL</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-orange-600 p-2 rounded bg-warmGray-800 w-32"
|
||||
class:border-orange-600="{type === 'mysql'}"
|
||||
on:click="{() => (type = 'mysql')}"
|
||||
class:bg-orange-600="{type === 'mysql'}"
|
||||
>
|
||||
MySQL
|
||||
</button>
|
||||
<button
|
||||
class="button bg-gray-500 p-2 text-white hover:bg-red-600 cursor-pointer w-32"
|
||||
on:click="{() => (type = 'couchdb')}"
|
||||
class:bg-red-600="{type === 'couchdb'}"
|
||||
>
|
||||
Couchdb
|
||||
</button>
|
||||
<div class="flex items-center justify-center">
|
||||
<Mysql customClass="w-10" />
|
||||
</div>
|
||||
<div class="text-white">MySQL</div>
|
||||
</div>
|
||||
|
||||
<!-- <button
|
||||
class="button bg-gray-500 p-2 text-white hover:bg-yellow-500 cursor-pointer w-32"
|
||||
on:click="{() => (type = 'clickhouse')}"
|
||||
class:bg-yellow-500="{type === 'clickhouse'}"
|
||||
>
|
||||
Clickhouse
|
||||
</button> -->
|
||||
</div>
|
||||
{#if type}
|
||||
<div>
|
||||
<div
|
||||
class="grid grid-rows-1 justify-center items-center text-center pb-5"
|
||||
>
|
||||
<label for="defaultDB">Default database</label>
|
||||
<input
|
||||
id="defaultDB"
|
||||
class="w-64"
|
||||
placeholder="random"
|
||||
bind:value="{defaultDatabaseName}"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center space-x-4 items-center">
|
||||
<label for="defaultDB">Default database</label>
|
||||
<input
|
||||
id="defaultDB"
|
||||
class="w-64"
|
||||
placeholder="random"
|
||||
bind:value="{defaultDatabaseName}"
|
||||
/>
|
||||
|
||||
<button
|
||||
class:bg-green-600="{type === 'mongodb'}"
|
||||
class:hover:bg-green-500="{type === 'mongodb'}"
|
||||
@@ -81,6 +102,8 @@
|
||||
class:hover:bg-orange-500="{type === 'mysql'}"
|
||||
class:bg-red-600="{type === 'couchdb'}"
|
||||
class:hover:bg-red-500="{type === 'couchdb'}"
|
||||
class:bg-yellow-500="{type === 'clickhouse'}"
|
||||
class:hover:bg-yellow-400="{type === 'clickhouse'}"
|
||||
class="button p-2 w-32 text-white"
|
||||
on:click="{deploy}">Deploy</button
|
||||
>
|
||||
|
||||
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>
|
||||
|
||||
<script>
|
||||
export let github = false;
|
||||
export let githubLoadingText = "Loading GitHub...";
|
||||
export let fullscreen = true;
|
||||
</script>
|
||||
|
||||
{#if fullscreen}
|
||||
<div class="fixed top-0 flex flex-wrap content-center h-full w-full">
|
||||
<span class="loader"></span>
|
||||
</div>
|
||||
{#if github}
|
||||
<div class="fixed left-0 top-0 flex flex-wrap content-center h-full w-full">
|
||||
<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}
|
||||
|
||||
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);
|
||||
--toastProgressBackground: transparent;
|
||||
--toastFont: 'Inter';
|
||||
}
|
||||
|
||||
.border-gradient {
|
||||
border-bottom: 2px solid transparent;
|
||||
border-image: linear-gradient(0.25turn, rgba(255, 249, 34), rgba(255, 0, 128), rgba(56, 2, 155, 0));
|
||||
border-image-slice: 1;
|
||||
}
|
||||
.border-gradient-full {
|
||||
border: 4px solid transparent;
|
||||
border-image: linear-gradient(0.25turn, rgba(255, 249, 34), rgba(255, 0, 128), rgba(56, 2, 155, 0));
|
||||
border-image-slice: 1;
|
||||
}
|
||||
|
||||
[aria-label][role~="tooltip"]::after {
|
||||
background: rgba(41, 37, 36, 0.9);
|
||||
color: white;
|
||||
font-family: 'Inter';
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
[role~="tooltip"][data-microtip-position|="bottom"]::before {
|
||||
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
|
||||
}
|
||||
|
||||
[role~="tooltip"][data-microtip-position|="top"]::before {
|
||||
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
|
||||
}
|
||||
[role~="tooltip"][data-microtip-position="right"]::before {
|
||||
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
|
||||
}
|
||||
|
||||
[role~="tooltip"][data-microtip-position="left"]::before {
|
||||
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
|
||||
}
|
||||
@@ -2,33 +2,80 @@
|
||||
.min-w-4rem {
|
||||
min-width: 4rem;
|
||||
}
|
||||
.main {
|
||||
width: calc(100% - 4rem);
|
||||
margin-left: 4rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { url, goto, route, isActive, redirect } from "@roxi/routify/runtime";
|
||||
import {
|
||||
loggedIn,
|
||||
session,
|
||||
fetch,
|
||||
deployments,
|
||||
application,
|
||||
initConf,
|
||||
} from "@store";
|
||||
import { goto, route, isChangingPage } from "@roxi/routify/runtime";
|
||||
import { loggedIn, session, fetch, deployments, activePage } from "@store";
|
||||
import { toast } from "@zerodevx/svelte-toast";
|
||||
import packageJson from "../../package.json";
|
||||
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 upgradeDisabled = false;
|
||||
let upgradeDone = false;
|
||||
let latest = {};
|
||||
let showAck = false;
|
||||
const branch =
|
||||
process.env.NODE_ENV === "production" &&
|
||||
window.location.hostname !== "test.andrasbacsai.dev"
|
||||
? "main"
|
||||
: "next";
|
||||
|
||||
$: if ($isChangingPage) {
|
||||
const path = window.location.pathname;
|
||||
if (path === "/dashboard/applications" || path.match(/\/application/)) {
|
||||
$activePage.mainmenu = "applications";
|
||||
} else if (path === "/dashboard/databases" || path.match(/\/database/)) {
|
||||
$activePage.mainmenu = "databases";
|
||||
} else if (path === "/dashboard/services" || path.match(/\/service/)) {
|
||||
$activePage.mainmenu = "services";
|
||||
} else if (path === "/settings") {
|
||||
$activePage.mainmenu = "settings";
|
||||
} else {
|
||||
$activePage.mainmenu = null;
|
||||
}
|
||||
|
||||
if (path.match(/\/application\/.*\/logs/)) {
|
||||
$activePage.application = "logs";
|
||||
$activePage.new = false;
|
||||
} else if (path === "/application/new") {
|
||||
$activePage.application = "configuration";
|
||||
$activePage.new = true;
|
||||
} else if (path.match(/\/application\/.*\/configuration/)) {
|
||||
$activePage.application = "configuration";
|
||||
$activePage.new = false;
|
||||
} else {
|
||||
$activePage.application = null;
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
upgradeAvailable = await checkUpgrade();
|
||||
if ($session.token) {
|
||||
upgradeAvailable = await checkUpgrade();
|
||||
if (!localStorage.getItem("automaticErrorReportsAck")) {
|
||||
showAck = true;
|
||||
if (latest?.coolify[branch]?.settings?.sendErrors) {
|
||||
const settings = {
|
||||
sendErrors: true,
|
||||
};
|
||||
await $fetch("/api/v1/settings", {
|
||||
body: {
|
||||
...settings,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${$session.token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
function ackError() {
|
||||
localStorage.setItem("automaticErrorReportsAck", "true");
|
||||
showAck = false;
|
||||
}
|
||||
async function verifyToken() {
|
||||
if ($session.token) {
|
||||
try {
|
||||
@@ -74,184 +121,233 @@
|
||||
}
|
||||
async function checkUpgrade() {
|
||||
latest = await window
|
||||
.fetch(
|
||||
"https://raw.githubusercontent.com/coollabsio/coolify/main/package.json",
|
||||
{ cache: "no-cache" },
|
||||
)
|
||||
.fetch(`https://get.coollabs.io/version.json`, {
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then(r => r.json());
|
||||
if (
|
||||
latest.version.split(".").join("") >
|
||||
packageJson.version.split(".").join("")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return compareVersions(
|
||||
latest.coolify[branch].version,
|
||||
packageJson.version,
|
||||
) === 1
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await verifyToken() then notUsed}
|
||||
{#if showAck}
|
||||
<div
|
||||
class="p-2 fixed top-0 right-0 z-50 w-64 m-2 rounded border-gradient-full bg-black"
|
||||
>
|
||||
<div class="text-white text-xs space-y-2 text-justify font-medium">
|
||||
<div>
|
||||
We implemented an automatic error reporting feature, which is enabled
|
||||
by default.
|
||||
</div>
|
||||
<div>
|
||||
Why? Because we would like to hunt down bugs faster and easier.
|
||||
</div>
|
||||
<div class="py-5">
|
||||
If you do not like it, you can turn it off in the <button
|
||||
class="underline font-bold"
|
||||
on:click="{$goto('/settings')}">Settings menu</button
|
||||
>.
|
||||
</div>
|
||||
<button
|
||||
class="button p-2 bg-warmGray-800 w-full text-center hover:bg-warmGray-700"
|
||||
on:click="{ackError}">OK</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $route.path !== "/index"}
|
||||
<nav
|
||||
class="w-16 bg-warmGray-800 text-white top-0 left-0 fixed min-w-4rem min-h-screen"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col w-full h-screen items-center space-y-4 transition-all duration-100"
|
||||
class:border-green-500="{$isActive('/dashboard/applications')}"
|
||||
class:border-purple-500="{$isActive('/dashboard/databases')}"
|
||||
class="flex flex-col w-full h-screen items-center transition-all duration-100"
|
||||
class:border-green-500="{$activePage.mainmenu === 'applications'}"
|
||||
class:border-purple-500="{$activePage.mainmenu === 'databases'}"
|
||||
>
|
||||
<img class="w-10 pt-4 pb-4" src="/favicon.png" alt="coolLabs logo" />
|
||||
<div
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 my-4 transition-all duration-100 cursor-pointer"
|
||||
on:click="{() => $goto('/dashboard/applications')}"
|
||||
class:text-green-500="{$isActive('/dashboard/applications') ||
|
||||
$isActive('/application')}"
|
||||
class:bg-warmGray-700="{$isActive('/dashboard/applications') ||
|
||||
$isActive('/application')}"
|
||||
>
|
||||
<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
|
||||
<Tooltip position="right" label="Applications">
|
||||
<div
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 mt-4 transition-all duration-100 cursor-pointer"
|
||||
on:click="{() => $goto('/dashboard/applications')}"
|
||||
class:text-green-500="{$activePage.mainmenu === 'applications'}"
|
||||
class:bg-warmGray-700="{$activePage.mainmenu === 'applications'}"
|
||||
>
|
||||
</div>
|
||||
<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="{$isActive('/dashboard/databases') ||
|
||||
$isActive('/database')}"
|
||||
class:bg-warmGray-700="{$isActive('/dashboard/databases') ||
|
||||
$isActive('/database')}"
|
||||
>
|
||||
<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"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
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>
|
||||
><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>
|
||||
</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>
|
||||
<button
|
||||
title="Settings"
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-yellow-500 my-4 transition-all duration-100 cursor-pointer"
|
||||
class:text-yellow-500="{$isActive('/settings')}"
|
||||
class:bg-warmGray-700="{$isActive('/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"
|
||||
<Tooltip position="right" label="Settings">
|
||||
<button
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-yellow-500 transition-all duration-100 cursor-pointer"
|
||||
class:text-yellow-500="{$activePage.mainmenu === 'settings'}"
|
||||
class:bg-warmGray-700="{$activePage.mainmenu === 'settings'}"
|
||||
on:click="{() => $goto('/settings')}"
|
||||
>
|
||||
<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-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>
|
||||
<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>
|
||||
><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>
|
||||
</Tooltip>
|
||||
<div
|
||||
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>
|
||||
</nav>
|
||||
{/if}
|
||||
{#if upgradeAvailable}
|
||||
<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></div>
|
||||
<div class="flex-1"></div>
|
||||
{#if !upgradeDisabled}
|
||||
<button
|
||||
class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 font-bold text-xs rounded px-2 py-2"
|
||||
disabled="{upgradeDisabled}"
|
||||
on:click="{upgrade}"
|
||||
>New version available. <br>Click here to upgrade!</button
|
||||
>
|
||||
{:else if upgradeDone}
|
||||
<button
|
||||
use:reloadInAMin
|
||||
class="font-bold text-xs rounded px-2 cursor-not-allowed"
|
||||
disabled="{upgradeDisabled}"
|
||||
>Upgrade done. 🎉 Automatically reloading in 30s.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
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
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if !upgradeDisabled}
|
||||
<button
|
||||
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}"
|
||||
on:click="{upgrade}"
|
||||
>New version available, <br />click here to upgrade!</button
|
||||
>
|
||||
{:else if upgradeDone}
|
||||
<button
|
||||
use:reloadInAMin
|
||||
class="font-bold text-xs rounded px-2 cursor-not-allowed"
|
||||
disabled="{upgradeDisabled}"
|
||||
>Upgrade done. 🎉 Automatically reloading in 30s.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
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
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</footer>
|
||||
{/if}
|
||||
<main class:main={$route.path !== "/index"}>
|
||||
<slot />
|
||||
{/if}
|
||||
<main class:main="{$route.path !== '/index'}">
|
||||
<slot />
|
||||
</main>
|
||||
{:catch test}
|
||||
{$goto("/index")}
|
||||
|
||||
@@ -1,62 +1,5 @@
|
||||
<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 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>
|
||||
|
||||
<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 />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { params } from "@roxi/routify";
|
||||
import { params, redirect } from "@roxi/routify";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import { fetch } from "@store";
|
||||
@@ -15,13 +15,18 @@
|
||||
});
|
||||
|
||||
async function loadLogs() {
|
||||
const { events, progress } = await $fetch(
|
||||
try {
|
||||
const { events, progress } = await $fetch(
|
||||
`/api/v1/application/deploy/logs/${$params.deployId}`,
|
||||
);
|
||||
logs = [...events];
|
||||
if (progress === "done" || progress === "failed") {
|
||||
clearInterval(loadLogsInterval);
|
||||
}
|
||||
} catch(error) {
|
||||
$redirect('/dashboard')
|
||||
}
|
||||
|
||||
}
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
@@ -38,12 +43,12 @@
|
||||
<Loading />
|
||||
{:then}
|
||||
<div
|
||||
class="text-center space-y-2 max-w-7xl mx-auto px-6"
|
||||
class="text-center px-6"
|
||||
in:fade="{{ duration: 100 }}"
|
||||
>
|
||||
<div class="max-w-4xl mx-auto" in:fade="{{ duration: 100 }}">
|
||||
<div in:fade="{{ duration: 100 }}">
|
||||
<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}
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
|
||||
@@ -59,17 +59,17 @@
|
||||
<Loading />
|
||||
{:then}
|
||||
<div
|
||||
class="text-center space-y-2 max-w-7xl mx-auto px-6"
|
||||
class="text-center px-6"
|
||||
in:fade="{{ duration: 100 }}"
|
||||
>
|
||||
<div class="flex pt-2 space-x-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="font-bold text-left pb-2 text-xl">Application logs</div>
|
||||
{#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}
|
||||
<pre
|
||||
class="text-left font-mono text-xs font-medium rounded bg-warmGray-800 text-white p-4 whitespace-pre-wrap w-full">
|
||||
<pre
|
||||
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}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { fade } from "svelte/transition";
|
||||
import { application } from "@store";
|
||||
import Configuration from "../../../../../components/Application/Configuration/Configuration.svelte";
|
||||
</script>
|
||||
@@ -11,7 +10,7 @@
|
||||
Overview of
|
||||
<a
|
||||
target="_blank"
|
||||
class="text-green-500 hover:underline cursor-pointer px-2"
|
||||
class="hover:underline cursor-pointer px-2"
|
||||
href="{'https://' +
|
||||
$application.publish.domain +
|
||||
$application.publish.path}">{$application.publish.domain}</a
|
||||
|
||||
@@ -1,215 +1,75 @@
|
||||
<script>
|
||||
import { params, goto, redirect, isActive } from "@roxi/routify";
|
||||
import { application, fetch, initialApplication, initConf } from "@store";
|
||||
import { params, redirect } from "@roxi/routify";
|
||||
import {
|
||||
application,
|
||||
fetch,
|
||||
initialApplication,
|
||||
initConf,
|
||||
deployments,
|
||||
activePage,
|
||||
} from "@store";
|
||||
import { onDestroy } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import Loading from "../../components/Loading.svelte";
|
||||
import { toast } from "@zerodevx/svelte-toast";
|
||||
import Navbar from "../../components/Application/Navbar.svelte";
|
||||
|
||||
$application.repository.organization = $params.organization;
|
||||
$application.repository.name = $params.name;
|
||||
$application.repository.branch = $params.branch;
|
||||
|
||||
async function setConfiguration() {
|
||||
try {
|
||||
const config = await $fetch(`/api/v1/config`, {
|
||||
body: {
|
||||
name: $application.repository.name,
|
||||
organization: $application.repository.organization,
|
||||
branch: $application.repository.branch,
|
||||
},
|
||||
});
|
||||
$application = { ...config };
|
||||
$initConf = JSON.parse(JSON.stringify($application));
|
||||
} catch (error) {
|
||||
toast.push("Configuration not found.");
|
||||
$redirect("/dashboard/applications");
|
||||
}
|
||||
}
|
||||
async function loadConfiguration() {
|
||||
if (!$isActive("/application/new")) {
|
||||
try {
|
||||
const config = await $fetch(`/api/v1/config`, {
|
||||
body: {
|
||||
name: $application.repository.name,
|
||||
organization: $application.repository.organization,
|
||||
branch: $application.repository.branch,
|
||||
},
|
||||
if (!$activePage.new) {
|
||||
if ($deployments.length === 0) {
|
||||
await setConfiguration();
|
||||
} else {
|
||||
const found = $deployments.applications.deployed.find(app => {
|
||||
const { organization, name, branch } = app.configuration;
|
||||
if (
|
||||
organization === $application.repository.organization &&
|
||||
name === $application.repository.name &&
|
||||
branch === $application.repository.branch
|
||||
) {
|
||||
return app;
|
||||
}
|
||||
});
|
||||
$application = { ...config };
|
||||
$initConf = JSON.parse(JSON.stringify($application));
|
||||
} catch (error) {
|
||||
toast.push("Configuration not found.");
|
||||
$redirect("/dashboard/applications");
|
||||
if (found) {
|
||||
$application = { ...found.configuration };
|
||||
$initConf = JSON.parse(JSON.stringify($application));
|
||||
} else {
|
||||
await setConfiguration();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||
}
|
||||
}
|
||||
async function removeApplication() {
|
||||
await $fetch(`/api/v1/application/remove`, {
|
||||
body: {
|
||||
organization: $params.organization,
|
||||
name: $params.name,
|
||||
branch: $params.branch,
|
||||
},
|
||||
});
|
||||
|
||||
toast.push("Application removed.");
|
||||
$redirect(`/dashboard/applications`);
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||
});
|
||||
|
||||
async function deploy() {
|
||||
try {
|
||||
toast.push("Checking inputs.");
|
||||
await $fetch(`/api/v1/application/check`, {
|
||||
body: $application,
|
||||
});
|
||||
const { nickname, name } = await $fetch(`/api/v1/application/deploy`, {
|
||||
body: $application,
|
||||
});
|
||||
$application.general.nickname = nickname;
|
||||
$application.build.container.name = name;
|
||||
$initConf = JSON.parse(JSON.stringify($application));
|
||||
toast.push("Application deployment queued.");
|
||||
$redirect(
|
||||
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.push(error.error ? error.error : "Ooops something went wrong.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await loadConfiguration()}
|
||||
<Loading />
|
||||
{:then}
|
||||
<nav
|
||||
class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4"
|
||||
>
|
||||
<button
|
||||
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>
|
||||
<Navbar />
|
||||
<div class="text-white">
|
||||
<slot />
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
@@ -2,12 +2,4 @@
|
||||
import Configuration from "../../components/Application/Configuration/Configuration.svelte";
|
||||
</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 />
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script>
|
||||
import { fetch, deployments } from "@store";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import { goto, isActive } from "@roxi/routify/runtime";
|
||||
import { toast } from "@zerodevx/svelte-toast";
|
||||
let loadDashboardInterval = null;
|
||||
|
||||
|
||||
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