mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 12:14:07 +00:00
v1.0.12 - Sveltekit migration (#44)
Changed the whole tech stack to SvelteKit which means: - Typescript - SSR - No fastify :( - Beta, but it's fine! Other changes: - Tailwind -> Tailwind JIT - A lot more
This commit is contained in:
1 parent
cccb9a5fec
commit
23a4ebb74a
227 files changed
+7774
-11326
No files matched your search
+5
-2
@@ -1,4 +1,7 @@
|
|||||||
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
/.svelte
|
||||||
.routify
|
/build
|
||||||
|
/functions
|
||||||
.pnpm-store
|
.pnpm-store
|
||||||
|
.pnpm-debug.log
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||||
|
plugins: ['svelte3', '@typescript-eslint'],
|
||||||
|
ignorePatterns: ['*.cjs'],
|
||||||
|
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||||
|
settings: {
|
||||||
|
'svelte3/typescript': () => require('typescript')
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2019
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
}
|
||||||
|
};
|
||||||
+7
-9
@@ -1,11 +1,9 @@
|
|||||||
.vscode
|
/node_modules
|
||||||
.idea
|
/.svelte
|
||||||
node_modules
|
/.svelte-kit
|
||||||
dist
|
/.pnpm-store
|
||||||
dist-ssr
|
/build
|
||||||
.routify
|
/functions
|
||||||
.env
|
.env
|
||||||
yarn-error.log
|
.DS_Store
|
||||||
api/development/console.log
|
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
.pnpm-store
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.svelte/**
|
||||||
|
static/**
|
||||||
|
build/**
|
||||||
|
node_modules/**
|
||||||
|
.svelte-kit/**
|
||||||
+5
-13
@@ -1,14 +1,6 @@
|
|||||||
{
|
{
|
||||||
"arrowParens": "avoid",
|
"useTabs": true,
|
||||||
"bracketSpacing": true,
|
"singleQuote": true,
|
||||||
"printWidth": 80,
|
"trailingComma": "none",
|
||||||
"semi": true,
|
"printWidth": 100
|
||||||
"singleQuote": false,
|
}
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"svelteSortOrder" : "styles-scripts-markup",
|
|
||||||
"svelteStrictMode": true,
|
|
||||||
"svelteBracketNewLine": true,
|
|
||||||
"svelteAllowShorthand": true,
|
|
||||||
"plugins": ["prettier-plugin-svelte"]
|
|
||||||
}
|
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
module.exports = async function (fastify, opts) {
|
|
||||||
// Private routes
|
|
||||||
fastify.register(async function (server) {
|
|
||||||
server.register(require('./plugins/authentication'))
|
|
||||||
server.register(require('./routes/v1/upgrade'), { prefix: '/upgrade' })
|
|
||||||
server.register(require('./routes/v1/settings'), { prefix: '/settings' })
|
|
||||||
server.register(require('./routes/v1/dashboard'), { prefix: '/dashboard' })
|
|
||||||
server.register(require('./routes/v1/config'), { prefix: '/config' })
|
|
||||||
server.register(require('./routes/v1/application/remove'), { prefix: '/application/remove' })
|
|
||||||
server.register(require('./routes/v1/application/logs'), { prefix: '/application/logs' })
|
|
||||||
server.register(require('./routes/v1/application/check'), { prefix: '/application/check' })
|
|
||||||
server.register(require('./routes/v1/application/deploy'), { prefix: '/application/deploy' })
|
|
||||||
server.register(require('./routes/v1/application/deploy/logs'), { prefix: '/application/deploy/logs' })
|
|
||||||
server.register(require('./routes/v1/databases'), { prefix: '/databases' })
|
|
||||||
server.register(require('./routes/v1/services'), { prefix: '/services' })
|
|
||||||
server.register(require('./routes/v1/services/deploy'), { prefix: '/services/deploy' })
|
|
||||||
server.register(require('./routes/v1/server'), { prefix: '/server' })
|
|
||||||
})
|
|
||||||
// Public routes
|
|
||||||
fastify.register(require('./routes/v1/verify'), { prefix: '/verify' })
|
|
||||||
fastify.register(require('./routes/v1/login/github'), {
|
|
||||||
prefix: '/login/github'
|
|
||||||
})
|
|
||||||
fastify.register(require('./routes/v1/webhooks/deploy'), {
|
|
||||||
prefix: '/webhooks/deploy'
|
|
||||||
})
|
|
||||||
fastify.register(require('./routes/v1/undead'), {
|
|
||||||
prefix: '/undead'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
const path = `${configuration.general.workdir}/${configuration.build.directory ? configuration.build.directory : ''}`
|
|
||||||
if (fs.stat(`${path}/Dockerfile`)) {
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: path },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
} else {
|
|
||||||
throw new Error('No custom dockerfile found.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
|
|
||||||
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
|
|
||||||
const publishStaticDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM nginx:stable-alpine',
|
|
||||||
'COPY nginx.conf /etc/nginx/nginx.conf',
|
|
||||||
'WORKDIR /usr/share/nginx/html',
|
|
||||||
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
|
|
||||||
'EXPOSE 80',
|
|
||||||
'CMD ["nginx", "-g", "daemon off;"]'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
await buildImage(configuration, true)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { streamEvents, docker } = require('../libs/docker')
|
|
||||||
const buildImageNodeDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM node:lts',
|
|
||||||
'WORKDIR /usr/src/app',
|
|
||||||
`COPY ${configuration.build.directory}/package*.json ./`,
|
|
||||||
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
|
|
||||||
`COPY ./${configuration.build.directory} ./`,
|
|
||||||
`RUN ${configuration.build.command.build}`
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
async function buildImage (configuration, cacheBuild) {
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, buildImageNodeDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${cacheBuild ? `${configuration.build.container.tag}-cache` : configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
buildImage
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const Static = require('./static')
|
|
||||||
const react = require('./react')
|
|
||||||
const nextjs = require('./nextjs')
|
|
||||||
const nuxtjs = require('./nuxtjs')
|
|
||||||
const gatsby = require('./gatsby')
|
|
||||||
const vuejs = require('./vuejs')
|
|
||||||
const svelte = require('./svelte')
|
|
||||||
const nodejs = require('./nodejs')
|
|
||||||
const php = require('./php')
|
|
||||||
const docker = require('./docker')
|
|
||||||
const rust = require('./rust')
|
|
||||||
|
|
||||||
module.exports = { static: Static, nodejs, php, docker, rust, react, vuejs, nextjs, nuxtjs, svelte, gatsby }
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
|
|
||||||
const publishNodejsDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM node:lts',
|
|
||||||
'WORKDIR /usr/src/app',
|
|
||||||
configuration.build.command.build
|
|
||||||
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
|
|
||||||
: `
|
|
||||||
COPY ${configuration.build.directory}/package*.json ./
|
|
||||||
RUN ${configuration.build.command.installation}
|
|
||||||
COPY ./${configuration.build.directory} ./`,
|
|
||||||
`EXPOSE ${configuration.publish.port}`,
|
|
||||||
'CMD [ "yarn", "start" ]'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
await buildImage(configuration)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishNodejsDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
|
|
||||||
const publishNodejsDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM node:lts',
|
|
||||||
'WORKDIR /usr/src/app',
|
|
||||||
configuration.build.command.build
|
|
||||||
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
|
|
||||||
: `
|
|
||||||
COPY ${configuration.build.directory}/package*.json ./
|
|
||||||
RUN ${configuration.build.command.installation}
|
|
||||||
COPY ./${configuration.build.directory} ./`,
|
|
||||||
`EXPOSE ${configuration.publish.port}`,
|
|
||||||
'CMD [ "yarn", "start" ]'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
if (configuration.build.command.build) await buildImage(configuration)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishNodejsDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
|
|
||||||
const publishNodejsDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM node:lts',
|
|
||||||
'WORKDIR /usr/src/app',
|
|
||||||
configuration.build.command.build
|
|
||||||
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
|
|
||||||
: `
|
|
||||||
COPY ${configuration.build.directory}/package*.json ./
|
|
||||||
RUN ${configuration.build.command.installation}
|
|
||||||
COPY ./${configuration.build.directory} ./`,
|
|
||||||
`EXPOSE ${configuration.publish.port}`,
|
|
||||||
'CMD [ "yarn", "start" ]'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
await buildImage(configuration)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishNodejsDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
|
|
||||||
const publishPHPDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM php:apache',
|
|
||||||
'RUN a2enmod rewrite',
|
|
||||||
'WORKDIR /usr/src/app',
|
|
||||||
`COPY ./${configuration.build.directory} /var/www/html`,
|
|
||||||
'EXPOSE 80',
|
|
||||||
' CMD ["apache2-foreground"]'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishPHPDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
|
|
||||||
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
|
|
||||||
const publishStaticDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM nginx:stable-alpine',
|
|
||||||
'COPY nginx.conf /etc/nginx/nginx.conf',
|
|
||||||
'WORKDIR /usr/share/nginx/html',
|
|
||||||
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
|
|
||||||
'EXPOSE 80',
|
|
||||||
'CMD ["nginx", "-g", "daemon off;"]'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
await buildImage(configuration, true)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
const { execShellAsync } = require('../../libs/common')
|
|
||||||
const TOML = require('@iarna/toml')
|
|
||||||
|
|
||||||
const publishRustDocker = (configuration, custom) => {
|
|
||||||
return [
|
|
||||||
'FROM rust:latest',
|
|
||||||
'WORKDIR /app',
|
|
||||||
`COPY --from=${configuration.build.container.name}:cache /app/target target`,
|
|
||||||
`COPY --from=${configuration.build.container.name}:cache /usr/local/cargo /usr/local/cargo`,
|
|
||||||
'COPY . .',
|
|
||||||
`RUN cargo build --release --bin ${custom.name}`,
|
|
||||||
'FROM debian:buster-slim',
|
|
||||||
'WORKDIR /app',
|
|
||||||
'RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*',
|
|
||||||
'RUN update-ca-certificates',
|
|
||||||
`COPY --from=${configuration.build.container.name}:cache /app/target/release/${custom.name} ${custom.name}`,
|
|
||||||
`EXPOSE ${configuration.publish.port}`,
|
|
||||||
`CMD ["/app/${custom.name}"]`
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheRustDocker = (configuration, custom) => {
|
|
||||||
return [
|
|
||||||
`FROM rust:latest AS planner-${configuration.build.container.name}`,
|
|
||||||
'WORKDIR /app',
|
|
||||||
'RUN cargo install cargo-chef',
|
|
||||||
'COPY . .',
|
|
||||||
'RUN cargo chef prepare --recipe-path recipe.json',
|
|
||||||
'FROM rust:latest',
|
|
||||||
'WORKDIR /app',
|
|
||||||
'RUN cargo install cargo-chef',
|
|
||||||
`COPY --from=planner-${configuration.build.container.name} /app/recipe.json recipe.json`,
|
|
||||||
'RUN cargo chef cook --release --recipe-path recipe.json'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
const cargoToml = await execShellAsync(`cat ${configuration.general.workdir}/Cargo.toml`)
|
|
||||||
const parsedToml = TOML.parse(cargoToml)
|
|
||||||
const custom = {
|
|
||||||
name: parsedToml.package.name
|
|
||||||
}
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, cacheRustDocker(configuration, custom))
|
|
||||||
|
|
||||||
let stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:cache` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishRustDocker(configuration, custom))
|
|
||||||
|
|
||||||
stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
|
|
||||||
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
|
|
||||||
const publishStaticDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM nginx:stable-alpine',
|
|
||||||
'COPY nginx.conf /etc/nginx/nginx.conf',
|
|
||||||
'WORKDIR /usr/share/nginx/html',
|
|
||||||
configuration.build.command.build
|
|
||||||
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`
|
|
||||||
: `COPY ./${configuration.build.directory} ./`,
|
|
||||||
'EXPOSE 80',
|
|
||||||
'CMD ["nginx", "-g", "daemon off;"]'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
if (configuration.build.command.build) await buildImage(configuration, true)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
|
|
||||||
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
|
|
||||||
const publishStaticDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM nginx:stable-alpine',
|
|
||||||
'COPY nginx.conf /etc/nginx/nginx.conf',
|
|
||||||
'WORKDIR /usr/share/nginx/html',
|
|
||||||
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
|
|
||||||
'EXPOSE 80',
|
|
||||||
'CMD ["nginx", "-g", "daemon off;"]'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
await buildImage(configuration, true)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
const { buildImage } = require('../helpers')
|
|
||||||
const { streamEvents, docker } = require('../../libs/docker')
|
|
||||||
|
|
||||||
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
|
|
||||||
const publishStaticDocker = (configuration) => {
|
|
||||||
return [
|
|
||||||
'FROM nginx:stable-alpine',
|
|
||||||
'COPY nginx.conf /etc/nginx/nginx.conf',
|
|
||||||
'WORKDIR /usr/share/nginx/html',
|
|
||||||
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
|
|
||||||
'EXPOSE 80',
|
|
||||||
'CMD ["nginx", "-g", "daemon off;"]'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
await buildImage(configuration, true)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration))
|
|
||||||
const stream = await docker.engine.buildImage(
|
|
||||||
{ src: ['.'], context: configuration.general.workdir },
|
|
||||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
|
||||||
)
|
|
||||||
await streamEvents(stream, configuration)
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
const mongoose = require('mongoose')
|
|
||||||
const { MongoMemoryServer } = require('mongodb-memory-server-core')
|
|
||||||
|
|
||||||
const mongoServer = new MongoMemoryServer({
|
|
||||||
instance: {
|
|
||||||
port: 27017,
|
|
||||||
dbName: 'coolify',
|
|
||||||
storageEngine: 'wiredTiger'
|
|
||||||
},
|
|
||||||
binary: {
|
|
||||||
version: '4.4.3'
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mongoose.Promise = Promise
|
|
||||||
mongoServer.getUri().then((mongoUri) => {
|
|
||||||
const mongooseOpts = {
|
|
||||||
useNewUrlParser: true,
|
|
||||||
useUnifiedTopology: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mongoose.connect(mongoUri, mongooseOpts)
|
|
||||||
|
|
||||||
mongoose.connection.on('error', (e) => {
|
|
||||||
if (e.message.code === 'ETIMEDOUT') {
|
|
||||||
console.log(e)
|
|
||||||
mongoose.connect(mongoUri, mongooseOpts)
|
|
||||||
}
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
mongoose.connection.once('open', () => {
|
|
||||||
console.log(`Started in-memory mongodb ${mongoUri}`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const packs = require('../../../buildPacks')
|
|
||||||
const { saveAppLog } = require('../../logging')
|
|
||||||
const Deployment = require('../../../models/Deployment')
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
const { id, organization, name, branch } = configuration.repository
|
|
||||||
const { domain } = configuration.publish
|
|
||||||
const deployId = configuration.general.deployId
|
|
||||||
|
|
||||||
const execute = packs[configuration.build.pack]
|
|
||||||
if (execute) {
|
|
||||||
await Deployment.findOneAndUpdate(
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain },
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'inprogress' })
|
|
||||||
await saveAppLog('### Building application.', configuration)
|
|
||||||
await execute(configuration)
|
|
||||||
await saveAppLog('### Building done.', configuration)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await Deployment.findOneAndUpdate(
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain },
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
|
||||||
} catch (error) {
|
|
||||||
// Hmm.
|
|
||||||
}
|
|
||||||
throw new Error('No buildpack found.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
const { docker } = require('../../docker')
|
|
||||||
const { execShellAsync } = require('../../common')
|
|
||||||
const Deployment = require('../../../models/Deployment')
|
|
||||||
|
|
||||||
async function purgeImagesContainers (configuration, deleteAll = false) {
|
|
||||||
const { name, tag } = configuration.build.container
|
|
||||||
await execShellAsync('docker container prune -f')
|
|
||||||
if (deleteAll) {
|
|
||||||
const IDsToDelete = (await execShellAsync(`docker images ls --filter=reference='${name}' --format '{{json .ID }}'`)).trim().replace(/"/g, '').split('\n')
|
|
||||||
if (IDsToDelete.length > 0) await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`)
|
|
||||||
} else {
|
|
||||||
const IDsToDelete = (await execShellAsync(`docker images ls --filter=reference='${name}' --filter=before='${name}:${tag}' --format '{{json .ID }}'`)).trim().replace(/"/g, '').split('\n')
|
|
||||||
if (IDsToDelete.length > 1) await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`)
|
|
||||||
}
|
|
||||||
await execShellAsync('docker image prune -f')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanupStuckedDeploymentsInDB () {
|
|
||||||
// Cleanup stucked deployments.
|
|
||||||
await Deployment.updateMany(
|
|
||||||
{ progress: { $in: ['queued', 'inprogress'] } },
|
|
||||||
{ progress: 'failed' }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteSameDeployments (configuration) {
|
|
||||||
await (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application').map(async s => {
|
|
||||||
const running = JSON.parse(s.Spec.Labels.configuration)
|
|
||||||
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
|
|
||||||
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { cleanupStuckedDeploymentsInDB, deleteSameDeployments, purgeImagesContainers }
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
|
|
||||||
const cuid = require('cuid')
|
|
||||||
const crypto = require('crypto')
|
|
||||||
const { docker } = require('../docker')
|
|
||||||
const { execShellAsync, baseServiceConfiguration } = require('../common')
|
|
||||||
|
|
||||||
function getUniq () {
|
|
||||||
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 })
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultConfiguration (configuration) {
|
|
||||||
const nickname = getUniq()
|
|
||||||
const deployId = cuid()
|
|
||||||
|
|
||||||
const shaBase = JSON.stringify({ repository: configuration.repository })
|
|
||||||
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex')
|
|
||||||
|
|
||||||
configuration.build.container.name = sha256.slice(0, 15)
|
|
||||||
|
|
||||||
configuration.general.nickname = nickname
|
|
||||||
configuration.general.deployId = deployId
|
|
||||||
configuration.general.workdir = `/tmp/${deployId}`
|
|
||||||
|
|
||||||
if (!configuration.publish.path) configuration.publish.path = '/'
|
|
||||||
if (!configuration.publish.port) {
|
|
||||||
if (configuration.build.pack === 'nodejs' && configuration.build.pack === 'vuejs' && configuration.build.pack === 'nuxtjs' && configuration.build.pack === 'rust' && configuration.build.pack === 'nextjs') {
|
|
||||||
configuration.publish.port = 3000
|
|
||||||
} else {
|
|
||||||
configuration.publish.port = 80
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!configuration.build.directory) configuration.build.directory = ''
|
|
||||||
if (configuration.build.directory.startsWith('/')) configuration.build.directory = configuration.build.directory.replace('/', '')
|
|
||||||
|
|
||||||
if (!configuration.publish.directory) configuration.publish.directory = ''
|
|
||||||
if (configuration.publish.directory.startsWith('/')) configuration.publish.directory = configuration.publish.directory.replace('/', '')
|
|
||||||
|
|
||||||
if (configuration.build.pack === 'static' || configuration.build.pack === 'nodejs') {
|
|
||||||
if (!configuration.build.command.installation) configuration.build.command.installation = 'yarn install'
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration.build.container.baseSHA = crypto.createHash('sha256').update(JSON.stringify(baseServiceConfiguration)).digest('hex')
|
|
||||||
configuration.baseServiceConfiguration = baseServiceConfiguration
|
|
||||||
|
|
||||||
return configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateServiceLabels (configuration) {
|
|
||||||
// In case of any failure during deployment, still update the current configuration.
|
|
||||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
|
||||||
const found = services.find(s => {
|
|
||||||
const config = JSON.parse(s.Spec.Labels.configuration)
|
|
||||||
if (config.repository.id === configuration.repository.id && config.repository.branch === configuration.repository.branch) {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
if (found) {
|
|
||||||
const { ID } = found
|
|
||||||
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration }
|
|
||||||
await execShellAsync(`docker service update --label-add configuration='${JSON.stringify(Labels)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${configuration.build.container.tag}' ${ID}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function precheckDeployment ({ services, configuration }) {
|
|
||||||
let foundService = false
|
|
||||||
let configChanged = false
|
|
||||||
let imageChanged = false
|
|
||||||
|
|
||||||
let forceUpdate = false
|
|
||||||
|
|
||||||
for (const service of services) {
|
|
||||||
const running = JSON.parse(service.Spec.Labels.configuration)
|
|
||||||
if (running) {
|
|
||||||
if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) {
|
|
||||||
// Base service configuration changed
|
|
||||||
if (!running.build.container.baseSHA || running.build.container.baseSHA !== configuration.build.container.baseSHA) {
|
|
||||||
forceUpdate = true
|
|
||||||
}
|
|
||||||
// If the deployment is in error state, forceUpdate
|
|
||||||
const state = await execShellAsync(`docker stack ps ${running.build.container.name} --format '{{ json . }}'`)
|
|
||||||
const isError = state.split('\n').filter(n => n).map(s => JSON.parse(s)).filter(n => n.DesiredState !== 'Running' && n.Image.split(':')[1] === running.build.container.tag)
|
|
||||||
if (isError.length > 0) forceUpdate = true
|
|
||||||
foundService = true
|
|
||||||
|
|
||||||
const runningWithoutContainer = JSON.parse(JSON.stringify(running))
|
|
||||||
delete runningWithoutContainer.build.container
|
|
||||||
|
|
||||||
const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration))
|
|
||||||
delete configurationWithoutContainer.build.container
|
|
||||||
|
|
||||||
// If only the configuration changed
|
|
||||||
if (JSON.stringify(runningWithoutContainer.build) !== JSON.stringify(configurationWithoutContainer.build) || JSON.stringify(runningWithoutContainer.publish) !== JSON.stringify(configurationWithoutContainer.publish)) configChanged = true
|
|
||||||
// If only the image changed
|
|
||||||
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true
|
|
||||||
// If build pack changed, forceUpdate the service
|
|
||||||
if (running.build.pack !== configuration.build.pack) forceUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (forceUpdate) {
|
|
||||||
imageChanged = false
|
|
||||||
configChanged = false
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
foundService,
|
|
||||||
imageChanged,
|
|
||||||
configChanged,
|
|
||||||
forceUpdate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = { setDefaultConfiguration, updateServiceLabels, precheckDeployment, baseServiceConfiguration }
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
const fs = require('fs').promises
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby']
|
|
||||||
try {
|
|
||||||
// TODO: Write full .dockerignore for all deployments!!
|
|
||||||
if (configuration.build.pack === 'php') {
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/.htaccess`, `
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteBase /
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteRule ^(.+)$ index.php [QSA,L]
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
// await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules')
|
|
||||||
if (staticDeployments.includes(configuration.build.pack)) {
|
|
||||||
await fs.writeFile(
|
|
||||||
`${configuration.general.workdir}/nginx.conf`,
|
|
||||||
`user nginx;
|
|
||||||
worker_processes auto;
|
|
||||||
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
|
|
||||||
access_log off;
|
|
||||||
sendfile on;
|
|
||||||
#tcp_nopush on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
try_files $uri $uri/index.html $uri/ /index.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 404 /50x.html;
|
|
||||||
|
|
||||||
# redirect server error pages to the static page /50x.html
|
|
||||||
#
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
|
||||||
location = /50x.html {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
const yaml = require('js-yaml')
|
|
||||||
const fs = require('fs').promises
|
|
||||||
const { execShellAsync } = require('../../common')
|
|
||||||
const { docker } = require('../../docker')
|
|
||||||
const { saveAppLog } = require('../../logging')
|
|
||||||
const { deleteSameDeployments } = require('../cleanup')
|
|
||||||
|
|
||||||
module.exports = async function (configuration, imageChanged) {
|
|
||||||
const generateEnvs = {}
|
|
||||||
for (const secret of configuration.publish.secrets) {
|
|
||||||
generateEnvs[secret.name] = secret.value
|
|
||||||
}
|
|
||||||
const containerName = configuration.build.container.name
|
|
||||||
|
|
||||||
// Only save SHA256 of it in the configuration label
|
|
||||||
const baseServiceConfiguration = configuration.baseServiceConfiguration
|
|
||||||
delete configuration.baseServiceConfiguration
|
|
||||||
|
|
||||||
const stack = {
|
|
||||||
version: '3.8',
|
|
||||||
services: {
|
|
||||||
[containerName]: {
|
|
||||||
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
|
|
||||||
networks: [`${docker.network}`],
|
|
||||||
environment: generateEnvs,
|
|
||||||
deploy: {
|
|
||||||
...baseServiceConfiguration,
|
|
||||||
labels: [
|
|
||||||
'managedBy=coolify',
|
|
||||||
'type=application',
|
|
||||||
'configuration=' + JSON.stringify(configuration),
|
|
||||||
'traefik.enable=true',
|
|
||||||
'traefik.http.services.' +
|
|
||||||
configuration.build.container.name +
|
|
||||||
`.loadbalancer.server.port=${configuration.publish.port}`,
|
|
||||||
'traefik.http.routers.' +
|
|
||||||
configuration.build.container.name +
|
|
||||||
'.entrypoints=websecure',
|
|
||||||
'traefik.http.routers.' +
|
|
||||||
configuration.build.container.name +
|
|
||||||
'.rule=Host(`' +
|
|
||||||
configuration.publish.domain +
|
|
||||||
'`) && PathPrefix(`' +
|
|
||||||
configuration.publish.path +
|
|
||||||
'`)',
|
|
||||||
'traefik.http.routers.' +
|
|
||||||
configuration.build.container.name +
|
|
||||||
'.tls.certresolver=letsencrypt',
|
|
||||||
'traefik.http.routers.' +
|
|
||||||
configuration.build.container.name +
|
|
||||||
'.middlewares=global-compress'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
networks: {
|
|
||||||
[`${docker.network}`]: {
|
|
||||||
external: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await saveAppLog('### Publishing.', configuration)
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
|
|
||||||
if (imageChanged) {
|
|
||||||
// console.log('image changed')
|
|
||||||
await execShellAsync(`docker service update --image ${configuration.build.container.name}:${configuration.build.container.tag} ${configuration.build.container.name}_${configuration.build.container.name}`)
|
|
||||||
} else {
|
|
||||||
// console.log('new deployment or force deployment or config changed')
|
|
||||||
await deleteSameDeployments(configuration)
|
|
||||||
await execShellAsync(
|
|
||||||
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveAppLog('### Published done!', configuration)
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const jwt = require('jsonwebtoken')
|
|
||||||
const axios = require('axios')
|
|
||||||
const { execShellAsync } = require('../../common')
|
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
|
||||||
try {
|
|
||||||
const { workdir } = configuration.general
|
|
||||||
const { organization, name, branch } = configuration.repository
|
|
||||||
const github = configuration.github
|
|
||||||
if (!github.installation.id || !github.app.id) {
|
|
||||||
throw new Error('Github installation ID is invalid.')
|
|
||||||
}
|
|
||||||
const githubPrivateKey = process.env.GITHUB_APP_PRIVATE_KEY.replace(/\\n/g, '\n').replace(/"/g, '')
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
iat: Math.round(new Date().getTime() / 1000),
|
|
||||||
exp: Math.round(new Date().getTime() / 1000 + 60),
|
|
||||||
iss: parseInt(github.app.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const jwtToken = jwt.sign(payload, githubPrivateKey, {
|
|
||||||
algorithm: 'RS256'
|
|
||||||
})
|
|
||||||
const accessToken = await axios({
|
|
||||||
method: 'POST',
|
|
||||||
url: `https://api.github.com/app/installations/${github.installation.id}/access_tokens`,
|
|
||||||
data: {},
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + jwtToken,
|
|
||||||
Accept: 'application/vnd.github.machine-man-preview+json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await execShellAsync(
|
|
||||||
`mkdir -p ${workdir} && git clone -q -b ${branch} https://x-access-token:${accessToken.data.token}@github.com/${organization}/${name}.git ${workdir}/`
|
|
||||||
)
|
|
||||||
configuration.build.container.tag = (
|
|
||||||
await execShellAsync(`cd ${configuration.general.workdir}/ && git rev-parse HEAD`)
|
|
||||||
)
|
|
||||||
.replace('\n', '')
|
|
||||||
.slice(0, 7)
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
const dayjs = require('dayjs')
|
|
||||||
|
|
||||||
const { saveAppLog } = require('../logging')
|
|
||||||
const copyFiles = require('./deploy/copyFiles')
|
|
||||||
const buildContainer = require('./build/container')
|
|
||||||
const deploy = require('./deploy/deploy')
|
|
||||||
const Deployment = require('../../models/Deployment')
|
|
||||||
const { updateServiceLabels } = require('./configuration')
|
|
||||||
|
|
||||||
async function queueAndBuild (configuration, imageChanged) {
|
|
||||||
const { id, organization, name, branch } = configuration.repository
|
|
||||||
const { domain } = configuration.publish
|
|
||||||
const { deployId, nickname } = configuration.general
|
|
||||||
await new Deployment({
|
|
||||||
repoId: id, branch, deployId, domain, organization, name, nickname
|
|
||||||
}).save()
|
|
||||||
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration)
|
|
||||||
await copyFiles(configuration)
|
|
||||||
await buildContainer(configuration)
|
|
||||||
await deploy(configuration, imageChanged)
|
|
||||||
await Deployment.findOneAndUpdate(
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain },
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' })
|
|
||||||
await updateServiceLabels(configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { queueAndBuild }
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
const crypto = require('crypto')
|
|
||||||
const shell = require('shelljs')
|
|
||||||
const jsonwebtoken = require('jsonwebtoken')
|
|
||||||
const { docker } = require('./docker')
|
|
||||||
const User = require('../models/User')
|
|
||||||
const algorithm = 'aes-256-cbc'
|
|
||||||
const key = process.env.SECRETS_ENCRYPTION_KEY
|
|
||||||
|
|
||||||
const baseServiceConfiguration = {
|
|
||||||
replicas: 1,
|
|
||||||
restart_policy: {
|
|
||||||
condition: 'any',
|
|
||||||
max_attempts: 6
|
|
||||||
},
|
|
||||||
update_config: {
|
|
||||||
parallelism: 1,
|
|
||||||
delay: '10s',
|
|
||||||
order: 'start-first'
|
|
||||||
},
|
|
||||||
rollback_config: {
|
|
||||||
parallelism: 1,
|
|
||||||
delay: '10s',
|
|
||||||
order: 'start-first',
|
|
||||||
failure_action: 'rollback'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function delay (t) {
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
setTimeout(function () {
|
|
||||||
resolve('OK')
|
|
||||||
}, t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyUserId (authorization) {
|
|
||||||
try {
|
|
||||||
const token = authorization.split(' ')[1]
|
|
||||||
const verify = jsonwebtoken.verify(token, process.env.JWT_SIGN_KEY)
|
|
||||||
const found = await User.findOne({ uid: verify.jti })
|
|
||||||
if (found) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function execShellAsync (cmd, opts = {}) {
|
|
||||||
try {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
shell.config.silent = true
|
|
||||||
shell.exec(cmd, opts, function (code, stdout, stderr) {
|
|
||||||
if (code !== 0) return reject(new Error(stderr))
|
|
||||||
return resolve(stdout)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Error('Oops')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function cleanupTmp (dir) {
|
|
||||||
if (dir !== '/') shell.rm('-fr', dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkImageAvailable (name) {
|
|
||||||
let cacheAvailable = false
|
|
||||||
try {
|
|
||||||
await docker.engine.getImage(name).get()
|
|
||||||
cacheAvailable = true
|
|
||||||
} catch (e) {
|
|
||||||
// Cache image not found
|
|
||||||
}
|
|
||||||
return cacheAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
function encryptData (text) {
|
|
||||||
const iv = crypto.randomBytes(16)
|
|
||||||
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv)
|
|
||||||
let encrypted = cipher.update(text)
|
|
||||||
encrypted = Buffer.concat([encrypted, cipher.final()])
|
|
||||||
return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') }
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptData (text) {
|
|
||||||
const iv = Buffer.from(text.iv, 'hex')
|
|
||||||
const encryptedText = Buffer.from(text.encryptedData, 'hex')
|
|
||||||
const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv)
|
|
||||||
let decrypted = decipher.update(encryptedText)
|
|
||||||
decrypted = Buffer.concat([decrypted, decipher.final()])
|
|
||||||
return decrypted.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
function createToken (payload) {
|
|
||||||
const { uuid } = payload
|
|
||||||
return jsonwebtoken.sign({}, process.env.JWT_SIGN_KEY, {
|
|
||||||
expiresIn: 15778800,
|
|
||||||
algorithm: 'HS256',
|
|
||||||
audience: 'coolify',
|
|
||||||
issuer: 'coolify',
|
|
||||||
jwtid: uuid,
|
|
||||||
subject: `User:${uuid}`,
|
|
||||||
notBefore: -1000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
delay,
|
|
||||||
createToken,
|
|
||||||
execShellAsync,
|
|
||||||
cleanupTmp,
|
|
||||||
checkImageAvailable,
|
|
||||||
encryptData,
|
|
||||||
decryptData,
|
|
||||||
verifyUserId,
|
|
||||||
baseServiceConfiguration
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const Dockerode = require('dockerode')
|
|
||||||
const { saveAppLog } = require('./logging')
|
|
||||||
|
|
||||||
const docker = {
|
|
||||||
engine: new Dockerode({
|
|
||||||
socketPath: process.env.DOCKER_ENGINE
|
|
||||||
}),
|
|
||||||
network: process.env.DOCKER_NETWORK
|
|
||||||
}
|
|
||||||
async function streamEvents (stream, configuration) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
docker.engine.modem.followProgress(stream, onFinished, onProgress)
|
|
||||||
function onFinished (err, res) {
|
|
||||||
if (err) reject(err)
|
|
||||||
resolve(res)
|
|
||||||
}
|
|
||||||
function onProgress (event) {
|
|
||||||
if (event.error) {
|
|
||||||
saveAppLog(event.error, configuration, true)
|
|
||||||
reject(event.error)
|
|
||||||
} else if (event.stream) {
|
|
||||||
saveAppLog(event.stream, configuration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { streamEvents, docker }
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.handleErrors = exports.handleValidationError = exports.handleNotFoundError = void 0;
|
|
||||||
const http_errors_enhanced_1 = require("http-errors-enhanced");
|
|
||||||
const interfaces_1 = require("./interfaces");
|
|
||||||
const utils_1 = require("./utils");
|
|
||||||
const validation_1 = require("./validation");
|
|
||||||
function handleNotFoundError(request, reply) {
|
|
||||||
handleErrors(new http_errors_enhanced_1.NotFoundError('Not found.'), request, reply);
|
|
||||||
}
|
|
||||||
exports.handleNotFoundError = handleNotFoundError;
|
|
||||||
function handleValidationError(error, request) {
|
|
||||||
/*
|
|
||||||
As seen in https://github.com/fastify/fastify/blob/master/lib/validation.js
|
|
||||||
the error.message will always start with the relative section (params, querystring, headers, body)
|
|
||||||
and fastify throws on first failing section.
|
|
||||||
*/
|
|
||||||
const section = error.message.match(/^\w+/)[0];
|
|
||||||
return new http_errors_enhanced_1.BadRequestError('One or more validations failed trying to process your request.', {
|
|
||||||
failedValidations: validation_1.convertValidationErrors(section, Reflect.get(request, section), error.validation)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
exports.handleValidationError = handleValidationError;
|
|
||||||
function handleErrors(error, request, reply) {
|
|
||||||
var _a, _b;
|
|
||||||
// It is a generic error, handle it
|
|
||||||
const code = error.code;
|
|
||||||
if (!('statusCode' in error)) {
|
|
||||||
if ('validation' in error && ((_a = request[interfaces_1.kHttpErrorsEnhancedConfiguration]) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
|
|
||||||
// If it is a validation error, convert errors to human friendly format
|
|
||||||
error = handleValidationError(error, request);
|
|
||||||
}
|
|
||||||
else if ((_b = request[interfaces_1.kHttpErrorsEnhancedConfiguration]) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
|
|
||||||
// It is requested to hide the error, just log it and then create a generic one
|
|
||||||
request.log.error({ error: http_errors_enhanced_1.serializeError(error) });
|
|
||||||
error = new http_errors_enhanced_1.InternalServerError('An error occurred trying to process your request.');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Wrap in a HttpError, making the stack explicitily available
|
|
||||||
error = new http_errors_enhanced_1.InternalServerError(http_errors_enhanced_1.serializeError(error));
|
|
||||||
Object.defineProperty(error, 'stack', { enumerable: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (code === 'INVALID_CONTENT_TYPE' || code === 'FST_ERR_CTP_INVALID_MEDIA_TYPE') {
|
|
||||||
error = new http_errors_enhanced_1.UnsupportedMediaTypeError(utils_1.upperFirst(validation_1.validationMessagesFormatters.contentType()));
|
|
||||||
}
|
|
||||||
else if (code === 'FST_ERR_CTP_EMPTY_JSON_BODY') {
|
|
||||||
error = new http_errors_enhanced_1.BadRequestError(utils_1.upperFirst(validation_1.validationMessagesFormatters.jsonEmpty()));
|
|
||||||
}
|
|
||||||
else if (code === 'MALFORMED_JSON' || error.message === 'Invalid JSON' || error.stack.includes('at JSON.parse')) {
|
|
||||||
error = new http_errors_enhanced_1.BadRequestError(utils_1.upperFirst(validation_1.validationMessagesFormatters.json()));
|
|
||||||
}
|
|
||||||
// Get the status code
|
|
||||||
let { statusCode, headers } = error;
|
|
||||||
// Code outside HTTP range
|
|
||||||
if (statusCode < 100 || statusCode > 599) {
|
|
||||||
statusCode = http_errors_enhanced_1.INTERNAL_SERVER_ERROR;
|
|
||||||
}
|
|
||||||
// Create the body
|
|
||||||
const body = {
|
|
||||||
statusCode,
|
|
||||||
error: http_errors_enhanced_1.messagesByCodes[statusCode],
|
|
||||||
message: error.message
|
|
||||||
};
|
|
||||||
http_errors_enhanced_1.addAdditionalProperties(body, error);
|
|
||||||
// Send the error back
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
reply
|
|
||||||
.code(statusCode)
|
|
||||||
.headers(headers !== null && headers !== void 0 ? headers : {})
|
|
||||||
.type('application/json')
|
|
||||||
.send(body);
|
|
||||||
}
|
|
||||||
exports.handleErrors = handleErrors;
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
"use strict";
|
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
||||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
||||||
};
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.plugin = exports.validationMessagesFormatters = exports.niceJoin = exports.convertValidationErrors = void 0;
|
|
||||||
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
|
|
||||||
const handlers_1 = require("./handlers");
|
|
||||||
const interfaces_1 = require("./interfaces");
|
|
||||||
const validation_1 = require("./validation");
|
|
||||||
__exportStar(require("./handlers"), exports);
|
|
||||||
__exportStar(require("./interfaces"), exports);
|
|
||||||
var validation_2 = require("./validation");
|
|
||||||
Object.defineProperty(exports, "convertValidationErrors", { enumerable: true, get: function () { return validation_2.convertValidationErrors; } });
|
|
||||||
Object.defineProperty(exports, "niceJoin", { enumerable: true, get: function () { return validation_2.niceJoin; } });
|
|
||||||
Object.defineProperty(exports, "validationMessagesFormatters", { enumerable: true, get: function () { return validation_2.validationMessagesFormatters; } });
|
|
||||||
exports.plugin = fastify_plugin_1.default(function (instance, options, done) {
|
|
||||||
var _a, _b, _c, _d;
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const convertResponsesValidationErrors = (_a = options.convertResponsesValidationErrors) !== null && _a !== void 0 ? _a : !isProduction;
|
|
||||||
const configuration = {
|
|
||||||
hideUnhandledErrors: (_b = options.hideUnhandledErrors) !== null && _b !== void 0 ? _b : isProduction,
|
|
||||||
convertValidationErrors: (_c = options.convertValidationErrors) !== null && _c !== void 0 ? _c : true,
|
|
||||||
responseValidatorCustomizer: options.responseValidatorCustomizer,
|
|
||||||
allowUndeclaredResponses: (_d = options.allowUndeclaredResponses) !== null && _d !== void 0 ? _d : false
|
|
||||||
};
|
|
||||||
instance.decorate(interfaces_1.kHttpErrorsEnhancedConfiguration, null);
|
|
||||||
instance.decorateRequest(interfaces_1.kHttpErrorsEnhancedConfiguration, null);
|
|
||||||
instance.addHook('onRequest', async (request) => {
|
|
||||||
request[interfaces_1.kHttpErrorsEnhancedConfiguration] = configuration;
|
|
||||||
});
|
|
||||||
instance.setErrorHandler(handlers_1.handleErrors);
|
|
||||||
// instance.setNotFoundHandler(handlers_1.handleNotFoundError);
|
|
||||||
if (convertResponsesValidationErrors) {
|
|
||||||
instance.decorate(interfaces_1.kHttpErrorsEnhancedResponseValidations, []);
|
|
||||||
instance.addHook('onRoute', validation_1.addResponseValidation);
|
|
||||||
instance.addHook('onReady', validation_1.compileResponseValidationSchema.bind(instance, configuration));
|
|
||||||
}
|
|
||||||
done();
|
|
||||||
}, { name: 'fastify-http-errors-enhanced' });
|
|
||||||
exports.default = exports.plugin;
|
|
||||||
// Fix CommonJS exporting
|
|
||||||
/* istanbul ignore else */
|
|
||||||
if (typeof module !== 'undefined') {
|
|
||||||
module.exports = exports.plugin;
|
|
||||||
Object.assign(module.exports, exports);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.kHttpErrorsEnhancedResponseValidations = exports.kHttpErrorsEnhancedConfiguration = void 0;
|
|
||||||
exports.kHttpErrorsEnhancedConfiguration = Symbol('fastify-http-errors-enhanced-configuration');
|
|
||||||
exports.kHttpErrorsEnhancedResponseValidations = Symbol('fastify-http-errors-enhanced-response-validation');
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.get = exports.upperFirst = void 0;
|
|
||||||
function upperFirst(source) {
|
|
||||||
if (typeof source !== 'string' || !source.length) {
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
return source[0].toUpperCase() + source.substring(1);
|
|
||||||
}
|
|
||||||
exports.upperFirst = upperFirst;
|
|
||||||
function get(target, path) {
|
|
||||||
var _a;
|
|
||||||
const tokens = path.split('.').map((t) => t.trim());
|
|
||||||
for (const token of tokens) {
|
|
||||||
if (typeof target === 'undefined' || target === null) {
|
|
||||||
// We're supposed to be still iterating, but the chain is over - Return undefined
|
|
||||||
target = undefined;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const index = token.match(/^(\d+)|(?:\[(\d+)\])$/);
|
|
||||||
if (index) {
|
|
||||||
target = target[parseInt((_a = index[1]) !== null && _a !== void 0 ? _a : index[2], 10)];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
target = target[token];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
exports.get = get;
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
"use strict";
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.compileResponseValidationSchema = exports.addResponseValidation = exports.convertValidationErrors = exports.validationMessagesFormatters = exports.niceJoin = void 0;
|
|
||||||
const ajv_1 = __importDefault(require("ajv"));
|
|
||||||
const http_errors_enhanced_1 = require("http-errors-enhanced");
|
|
||||||
const interfaces_1 = require("./interfaces");
|
|
||||||
const utils_1 = require("./utils");
|
|
||||||
function niceJoin(array, lastSeparator = ' and ', separator = ', ') {
|
|
||||||
switch (array.length) {
|
|
||||||
case 0:
|
|
||||||
return '';
|
|
||||||
case 1:
|
|
||||||
return array[0];
|
|
||||||
case 2:
|
|
||||||
return array.join(lastSeparator);
|
|
||||||
default:
|
|
||||||
return array.slice(0, array.length - 1).join(separator) + lastSeparator + array[array.length - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.niceJoin = niceJoin;
|
|
||||||
exports.validationMessagesFormatters = {
|
|
||||||
contentType: () => 'only JSON payloads are accepted. Please set the "Content-Type" header to start with "application/json"',
|
|
||||||
json: () => 'the body payload is not a valid JSON',
|
|
||||||
jsonEmpty: () => 'the JSON body payload cannot be empty if the "Content-Type" header is set',
|
|
||||||
missing: () => 'must be present',
|
|
||||||
unknown: () => 'is not a valid property',
|
|
||||||
uuid: () => 'must be a valid GUID (UUID v4)',
|
|
||||||
timestamp: () => 'must be a valid ISO 8601 / RFC 3339 timestamp (example: 2018-07-06T12:34:56Z)',
|
|
||||||
date: () => 'must be a valid ISO 8601 / RFC 3339 date (example: 2018-07-06)',
|
|
||||||
time: () => 'must be a valid ISO 8601 / RFC 3339 time (example: 12:34:56)',
|
|
||||||
uri: () => 'must be a valid URI',
|
|
||||||
hostname: () => 'must be a valid hostname',
|
|
||||||
ipv4: () => 'must be a valid IPv4',
|
|
||||||
ipv6: () => 'must be a valid IPv6',
|
|
||||||
paramType: (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'integer':
|
|
||||||
return 'must be a valid integer number';
|
|
||||||
case 'number':
|
|
||||||
return 'must be a valid number';
|
|
||||||
case 'boolean':
|
|
||||||
return 'must be a valid boolean (true or false)';
|
|
||||||
case 'object':
|
|
||||||
return 'must be a object';
|
|
||||||
case 'array':
|
|
||||||
return 'must be an array';
|
|
||||||
default:
|
|
||||||
return 'must be a string';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
presentString: () => 'must be a non empty string',
|
|
||||||
minimum: (min) => `must be a number greater than or equal to ${min}`,
|
|
||||||
maximum: (max) => `must be a number less than or equal to ${max}`,
|
|
||||||
minimumProperties(min) {
|
|
||||||
return min === 1 ? 'cannot be a empty object' : `must be a object with at least ${min} properties`;
|
|
||||||
},
|
|
||||||
maximumProperties(max) {
|
|
||||||
return max === 0 ? 'must be a empty object' : `must be a object with at most ${max} properties`;
|
|
||||||
},
|
|
||||||
minimumItems(min) {
|
|
||||||
return min === 1 ? 'cannot be a empty array' : `must be an array with at least ${min} items`;
|
|
||||||
},
|
|
||||||
maximumItems(max) {
|
|
||||||
return max === 0 ? 'must be a empty array' : `must be an array with at most ${max} items`;
|
|
||||||
},
|
|
||||||
enum: (values) => `must be one of the following values: ${niceJoin(values.map((f) => `"${f}"`), ' or ')}`,
|
|
||||||
pattern: (pattern) => `must match pattern "${pattern.replace(/\(\?:/g, '(')}"`,
|
|
||||||
invalidResponseCode: (code) => `This endpoint cannot respond with HTTP status ${code}.`,
|
|
||||||
invalidResponse: (code) => `The response returned from the endpoint violates its specification for the HTTP status ${code}.`,
|
|
||||||
invalidFormat: (format) => `must match format "${format}" (format)`
|
|
||||||
};
|
|
||||||
function convertValidationErrors(section, data, validationErrors) {
|
|
||||||
const errors = {};
|
|
||||||
if (section === 'querystring') {
|
|
||||||
section = 'query';
|
|
||||||
}
|
|
||||||
// For each error
|
|
||||||
for (const e of validationErrors) {
|
|
||||||
let message = '';
|
|
||||||
let pattern;
|
|
||||||
let value;
|
|
||||||
let reason;
|
|
||||||
// Normalize the key
|
|
||||||
let key = e.dataPath;
|
|
||||||
if (key.startsWith('.')) {
|
|
||||||
key = key.substring(1);
|
|
||||||
}
|
|
||||||
// Remove useless quotes
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (key.startsWith('[') && key.endsWith(']')) {
|
|
||||||
key = key.substring(1, key.length - 1);
|
|
||||||
}
|
|
||||||
// Depending on the type
|
|
||||||
switch (e.keyword) {
|
|
||||||
case 'required':
|
|
||||||
case 'dependencies':
|
|
||||||
key = e.params.missingProperty;
|
|
||||||
message = exports.validationMessagesFormatters.missing();
|
|
||||||
break;
|
|
||||||
case 'additionalProperties':
|
|
||||||
key = e.params.additionalProperty;
|
|
||||||
message = exports.validationMessagesFormatters.unknown();
|
|
||||||
break;
|
|
||||||
case 'type':
|
|
||||||
message = exports.validationMessagesFormatters.paramType(e.params.type);
|
|
||||||
break;
|
|
||||||
case 'minProperties':
|
|
||||||
message = exports.validationMessagesFormatters.minimumProperties(e.params.limit);
|
|
||||||
break;
|
|
||||||
case 'maxProperties':
|
|
||||||
message = exports.validationMessagesFormatters.maximumProperties(e.params.limit);
|
|
||||||
break;
|
|
||||||
case 'minItems':
|
|
||||||
message = exports.validationMessagesFormatters.minimumItems(e.params.limit);
|
|
||||||
break;
|
|
||||||
case 'maxItems':
|
|
||||||
message = exports.validationMessagesFormatters.maximumItems(e.params.limit);
|
|
||||||
break;
|
|
||||||
case 'minimum':
|
|
||||||
message = exports.validationMessagesFormatters.minimum(e.params.limit);
|
|
||||||
break;
|
|
||||||
case 'maximum':
|
|
||||||
message = exports.validationMessagesFormatters.maximum(e.params.limit);
|
|
||||||
break;
|
|
||||||
case 'enum':
|
|
||||||
message = exports.validationMessagesFormatters.enum(e.params.allowedValues);
|
|
||||||
break;
|
|
||||||
case 'pattern':
|
|
||||||
pattern = e.params.pattern;
|
|
||||||
value = utils_1.get(data, key);
|
|
||||||
if (pattern === '.+' && !value) {
|
|
||||||
message = exports.validationMessagesFormatters.presentString();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = exports.validationMessagesFormatters.pattern(e.params.pattern);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'format':
|
|
||||||
reason = e.params.format;
|
|
||||||
// Normalize the key
|
|
||||||
if (reason === 'date-time') {
|
|
||||||
reason = 'timestamp';
|
|
||||||
}
|
|
||||||
message = (exports.validationMessagesFormatters[reason] || exports.validationMessagesFormatters.invalidFormat)(reason);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No custom message was found, default to input one replacing the starting verb and adding some path info
|
|
||||||
if (!message.length) {
|
|
||||||
message = `${e.message.replace(/^should/, 'must')} (${e.keyword})`;
|
|
||||||
}
|
|
||||||
// Remove useless quotes
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (key.match(/(?:^['"])(?:[^.]+)(?:['"]$)/)) {
|
|
||||||
key = key.substring(1, key.length - 1);
|
|
||||||
}
|
|
||||||
// Fix empty properties
|
|
||||||
if (!key) {
|
|
||||||
key = '$root';
|
|
||||||
}
|
|
||||||
key = key.replace(/^\//, '');
|
|
||||||
errors[key] = message;
|
|
||||||
}
|
|
||||||
return { [section]: errors };
|
|
||||||
}
|
|
||||||
exports.convertValidationErrors = convertValidationErrors;
|
|
||||||
function addResponseValidation(route) {
|
|
||||||
var _a;
|
|
||||||
if (!((_a = route.schema) === null || _a === void 0 ? void 0 : _a.response)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const validators = {};
|
|
||||||
/*
|
|
||||||
Add these validators to the list of the one to compile once the server is started.
|
|
||||||
This makes possible to handle shared schemas.
|
|
||||||
*/
|
|
||||||
this[interfaces_1.kHttpErrorsEnhancedResponseValidations].push([
|
|
||||||
this,
|
|
||||||
validators,
|
|
||||||
Object.entries(route.schema.response)
|
|
||||||
]);
|
|
||||||
// Note that this hook is not called for non JSON payloads therefore validation is not possible in such cases
|
|
||||||
route.preSerialization = async function (request, reply, payload) {
|
|
||||||
const statusCode = reply.raw.statusCode;
|
|
||||||
// Never validate error 500
|
|
||||||
if (statusCode === http_errors_enhanced_1.INTERNAL_SERVER_ERROR) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
// No validator, it means the HTTP status is not allowed
|
|
||||||
const validator = validators[statusCode];
|
|
||||||
if (!validator) {
|
|
||||||
if (request[interfaces_1.kHttpErrorsEnhancedConfiguration].allowUndeclaredResponses) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
throw new http_errors_enhanced_1.InternalServerError(exports.validationMessagesFormatters.invalidResponseCode(statusCode));
|
|
||||||
}
|
|
||||||
// Now validate the payload
|
|
||||||
const valid = validator(payload);
|
|
||||||
if (!valid) {
|
|
||||||
throw new http_errors_enhanced_1.InternalServerError(exports.validationMessagesFormatters.invalidResponse(statusCode), {
|
|
||||||
failedValidations: convertValidationErrors('response', payload, validator.errors)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return payload;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
exports.addResponseValidation = addResponseValidation;
|
|
||||||
function compileResponseValidationSchema(configuration) {
|
|
||||||
// Fix CJS/ESM interoperability
|
|
||||||
// @ts-expect-error
|
|
||||||
let AjvConstructor = ajv_1.default;
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (AjvConstructor.default) {
|
|
||||||
AjvConstructor = AjvConstructor.default;
|
|
||||||
}
|
|
||||||
const hasCustomizer = typeof configuration.responseValidatorCustomizer === 'function';
|
|
||||||
for (const [instance, validators, schemas] of this[interfaces_1.kHttpErrorsEnhancedResponseValidations]) {
|
|
||||||
// @ts-expect-error
|
|
||||||
const compiler = new AjvConstructor({
|
|
||||||
// The fastify defaults, with the exception of removeAdditional and coerceTypes, which have been reversed
|
|
||||||
removeAdditional: false,
|
|
||||||
useDefaults: true,
|
|
||||||
coerceTypes: false,
|
|
||||||
allErrors: true
|
|
||||||
});
|
|
||||||
compiler.addSchema(Object.values(instance.getSchemas()));
|
|
||||||
compiler.addKeyword('example');
|
|
||||||
if (hasCustomizer) {
|
|
||||||
configuration.responseValidatorCustomizer(compiler);
|
|
||||||
}
|
|
||||||
for (const [code, schema] of schemas) {
|
|
||||||
validators[code] = compiler.compile(schema);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.compileResponseValidationSchema = compileResponseValidationSchema;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
const dayjs = require('dayjs')
|
|
||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
const ApplicationLog = require('../models/Logs/Application')
|
|
||||||
const ServerLog = require('../models/Logs/Server')
|
|
||||||
const Settings = require('../models/Settings')
|
|
||||||
const { version } = require('../../package.json')
|
|
||||||
|
|
||||||
function generateTimestamp () {
|
|
||||||
return `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} `
|
|
||||||
}
|
|
||||||
const patterns = [
|
|
||||||
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
|
||||||
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
|
|
||||||
].join('|')
|
|
||||||
|
|
||||||
async function saveAppLog (event, configuration, isError) {
|
|
||||||
try {
|
|
||||||
const deployId = configuration.general.deployId
|
|
||||||
const repoId = configuration.repository.id
|
|
||||||
const branch = configuration.repository.branch
|
|
||||||
if (isError) {
|
|
||||||
const clearedEvent = '[ERROR 😱] ' + generateTimestamp() + event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '')
|
|
||||||
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
|
|
||||||
} else {
|
|
||||||
if (event && event !== '\n') {
|
|
||||||
const clearedEvent = '[INFO] ' + generateTimestamp() + event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '')
|
|
||||||
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveServerLog (error) {
|
|
||||||
const settings = await Settings.findOne({ applicationName: 'coolify' })
|
|
||||||
const payload = { message: error.message, stack: error.stack, type: error.type || 'spaghetticode', version }
|
|
||||||
|
|
||||||
const found = await ServerLog.find(payload)
|
|
||||||
if (found.length === 0 && error.message) await new ServerLog(payload).save()
|
|
||||||
if (settings && settings.sendErrors && process.env.NODE_ENV === 'production') await axios.post('https://errors.coollabs.io/api/error', payload)
|
|
||||||
}
|
|
||||||
module.exports = {
|
|
||||||
saveAppLog,
|
|
||||||
saveServerLog
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
const { execShellAsync, cleanupTmp, baseServiceConfiguration } = require('../../common')
|
|
||||||
const yaml = require('js-yaml')
|
|
||||||
const fs = require('fs').promises
|
|
||||||
const generator = require('generate-password')
|
|
||||||
const { docker } = require('../../docker')
|
|
||||||
|
|
||||||
async function plausible ({ email, userName, userPassword, baseURL, traefikURL }) {
|
|
||||||
const deployId = 'plausible'
|
|
||||||
const workdir = '/tmp/plausible'
|
|
||||||
const secretKey = generator.generate({ length: 64, numbers: true, strict: true })
|
|
||||||
const generateEnvsPostgres = {
|
|
||||||
POSTGRESQL_PASSWORD: generator.generate({ length: 24, numbers: true, strict: true }),
|
|
||||||
POSTGRESQL_USERNAME: generator.generate({ length: 10, numbers: true, strict: true }),
|
|
||||||
POSTGRESQL_DATABASE: 'plausible'
|
|
||||||
}
|
|
||||||
|
|
||||||
const secrets = [
|
|
||||||
{ name: 'ADMIN_USER_EMAIL', value: email },
|
|
||||||
{ name: 'ADMIN_USER_NAME', value: userName },
|
|
||||||
{ name: 'ADMIN_USER_PWD', value: userPassword },
|
|
||||||
{ name: 'BASE_URL', value: baseURL },
|
|
||||||
{ name: 'SECRET_KEY_BASE', value: secretKey },
|
|
||||||
{ name: 'DISABLE_AUTH', value: 'false' },
|
|
||||||
{ name: 'DISABLE_REGISTRATION', value: 'true' },
|
|
||||||
{ name: 'DATABASE_URL', value: `postgresql://${generateEnvsPostgres.POSTGRESQL_USERNAME}:${generateEnvsPostgres.POSTGRESQL_PASSWORD}@plausible_db:5432/${generateEnvsPostgres.POSTGRESQL_DATABASE}` },
|
|
||||||
{ name: 'CLICKHOUSE_DATABASE_URL', value: 'http://plausible_events_db:8123/plausible' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const generateEnvsClickhouse = {}
|
|
||||||
for (const secret of secrets) generateEnvsClickhouse[secret.name] = secret.value
|
|
||||||
|
|
||||||
const clickhouseConfigXml = `
|
|
||||||
<yandex>
|
|
||||||
<logger>
|
|
||||||
<level>warning</level>
|
|
||||||
<console>true</console>
|
|
||||||
</logger>
|
|
||||||
|
|
||||||
<!-- Stop all the unnecessary logging -->
|
|
||||||
<query_thread_log remove="remove"/>
|
|
||||||
<query_log remove="remove"/>
|
|
||||||
<text_log remove="remove"/>
|
|
||||||
<trace_log remove="remove"/>
|
|
||||||
<metric_log remove="remove"/>
|
|
||||||
<asynchronous_metric_log remove="remove"/>
|
|
||||||
</yandex>`
|
|
||||||
const clickhouseUserConfigXml = `
|
|
||||||
<yandex>
|
|
||||||
<profiles>
|
|
||||||
<default>
|
|
||||||
<log_queries>0</log_queries>
|
|
||||||
<log_query_threads>0</log_query_threads>
|
|
||||||
</default>
|
|
||||||
</profiles>
|
|
||||||
</yandex>`
|
|
||||||
|
|
||||||
const clickhouseConfigs = [
|
|
||||||
{ source: 'plausible-clickhouse-user-config.xml', target: '/etc/clickhouse-server/users.d/logging.xml' },
|
|
||||||
{ source: 'plausible-clickhouse-config.xml', target: '/etc/clickhouse-server/config.d/logging.xml' },
|
|
||||||
{ source: 'plausible-init.query', target: '/docker-entrypoint-initdb.d/init.query' },
|
|
||||||
{ source: 'plausible-init-db.sh', target: '/docker-entrypoint-initdb.d/init-db.sh' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;'
|
|
||||||
const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query'
|
|
||||||
await execShellAsync(`mkdir -p ${workdir}`)
|
|
||||||
await fs.writeFile(`${workdir}/clickhouse-config.xml`, clickhouseConfigXml)
|
|
||||||
await fs.writeFile(`${workdir}/clickhouse-user-config.xml`, clickhouseUserConfigXml)
|
|
||||||
await fs.writeFile(`${workdir}/init.query`, initQuery)
|
|
||||||
await fs.writeFile(`${workdir}/init-db.sh`, initScript)
|
|
||||||
const stack = {
|
|
||||||
version: '3.8',
|
|
||||||
services: {
|
|
||||||
[deployId]: {
|
|
||||||
image: 'plausible/analytics:latest',
|
|
||||||
command: 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"',
|
|
||||||
networks: [`${docker.network}`],
|
|
||||||
volumes: [`${deployId}-postgres-data:/var/lib/postgresql/data`],
|
|
||||||
environment: generateEnvsClickhouse,
|
|
||||||
deploy: {
|
|
||||||
...baseServiceConfiguration,
|
|
||||||
labels: [
|
|
||||||
'managedBy=coolify',
|
|
||||||
'type=service',
|
|
||||||
'serviceName=plausible',
|
|
||||||
'configuration=' + JSON.stringify({ email, userName, userPassword, baseURL, secretKey, generateEnvsPostgres, generateEnvsClickhouse }),
|
|
||||||
'traefik.enable=true',
|
|
||||||
'traefik.http.services.' +
|
|
||||||
deployId +
|
|
||||||
'.loadbalancer.server.port=8000',
|
|
||||||
'traefik.http.routers.' +
|
|
||||||
deployId +
|
|
||||||
'.entrypoints=websecure',
|
|
||||||
'traefik.http.routers.' +
|
|
||||||
deployId +
|
|
||||||
'.rule=Host(`' +
|
|
||||||
traefikURL +
|
|
||||||
'`) && PathPrefix(`/`)',
|
|
||||||
'traefik.http.routers.' +
|
|
||||||
deployId +
|
|
||||||
'.tls.certresolver=letsencrypt',
|
|
||||||
'traefik.http.routers.' +
|
|
||||||
deployId +
|
|
||||||
'.middlewares=global-compress'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plausible_db: {
|
|
||||||
image: 'bitnami/postgresql:13.2.0',
|
|
||||||
networks: [`${docker.network}`],
|
|
||||||
environment: generateEnvsPostgres,
|
|
||||||
deploy: {
|
|
||||||
...baseServiceConfiguration,
|
|
||||||
labels: [
|
|
||||||
'managedBy=coolify',
|
|
||||||
'type=service',
|
|
||||||
'serviceName=plausible'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plausible_events_db: {
|
|
||||||
image: 'yandex/clickhouse-server:21.3.2.5',
|
|
||||||
networks: [`${docker.network}`],
|
|
||||||
volumes: [`${deployId}-clickhouse-data:/var/lib/clickhouse`],
|
|
||||||
ulimits: {
|
|
||||||
nofile: {
|
|
||||||
soft: 262144,
|
|
||||||
hard: 262144
|
|
||||||
}
|
|
||||||
},
|
|
||||||
configs: [...clickhouseConfigs],
|
|
||||||
deploy: {
|
|
||||||
...baseServiceConfiguration,
|
|
||||||
labels: [
|
|
||||||
'managedBy=coolify',
|
|
||||||
'type=service',
|
|
||||||
'serviceName=plausible'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
networks: {
|
|
||||||
[`${docker.network}`]: {
|
|
||||||
external: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
volumes: {
|
|
||||||
[`${deployId}-clickhouse-data`]: {
|
|
||||||
external: true
|
|
||||||
},
|
|
||||||
[`${deployId}-postgres-data`]: {
|
|
||||||
external: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
configs: {
|
|
||||||
'plausible-clickhouse-user-config.xml': {
|
|
||||||
file: `${workdir}/clickhouse-user-config.xml`
|
|
||||||
},
|
|
||||||
'plausible-clickhouse-config.xml': {
|
|
||||||
file: `${workdir}/clickhouse-config.xml`
|
|
||||||
},
|
|
||||||
'plausible-init.query': {
|
|
||||||
file: `${workdir}/init.query`
|
|
||||||
},
|
|
||||||
'plausible-init-db.sh': {
|
|
||||||
file: `${workdir}/init-db.sh`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack))
|
|
||||||
await execShellAsync('docker stack rm plausible')
|
|
||||||
await execShellAsync(
|
|
||||||
`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`
|
|
||||||
)
|
|
||||||
cleanupTmp(workdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function activateAdminUser () {
|
|
||||||
const { POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD, POSTGRESQL_DATABASE } = JSON.parse(JSON.parse((await execShellAsync('docker service inspect plausible_plausible --format=\'{{json .Spec.Labels.configuration}}\'')))).generateEnvsPostgres
|
|
||||||
const containers = (await execShellAsync('docker ps -a --format=\'{{json .Names}}\'')).replace(/"/g, '').trim().split('\n')
|
|
||||||
const postgresDB = containers.find(container => container.startsWith('plausible_plausible_db'))
|
|
||||||
await execShellAsync(`docker exec ${postgresDB} psql -H postgresql://${POSTGRESQL_USERNAME}:${POSTGRESQL_PASSWORD}@localhost:5432/${POSTGRESQL_DATABASE} -c "UPDATE users SET email_verified = true;"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { plausible, activateAdminUser }
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const mongoose = require('mongoose')
|
|
||||||
const deploymentSchema = mongoose.Schema(
|
|
||||||
{
|
|
||||||
deployId: { type: String, required: true },
|
|
||||||
nickname: { type: String, required: true },
|
|
||||||
repoId: { type: Number, required: true },
|
|
||||||
organization: { type: String, required: true },
|
|
||||||
name: { type: String, required: true },
|
|
||||||
branch: { type: String, required: true },
|
|
||||||
domain: { type: String, required: true },
|
|
||||||
progress: { type: String, require: true, default: 'queued' }
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = mongoose.model('deployment', deploymentSchema)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
const mongoose = require('mongoose')
|
|
||||||
const logSchema = mongoose.Schema(
|
|
||||||
{
|
|
||||||
deployId: { type: String, required: true },
|
|
||||||
event: { type: String, required: true }
|
|
||||||
},
|
|
||||||
{ timestamps: { createdAt: 'createdAt', updatedAt: false } }
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = mongoose.model('logs-application', logSchema)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const mongoose = require('mongoose')
|
|
||||||
const { version } = require('../../../package.json')
|
|
||||||
const logSchema = mongoose.Schema(
|
|
||||||
{
|
|
||||||
version: { type: String, default: version },
|
|
||||||
type: { type: String, required: true },
|
|
||||||
message: { type: String, required: true },
|
|
||||||
stack: { type: String },
|
|
||||||
seen: { type: Boolean, default: false }
|
|
||||||
},
|
|
||||||
{ timestamps: { createdAt: 'createdAt', updatedAt: false } }
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = mongoose.model('logs-server', logSchema)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
const mongoose = require('mongoose')
|
|
||||||
|
|
||||||
const settingsSchema = mongoose.Schema(
|
|
||||||
{
|
|
||||||
applicationName: { type: String, required: true, default: 'coolify' },
|
|
||||||
allowRegistration: { type: Boolean, required: true, default: false },
|
|
||||||
sendErrors: { type: Boolean, required: true, default: true }
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = mongoose.model('settings', settingsSchema)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
const mongoose = require('mongoose')
|
|
||||||
|
|
||||||
const userSchema = mongoose.Schema(
|
|
||||||
{
|
|
||||||
email: { type: String, required: true },
|
|
||||||
avatar: { type: String },
|
|
||||||
uid: { type: String, required: true }
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = mongoose.model('user', userSchema)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const fp = require('fastify-plugin')
|
|
||||||
const User = require('../models/User')
|
|
||||||
module.exports = fp(async function (fastify, options, next) {
|
|
||||||
fastify.register(require('fastify-jwt'), {
|
|
||||||
secret: fastify.config.JWT_SIGN_KEY
|
|
||||||
})
|
|
||||||
fastify.addHook('onRequest', async (request, reply) => {
|
|
||||||
try {
|
|
||||||
const { jti } = await request.jwtVerify()
|
|
||||||
const found = await User.findOne({ uid: jti })
|
|
||||||
if (found) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
reply.code(401).send('Unauthorized')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
reply.code(401).send('Unauthorized')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
|
|
||||||
const { setDefaultConfiguration } = require('../../../libs/applications/configuration')
|
|
||||||
const { docker } = require('../../../libs/docker')
|
|
||||||
const { saveServerLog } = require('../../../libs/logging')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.post('/', async (request, reply) => {
|
|
||||||
try {
|
|
||||||
const configuration = setDefaultConfiguration(request.body)
|
|
||||||
|
|
||||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
|
||||||
let foundDomain = false
|
|
||||||
|
|
||||||
for (const service of services) {
|
|
||||||
const running = JSON.parse(service.Spec.Labels.configuration)
|
|
||||||
if (running) {
|
|
||||||
if (
|
|
||||||
running.publish.domain === configuration.publish.domain &&
|
|
||||||
running.repository.id !== configuration.repository.id &&
|
|
||||||
running.publish.path === configuration.publish.path
|
|
||||||
) {
|
|
||||||
foundDomain = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fastify.config.DOMAIN === configuration.publish.domain) foundDomain = true
|
|
||||||
if (foundDomain) {
|
|
||||||
reply.code(500).send({ message: 'Domain already in use.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return { message: 'OK' }
|
|
||||||
} catch (error) {
|
|
||||||
await saveServerLog(error)
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
|
|
||||||
const Deployment = require('../../../../models/Deployment')
|
|
||||||
const ApplicationLog = require('../../../../models/Logs/Application')
|
|
||||||
const { verifyUserId, cleanupTmp } = require('../../../../libs/common')
|
|
||||||
const { purgeImagesContainers } = require('../../../../libs/applications/cleanup')
|
|
||||||
const { queueAndBuild } = require('../../../../libs/applications')
|
|
||||||
const { setDefaultConfiguration, precheckDeployment } = require('../../../../libs/applications/configuration')
|
|
||||||
const { docker } = require('../../../../libs/docker')
|
|
||||||
const { saveServerLog } = require('../../../../libs/logging')
|
|
||||||
const cloneRepository = require('../../../../libs/applications/github/cloneRepository')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.post('/', async (request, reply) => {
|
|
||||||
let configuration
|
|
||||||
try {
|
|
||||||
await verifyUserId(request.headers.authorization)
|
|
||||||
} catch (error) {
|
|
||||||
reply.code(500).send({ error: 'Invalid request' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
|
||||||
configuration = setDefaultConfiguration(request.body)
|
|
||||||
if (!configuration) {
|
|
||||||
throw new Error('Whaat?')
|
|
||||||
}
|
|
||||||
await cloneRepository(configuration)
|
|
||||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
|
|
||||||
|
|
||||||
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
|
|
||||||
cleanupTmp(configuration.general.workdir)
|
|
||||||
reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const alreadyQueued = await Deployment.find({
|
|
||||||
repoId: configuration.repository.id,
|
|
||||||
branch: configuration.repository.branch,
|
|
||||||
organization: configuration.repository.organization,
|
|
||||||
name: configuration.repository.name,
|
|
||||||
domain: configuration.publish.domain,
|
|
||||||
progress: { $in: ['queued', 'inprogress'] }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (alreadyQueued.length > 0) {
|
|
||||||
reply.code(200).send({ message: 'Already in the queue.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name, deployId: configuration.general.deployId })
|
|
||||||
await queueAndBuild(configuration, imageChanged)
|
|
||||||
} catch (error) {
|
|
||||||
const { id, organization, name, branch } = configuration.repository
|
|
||||||
const { domain } = configuration.publish
|
|
||||||
const { deployId } = configuration.general
|
|
||||||
await Deployment.findOneAndUpdate(
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain },
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
|
||||||
if (error.name) {
|
|
||||||
if (error.message && error.stack) await saveServerLog(error)
|
|
||||||
if (reply.sent) await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
|
|
||||||
}
|
|
||||||
throw new Error(error)
|
|
||||||
} finally {
|
|
||||||
cleanupTmp(configuration.general.workdir)
|
|
||||||
await purgeImagesContainers(configuration)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
const ApplicationLog = require('../../../../models/Logs/Application')
|
|
||||||
const Deployment = require('../../../../models/Deployment')
|
|
||||||
const dayjs = require('dayjs')
|
|
||||||
const utc = require('dayjs/plugin/utc')
|
|
||||||
const relativeTime = require('dayjs/plugin/relativeTime')
|
|
||||||
dayjs.extend(utc)
|
|
||||||
dayjs.extend(relativeTime)
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
const getLogSchema = {
|
|
||||||
querystring: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
repoId: { type: 'string' },
|
|
||||||
branch: { type: 'string' }
|
|
||||||
},
|
|
||||||
required: ['repoId', 'branch']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fastify.get('/', { schema: getLogSchema }, async (request, reply) => {
|
|
||||||
try {
|
|
||||||
const { repoId, branch, page } = request.query
|
|
||||||
const onePage = 5
|
|
||||||
const show = Number(page) * onePage || 5
|
|
||||||
const deploy = await Deployment.find({ repoId, branch })
|
|
||||||
.select('-_id -__v -repoId')
|
|
||||||
.sort({ createdAt: 'desc' })
|
|
||||||
.limit(show)
|
|
||||||
|
|
||||||
const finalLogs = deploy.map(d => {
|
|
||||||
const finalLogs = { ...d._doc }
|
|
||||||
|
|
||||||
const updatedAt = dayjs(d.updatedAt).utc()
|
|
||||||
|
|
||||||
finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000
|
|
||||||
finalLogs.since = updatedAt.fromNow()
|
|
||||||
|
|
||||||
return finalLogs
|
|
||||||
})
|
|
||||||
return finalLogs
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fastify.get('/:deployId', async (request, reply) => {
|
|
||||||
const { deployId } = request.params
|
|
||||||
try {
|
|
||||||
const logs = await ApplicationLog.find({ deployId })
|
|
||||||
.select('-_id -__v')
|
|
||||||
.sort({ createdAt: 'asc' })
|
|
||||||
|
|
||||||
const deploy = await Deployment.findOne({ deployId })
|
|
||||||
.select('-_id -__v')
|
|
||||||
.sort({ createdAt: 'desc' })
|
|
||||||
|
|
||||||
const finalLogs = {}
|
|
||||||
finalLogs.progress = deploy.progress
|
|
||||||
finalLogs.events = logs.map(log => log.event)
|
|
||||||
finalLogs.human = dayjs(deploy.updatedAt).from(dayjs(deploy.updatedAt))
|
|
||||||
return finalLogs
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('No logs found')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const { docker } = require('../../../libs/docker')
|
|
||||||
const { saveServerLog } = require('../../../libs/logging')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.get('/', async (request, reply) => {
|
|
||||||
try {
|
|
||||||
const { name } = request.query
|
|
||||||
const service = await docker.engine.getService(`${name}_${name}`)
|
|
||||||
const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a)
|
|
||||||
return { logs }
|
|
||||||
} catch (error) {
|
|
||||||
await saveServerLog(error)
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
const { docker } = require('../../../libs/docker')
|
|
||||||
const { execShellAsync, delay } = require('../../../libs/common')
|
|
||||||
const ApplicationLog = require('../../../models/Logs/Application')
|
|
||||||
const Deployment = require('../../../models/Deployment')
|
|
||||||
const { purgeImagesContainers } = require('../../../libs/applications/cleanup')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.post('/', async (request, reply) => {
|
|
||||||
const { organization, name, branch } = request.body
|
|
||||||
let found = false
|
|
||||||
try {
|
|
||||||
(await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application').map(s => {
|
|
||||||
const running = JSON.parse(s.Spec.Labels.configuration)
|
|
||||||
if (running.repository.organization === organization &&
|
|
||||||
running.repository.name === name &&
|
|
||||||
running.repository.branch === branch) {
|
|
||||||
found = running
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
if (found) {
|
|
||||||
const deploys = await Deployment.find({ organization, branch, name })
|
|
||||||
for (const deploy of deploys) {
|
|
||||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId })
|
|
||||||
await Deployment.deleteMany({ deployId: deploy.deployId })
|
|
||||||
}
|
|
||||||
await execShellAsync(`docker stack rm ${found.build.container.name}`)
|
|
||||||
reply.code(200).send({ organization, name, branch })
|
|
||||||
await delay(10000)
|
|
||||||
await purgeImagesContainers(found, true)
|
|
||||||
} else {
|
|
||||||
reply.code(500).send({ message: 'Nothing to do.' })
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
reply.code(500).send({ message: 'Nothing to do.' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const { docker } = require('../../libs/docker')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.post('/', async (request, reply) => {
|
|
||||||
const { name, organization, branch } = request.body
|
|
||||||
const services = await docker.engine.listServices()
|
|
||||||
const applications = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
|
||||||
|
|
||||||
const found = applications.find(r => {
|
|
||||||
const configuration = r.Spec.Labels.configuration ? JSON.parse(r.Spec.Labels.configuration) : null
|
|
||||||
if (branch) {
|
|
||||||
if (configuration.repository.name === name && configuration.repository.organization === organization && configuration.repository.branch === branch) {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (configuration.repository.name === name && configuration.repository.organization === organization) {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
if (found) {
|
|
||||||
return JSON.parse(found.Spec.Labels.configuration)
|
|
||||||
} else {
|
|
||||||
reply.code(500).send({ message: 'No configuration found.' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
const { docker } = require('../../../libs/docker')
|
|
||||||
const ServerLog = require('../../../models/Logs/Server')
|
|
||||||
const { saveServerLog } = require('../../../libs/logging')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.get('/', async (request, reply) => {
|
|
||||||
try {
|
|
||||||
const serverLogs = await ServerLog.find()
|
|
||||||
const dockerServices = await docker.engine.listServices()
|
|
||||||
let applications = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' && r.Spec.Labels.configuration)
|
|
||||||
let databases = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && r.Spec.Labels.configuration)
|
|
||||||
let services = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'service' && r.Spec.Labels.configuration)
|
|
||||||
applications = applications.map(r => {
|
|
||||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
|
||||||
return {
|
|
||||||
configuration: JSON.parse(r.Spec.Labels.configuration),
|
|
||||||
UpdatedAt: r.UpdatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
databases = databases.map(r => {
|
|
||||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
|
||||||
return {
|
|
||||||
configuration: JSON.parse(r.Spec.Labels.configuration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
services = services.map(r => {
|
|
||||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
|
||||||
return {
|
|
||||||
serviceName: r.Spec.Labels.serviceName,
|
|
||||||
configuration: JSON.parse(r.Spec.Labels.configuration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
applications = [...new Map(applications.map(item => [item.configuration.publish.domain + item.configuration.publish.path, item])).values()]
|
|
||||||
return {
|
|
||||||
serverLogs,
|
|
||||||
applications: {
|
|
||||||
deployed: applications
|
|
||||||
},
|
|
||||||
databases: {
|
|
||||||
deployed: databases
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
deployed: services
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT' && error.errno === -2) {
|
|
||||||
throw new Error(`Docker service unavailable at ${error.address}.`)
|
|
||||||
} else {
|
|
||||||
await saveServerLog(error)
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
const yaml = require('js-yaml')
|
|
||||||
const fs = require('fs').promises
|
|
||||||
const cuid = require('cuid')
|
|
||||||
const { docker } = require('../../../libs/docker')
|
|
||||||
const { execShellAsync } = require('../../../libs/common')
|
|
||||||
const { saveServerLog } = require('../../../libs/logging')
|
|
||||||
|
|
||||||
const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator')
|
|
||||||
const generator = require('generate-password')
|
|
||||||
|
|
||||||
function getUniq () {
|
|
||||||
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 })
|
|
||||||
}
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.get('/:deployId', async (request, reply) => {
|
|
||||||
const { deployId } = request.params
|
|
||||||
try {
|
|
||||||
const database = (await docker.engine.listServices()).find(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && JSON.parse(r.Spec.Labels.configuration).general.deployId === deployId)
|
|
||||||
if (database) {
|
|
||||||
const jsonEnvs = {}
|
|
||||||
if (database.Spec.TaskTemplate.ContainerSpec.Env) {
|
|
||||||
for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
|
|
||||||
const s = d.split('=')
|
|
||||||
jsonEnvs[s[0]] = s[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const payload = {
|
|
||||||
config: JSON.parse(database.Spec.Labels.configuration),
|
|
||||||
envs: jsonEnvs || null
|
|
||||||
}
|
|
||||||
reply.code(200).send(payload)
|
|
||||||
} else {
|
|
||||||
throw new Error()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error('No database found?')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const postSchema = {
|
|
||||||
body: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb', 'clickhouse'] }
|
|
||||||
},
|
|
||||||
required: ['type']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fastify.post('/deploy', { schema: postSchema }, async (request, reply) => {
|
|
||||||
try {
|
|
||||||
let { type, defaultDatabaseName } = request.body
|
|
||||||
const passwords = generator.generateMultiple(2, {
|
|
||||||
length: 24,
|
|
||||||
numbers: true,
|
|
||||||
strict: true
|
|
||||||
})
|
|
||||||
const usernames = generator.generateMultiple(2, {
|
|
||||||
length: 10,
|
|
||||||
numbers: true,
|
|
||||||
strict: true
|
|
||||||
})
|
|
||||||
// TODO: Query for existing db with the same name
|
|
||||||
const nickname = getUniq()
|
|
||||||
|
|
||||||
if (!defaultDatabaseName) defaultDatabaseName = nickname
|
|
||||||
|
|
||||||
reply.code(201).send({ message: 'Deploying.' })
|
|
||||||
const deployId = cuid()
|
|
||||||
const configuration = {
|
|
||||||
general: {
|
|
||||||
workdir: `/tmp/${deployId}`,
|
|
||||||
deployId,
|
|
||||||
nickname,
|
|
||||||
type
|
|
||||||
},
|
|
||||||
database: {
|
|
||||||
usernames,
|
|
||||||
passwords,
|
|
||||||
defaultDatabaseName
|
|
||||||
},
|
|
||||||
deploy: {
|
|
||||||
name: nickname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
|
|
||||||
let generateEnvs = {}
|
|
||||||
let image = null
|
|
||||||
let volume = null
|
|
||||||
let ulimits = {}
|
|
||||||
if (type === 'mongodb') {
|
|
||||||
generateEnvs = {
|
|
||||||
MONGODB_ROOT_PASSWORD: passwords[0],
|
|
||||||
MONGODB_USERNAME: usernames[0],
|
|
||||||
MONGODB_PASSWORD: passwords[1],
|
|
||||||
MONGODB_DATABASE: defaultDatabaseName
|
|
||||||
}
|
|
||||||
image = 'bitnami/mongodb:4.4'
|
|
||||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb`
|
|
||||||
} else if (type === 'postgresql') {
|
|
||||||
generateEnvs = {
|
|
||||||
POSTGRESQL_PASSWORD: passwords[0],
|
|
||||||
POSTGRESQL_USERNAME: usernames[0],
|
|
||||||
POSTGRESQL_DATABASE: defaultDatabaseName
|
|
||||||
}
|
|
||||||
image = 'bitnami/postgresql:13.2.0'
|
|
||||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql`
|
|
||||||
} else if (type === 'couchdb') {
|
|
||||||
generateEnvs = {
|
|
||||||
COUCHDB_PASSWORD: passwords[0],
|
|
||||||
COUCHDB_USER: usernames[0]
|
|
||||||
}
|
|
||||||
image = 'bitnami/couchdb:3'
|
|
||||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb`
|
|
||||||
} else if (type === 'mysql') {
|
|
||||||
generateEnvs = {
|
|
||||||
MYSQL_ROOT_PASSWORD: passwords[0],
|
|
||||||
MYSQL_ROOT_USER: usernames[0],
|
|
||||||
MYSQL_USER: usernames[1],
|
|
||||||
MYSQL_PASSWORD: passwords[1],
|
|
||||||
MYSQL_DATABASE: defaultDatabaseName
|
|
||||||
}
|
|
||||||
image = 'bitnami/mysql:8.0'
|
|
||||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`
|
|
||||||
} else if (type === 'clickhouse') {
|
|
||||||
image = 'yandex/clickhouse-server'
|
|
||||||
volume = `${configuration.general.deployId}-${type}-data:/var/lib/clickhouse`
|
|
||||||
ulimits = {
|
|
||||||
nofile: {
|
|
||||||
soft: 262144,
|
|
||||||
hard: 262144
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stack = {
|
|
||||||
version: '3.8',
|
|
||||||
services: {
|
|
||||||
[configuration.general.deployId]: {
|
|
||||||
image,
|
|
||||||
networks: [`${docker.network}`],
|
|
||||||
environment: generateEnvs,
|
|
||||||
volumes: [volume],
|
|
||||||
ulimits,
|
|
||||||
deploy: {
|
|
||||||
replicas: 1,
|
|
||||||
update_config: {
|
|
||||||
parallelism: 0,
|
|
||||||
delay: '10s',
|
|
||||||
order: 'start-first'
|
|
||||||
},
|
|
||||||
rollback_config: {
|
|
||||||
parallelism: 0,
|
|
||||||
delay: '10s',
|
|
||||||
order: 'start-first'
|
|
||||||
},
|
|
||||||
labels: [
|
|
||||||
'managedBy=coolify',
|
|
||||||
'type=database',
|
|
||||||
'configuration=' + JSON.stringify(configuration)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
networks: {
|
|
||||||
[`${docker.network}`]: {
|
|
||||||
external: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
volumes: {
|
|
||||||
[`${configuration.general.deployId}-${type}-data`]: {
|
|
||||||
external: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
|
|
||||||
await execShellAsync(
|
|
||||||
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
await saveServerLog(error)
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fastify.delete('/:dbName', async (request, reply) => {
|
|
||||||
const { dbName } = request.params
|
|
||||||
await execShellAsync(`docker stack rm ${dbName}`)
|
|
||||||
reply.code(200).send({})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
const axios = require('axios')
|
|
||||||
const User = require('../../../models/User')
|
|
||||||
const Settings = require('../../../models/Settings')
|
|
||||||
const cuid = require('cuid')
|
|
||||||
const mongoose = require('mongoose')
|
|
||||||
const jwt = require('jsonwebtoken')
|
|
||||||
const { saveServerLog } = require('../../../libs/logging')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
const githubCodeSchema = {
|
|
||||||
schema: {
|
|
||||||
querystring: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
code: { type: 'string' }
|
|
||||||
},
|
|
||||||
required: ['code']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fastify.get('/app', { schema: githubCodeSchema }, async (request, reply) => {
|
|
||||||
const { code } = request.query
|
|
||||||
try {
|
|
||||||
const { data } = await axios({
|
|
||||||
method: 'post',
|
|
||||||
url: `https://github.com/login/oauth/access_token?client_id=${fastify.config.VITE_GITHUB_APP_CLIENTID}&client_secret=${fastify.config.GITHUB_APP_CLIENT_SECRET}&code=${code}`,
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const token = data.access_token
|
|
||||||
const githubAxios = axios.create({
|
|
||||||
baseURL: 'https://api.github.com'
|
|
||||||
})
|
|
||||||
|
|
||||||
githubAxios.defaults.headers.common.Accept = 'Application/json'
|
|
||||||
githubAxios.defaults.headers.common.Authorization = `token ${token}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
let uid = cuid()
|
|
||||||
const { avatar_url } = (await githubAxios.get('/user')).data // eslint-disable-line
|
|
||||||
const email = (await githubAxios.get('/user/emails')).data.filter(
|
|
||||||
(e) => e.primary
|
|
||||||
)[0].email
|
|
||||||
const settings = await Settings.findOne({ applicationName: 'coolify' })
|
|
||||||
const registeredUsers = await User.find().countDocuments()
|
|
||||||
const foundUser = await User.findOne({ email })
|
|
||||||
if (foundUser) {
|
|
||||||
await User.findOneAndUpdate(
|
|
||||||
{ email },
|
|
||||||
{ avatar: avatar_url },
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
)
|
|
||||||
uid = foundUser.uid
|
|
||||||
} else {
|
|
||||||
if (registeredUsers === 0) {
|
|
||||||
const newUser = new User({
|
|
||||||
_id: new mongoose.Types.ObjectId(),
|
|
||||||
email,
|
|
||||||
avatar: avatar_url,
|
|
||||||
uid
|
|
||||||
})
|
|
||||||
const defaultSettings = new Settings({
|
|
||||||
_id: new mongoose.Types.ObjectId()
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
await newUser.save()
|
|
||||||
await defaultSettings.save()
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
reply.code(500).send({ success: false, error: e })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!settings && registeredUsers > 0) {
|
|
||||||
reply.code(500).send('Registration disabled, enable it in settings.')
|
|
||||||
} else {
|
|
||||||
if (!settings.allowRegistration) {
|
|
||||||
reply.code(500).send('You are not allowed here!')
|
|
||||||
} else {
|
|
||||||
const newUser = new User({
|
|
||||||
_id: new mongoose.Types.ObjectId(),
|
|
||||||
email,
|
|
||||||
avatar: avatar_url,
|
|
||||||
uid
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
await newUser.save()
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
reply.code(500).send({ success: false, error: e })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const jwtToken = jwt.sign({}, fastify.config.JWT_SIGN_KEY, {
|
|
||||||
expiresIn: 15778800,
|
|
||||||
algorithm: 'HS256',
|
|
||||||
audience: 'coolLabs',
|
|
||||||
issuer: 'coolLabs',
|
|
||||||
jwtid: uid,
|
|
||||||
subject: `User:${uid}`,
|
|
||||||
notBefore: -1000
|
|
||||||
})
|
|
||||||
reply
|
|
||||||
.code(200)
|
|
||||||
.redirect(
|
|
||||||
302,
|
|
||||||
`/api/v1/login/github/success?jwtToken=${jwtToken}&ghToken=${token}`
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
reply.code(500).send({ success: false, error: e })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await saveServerLog(error)
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
fastify.get('/success', async (request, reply) => {
|
|
||||||
return reply.sendFile('bye.html')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const Server = require('../../../models/Logs/Server')
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.get('/', async (request, reply) => {
|
|
||||||
try {
|
|
||||||
const serverLogs = await Server.find().select('-_id -__v')
|
|
||||||
// TODO: Should do better
|
|
||||||
return {
|
|
||||||
serverLogs
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
const { plausible, activateAdminUser } = require('../../../libs/services/plausible')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.post('/plausible', async (request, reply) => {
|
|
||||||
let { email, userName, userPassword, baseURL } = request.body
|
|
||||||
const traefikURL = baseURL
|
|
||||||
baseURL = `https://${baseURL}`
|
|
||||||
await plausible({ email, userName, userPassword, baseURL, traefikURL })
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
fastify.patch('/plausible/activate', async (request, reply) => {
|
|
||||||
await activateAdminUser()
|
|
||||||
return 'OK'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
const { execShellAsync } = require('../../../libs/common')
|
|
||||||
const { docker } = require('../../../libs/docker')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.get('/:serviceName', async (request, reply) => {
|
|
||||||
const { serviceName } = request.params
|
|
||||||
try {
|
|
||||||
const service = (await docker.engine.listServices()).find(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'service' && r.Spec.Labels.serviceName === serviceName && r.Spec.Name === `${serviceName}_${serviceName}`)
|
|
||||||
if (service) {
|
|
||||||
const payload = {
|
|
||||||
config: JSON.parse(service.Spec.Labels.configuration)
|
|
||||||
}
|
|
||||||
reply.code(200).send(payload)
|
|
||||||
} else {
|
|
||||||
throw new Error()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
throw new Error('No service found?')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
fastify.delete('/:serviceName', async (request, reply) => {
|
|
||||||
const { serviceName } = request.params
|
|
||||||
await execShellAsync(`docker stack rm ${serviceName}`)
|
|
||||||
reply.code(200).send({})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const Settings = require('../../../models/Settings')
|
|
||||||
const { saveServerLog } = require('../../../libs/logging')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
const applicationName = 'coolify'
|
|
||||||
const postSchema = {
|
|
||||||
body: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
allowRegistration: { type: 'boolean' },
|
|
||||||
sendErrors: { type: 'boolean' }
|
|
||||||
},
|
|
||||||
required: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fastify.get('/', async (request, reply) => {
|
|
||||||
try {
|
|
||||||
let settings = await Settings.findOne({ applicationName }).select('-_id -__v')
|
|
||||||
// TODO: Should do better
|
|
||||||
if (!settings) {
|
|
||||||
settings = {
|
|
||||||
applicationName,
|
|
||||||
allowRegistration: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
settings
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await saveServerLog(error)
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fastify.post('/', { schema: postSchema }, async (request, reply) => {
|
|
||||||
try {
|
|
||||||
const settings = await Settings.findOneAndUpdate(
|
|
||||||
{ applicationName },
|
|
||||||
{ applicationName, ...request.body },
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
).select('-_id -__v')
|
|
||||||
reply.code(201).send({ settings })
|
|
||||||
} catch (error) {
|
|
||||||
await saveServerLog(error)
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.get('/', async (request, reply) => {
|
|
||||||
reply.code(200).send('NO')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
const { execShellAsync } = require('../../../libs/common')
|
|
||||||
const { saveServerLog } = require('../../../libs/logging')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.get('/', async (request, reply) => {
|
|
||||||
const upgradeP1 = await execShellAsync('bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p1.sh)"')
|
|
||||||
await saveServerLog({ message: upgradeP1, type: 'UPGRADE-P-1' })
|
|
||||||
reply.code(200).send('I\'m trying, okay?')
|
|
||||||
const upgradeP2 = await execShellAsync('docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -u root coolify bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p2.sh)"')
|
|
||||||
await saveServerLog({ message: upgradeP2, type: 'UPGRADE-P-2' })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
const User = require('../../models/User')
|
|
||||||
const jwt = require('jsonwebtoken')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
fastify.get('/', async (request, reply) => {
|
|
||||||
try {
|
|
||||||
const { authorization } = request.headers
|
|
||||||
if (!authorization) {
|
|
||||||
reply.code(401).send({})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const token = authorization.split(' ')[1]
|
|
||||||
const verify = jwt.verify(token, fastify.config.JWT_SIGN_KEY)
|
|
||||||
const found = await User.findOne({ uid: verify.jti })
|
|
||||||
found ? reply.code(200).send({}) : reply.code(401).send({})
|
|
||||||
} catch (error) {
|
|
||||||
reply.code(401).send({})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
const crypto = require('crypto')
|
|
||||||
const { cleanupTmp } = require('../../../libs/common')
|
|
||||||
|
|
||||||
const Deployment = require('../../../models/Deployment')
|
|
||||||
const ApplicationLog = require('../../../models/Logs/Application')
|
|
||||||
const ServerLog = require('../../../models/Logs/Server')
|
|
||||||
|
|
||||||
const { queueAndBuild } = require('../../../libs/applications')
|
|
||||||
const { setDefaultConfiguration, precheckDeployment } = require('../../../libs/applications/configuration')
|
|
||||||
const { docker } = require('../../../libs/docker')
|
|
||||||
const cloneRepository = require('../../../libs/applications/github/cloneRepository')
|
|
||||||
const { purgeImagesContainers } = require('../../../libs/applications/cleanup')
|
|
||||||
|
|
||||||
module.exports = async function (fastify) {
|
|
||||||
const postSchema = {
|
|
||||||
body: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
ref: { type: 'string' },
|
|
||||||
repository: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'number' },
|
|
||||||
full_name: { type: 'string' }
|
|
||||||
},
|
|
||||||
required: ['id', 'full_name']
|
|
||||||
},
|
|
||||||
installation: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'number' }
|
|
||||||
},
|
|
||||||
required: ['id']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ['ref', 'repository', 'installation']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fastify.post('/', { schema: postSchema }, async (request, reply) => {
|
|
||||||
let configuration
|
|
||||||
const hmac = crypto.createHmac('sha256', fastify.config.GITHUP_APP_WEBHOOK_SECRET)
|
|
||||||
const digest = Buffer.from('sha256=' + hmac.update(JSON.stringify(request.body)).digest('hex'), 'utf8')
|
|
||||||
const checksum = Buffer.from(request.headers['x-hub-signature-256'], 'utf8')
|
|
||||||
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
|
|
||||||
reply.code(500).send({ error: 'Invalid request' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.headers['x-github-event'] !== 'push') {
|
|
||||||
reply.code(500).send({ error: 'Not a push event.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
|
||||||
|
|
||||||
configuration = services.find(r => {
|
|
||||||
if (request.body.ref.startsWith('refs')) {
|
|
||||||
const branch = request.body.ref.split('/')[2]
|
|
||||||
if (
|
|
||||||
JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
|
|
||||||
JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
|
|
||||||
) {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
if (!configuration) {
|
|
||||||
reply.code(500).send({ error: 'No configuration found.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration))
|
|
||||||
await cloneRepository(configuration)
|
|
||||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration })
|
|
||||||
|
|
||||||
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
|
|
||||||
cleanupTmp(configuration.general.workdir)
|
|
||||||
reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const alreadyQueued = await Deployment.find({
|
|
||||||
repoId: configuration.repository.id,
|
|
||||||
branch: configuration.repository.branch,
|
|
||||||
organization: configuration.repository.organization,
|
|
||||||
name: configuration.repository.name,
|
|
||||||
domain: configuration.publish.domain,
|
|
||||||
progress: { $in: ['queued', 'inprogress'] }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (alreadyQueued.length > 0) {
|
|
||||||
reply.code(200).send({ message: 'Already in the queue.' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name })
|
|
||||||
await queueAndBuild(configuration, imageChanged)
|
|
||||||
} catch (error) {
|
|
||||||
const { id, organization, name, branch } = configuration.repository
|
|
||||||
const { domain } = configuration.publish
|
|
||||||
const { deployId } = configuration.general
|
|
||||||
await Deployment.findOneAndUpdate(
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain },
|
|
||||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' })
|
|
||||||
if (error.name === 'Error') {
|
|
||||||
// Error during runtime
|
|
||||||
await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
|
|
||||||
} else {
|
|
||||||
// Error in my code
|
|
||||||
const payload = { message: error.message, stack: error.stack, type: 'spaghetticode' }
|
|
||||||
if (error.message && error.stack) await new ServerLog(payload).save()
|
|
||||||
if (reply.sent) await new ApplicationLog({ repoId: id, branch, deployId, event: `[ERROR 😖]: ${error.stack}` }).save()
|
|
||||||
}
|
|
||||||
throw new Error(error)
|
|
||||||
} finally {
|
|
||||||
cleanupTmp(configuration.general.workdir)
|
|
||||||
await purgeImagesContainers(configuration)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const schema = {
|
|
||||||
type: 'object',
|
|
||||||
required: [
|
|
||||||
'DOMAIN',
|
|
||||||
'EMAIL',
|
|
||||||
'VITE_GITHUB_APP_CLIENTID',
|
|
||||||
'GITHUB_APP_CLIENT_SECRET',
|
|
||||||
'GITHUB_APP_PRIVATE_KEY',
|
|
||||||
'GITHUP_APP_WEBHOOK_SECRET',
|
|
||||||
'JWT_SIGN_KEY',
|
|
||||||
'SECRETS_ENCRYPTION_KEY'
|
|
||||||
],
|
|
||||||
properties: {
|
|
||||||
DOMAIN: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
EMAIL: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
VITE_GITHUB_APP_CLIENTID: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
GITHUB_APP_CLIENT_SECRET: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
GITHUB_APP_PRIVATE_KEY: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
GITHUP_APP_WEBHOOK_SECRET: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
JWT_SIGN_KEY: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
DOCKER_ENGINE: {
|
|
||||||
type: 'string',
|
|
||||||
default: '/var/run/docker.sock'
|
|
||||||
},
|
|
||||||
DOCKER_NETWORK: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'coollabs'
|
|
||||||
},
|
|
||||||
SECRETS_ENCRYPTION_KEY: {
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { schema }
|
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
require('dotenv').config()
|
|
||||||
const fs = require('fs')
|
|
||||||
const util = require('util')
|
|
||||||
const mongoose = require('mongoose')
|
|
||||||
const path = require('path')
|
|
||||||
const { saveServerLog } = require('./libs/logging')
|
|
||||||
const { execShellAsync } = require('./libs/common')
|
|
||||||
const { cleanupStuckedDeploymentsInDB } = require('./libs/applications/cleanup')
|
|
||||||
const fastify = require('fastify')({
|
|
||||||
trustProxy: true,
|
|
||||||
logger: {
|
|
||||||
level: 'error'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
fastify.register(require('../api/libs/http-error'))
|
|
||||||
|
|
||||||
const { schema } = require('./schema')
|
|
||||||
|
|
||||||
process.on('unhandledRejection', async (reason, p) => {
|
|
||||||
await saveServerLog({ message: reason.message, type: 'unhandledRejection' })
|
|
||||||
})
|
|
||||||
|
|
||||||
fastify.register(require('fastify-env'), {
|
|
||||||
schema,
|
|
||||||
dotenv: true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
fastify.register(require('fastify-static'), {
|
|
||||||
root: path.join(__dirname, '../dist/')
|
|
||||||
})
|
|
||||||
|
|
||||||
fastify.setNotFoundHandler(function (request, reply) {
|
|
||||||
reply.sendFile('index.html')
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
fastify.register(require('fastify-static'), {
|
|
||||||
root: path.join(__dirname, '../public/')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fastify.register(require('./app'), { prefix: '/api/v1' })
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
mongoose.connect(
|
|
||||||
`mongodb://${process.env.MONGODB_USER}:${process.env.MONGODB_PASSWORD}@${process.env.MONGODB_HOST}:${process.env.MONGODB_PORT}/${process.env.MONGODB_DB}?authSource=${process.env.MONGODB_DB}&readPreference=primary&ssl=false`,
|
|
||||||
{ useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
mongoose.connect(
|
|
||||||
'mongodb://localhost:27017/coolify?&readPreference=primary&ssl=false',
|
|
||||||
{ useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
mongoose.connection.on(
|
|
||||||
'error',
|
|
||||||
console.error.bind(console, 'connection error:')
|
|
||||||
)
|
|
||||||
mongoose.connection.once('open', async function () {
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
fastify.listen(3000, '0.0.0.0')
|
|
||||||
console.log('Coolify API is up and running in production.')
|
|
||||||
} else {
|
|
||||||
const logFile = fs.createWriteStream('api/development/console.log', { flags: 'w' })
|
|
||||||
const logStdout = process.stdout
|
|
||||||
|
|
||||||
console.log = function (d) {
|
|
||||||
logFile.write(`[INFO]: ${util.format(d)}\n`)
|
|
||||||
logStdout.write(util.format(d) + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error = function (d) {
|
|
||||||
logFile.write(`[ERROR]: ${util.format(d)}\n`)
|
|
||||||
logStdout.write(util.format(d) + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn = function (d) {
|
|
||||||
logFile.write(`[WARN]: ${util.format(d)}\n`)
|
|
||||||
logStdout.write(util.format(d) + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
fastify.listen(3001)
|
|
||||||
console.log('Coolify API is up and running in development.')
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Always cleanup server logs
|
|
||||||
await mongoose.connection.db.dropCollection('logs-servers')
|
|
||||||
} catch (error) {
|
|
||||||
// Could not cleanup logs-servers collection
|
|
||||||
}
|
|
||||||
// On start cleanup inprogress/queued deployments.
|
|
||||||
try {
|
|
||||||
await cleanupStuckedDeploymentsInDB()
|
|
||||||
} catch (error) {
|
|
||||||
// Could not cleanup DB 🤔
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Doing because I do not want to prune these images. Prune skips coolify-reserve labeled images.
|
|
||||||
const basicImages = ['nginx:stable-alpine', 'node:lts', 'ubuntu:20.04', 'php:apache', 'rust:latest']
|
|
||||||
for (const image of basicImages) {
|
|
||||||
// await execShellAsync(`echo "FROM ${image}" | docker build --label coolify-reserve=true -t ${image} -`)
|
|
||||||
await execShellAsync(`docker pull ${image}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Could not pull some basic images from Docker Hub.')
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
proxy:
|
||||||
|
image: traefik:v2.4
|
||||||
|
hostname: coollabs-proxy
|
||||||
|
ports:
|
||||||
|
- target: 80
|
||||||
|
published: 80
|
||||||
|
protocol: tcp
|
||||||
|
mode: host
|
||||||
|
- target: 443
|
||||||
|
published: 443
|
||||||
|
protocol: tcp
|
||||||
|
mode: host
|
||||||
|
- target: 8080
|
||||||
|
published: 8080
|
||||||
|
protocol: tcp
|
||||||
|
mode: host
|
||||||
|
command:
|
||||||
|
- --api.insecure=true
|
||||||
|
- --api.dashboard=true
|
||||||
|
- --api.debug=true
|
||||||
|
- --log.level=ERROR
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.swarmMode=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
- --providers.docker.network=coollabs
|
||||||
|
- --providers.docker.swarmModeRefreshSeconds=1s
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --entrypoints.websecure.address=:443
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
networks:
|
||||||
|
- coollabs
|
||||||
|
deploy:
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
delay: 10s
|
||||||
|
order: start-first
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.role == manager
|
||||||
|
labels:
|
||||||
|
- 'traefik.enable=true'
|
||||||
|
- 'traefik.http.routers.api.entrypoints=websecure'
|
||||||
|
- 'traefik.http.routers.api.service=api@internal'
|
||||||
|
- 'traefik.http.routers.api.middlewares=auth'
|
||||||
|
- 'traefik.http.services.traefik.loadbalancer.server.port=80'
|
||||||
|
|
||||||
|
# Global redirect www to non-www
|
||||||
|
- 'traefik.http.routers.www-catchall.rule=hostregexp(`{host:www.(.+)}`)'
|
||||||
|
- 'traefik.http.routers.www-catchall.entrypoints=web'
|
||||||
|
- 'traefik.http.routers.www-catchall.middlewares=redirect-www-to-nonwww'
|
||||||
|
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.regex=^http://(?:www\\.)?(.+)"
|
||||||
|
- 'traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.replacement=http://$${1}'
|
||||||
|
mongodb:
|
||||||
|
image: bitnami/mongodb:4.4
|
||||||
|
hostname: coollabs-mongodb
|
||||||
|
ports:
|
||||||
|
- target: 27017
|
||||||
|
published: 27017
|
||||||
|
protocol: tcp
|
||||||
|
mode: host
|
||||||
|
environment:
|
||||||
|
- MONGODB_DISABLE_SYSTEM_LOG=true
|
||||||
|
- MONGODB_ROOT_PASSWORD=developmentPassword4db
|
||||||
|
- MONGODB_USERNAME=supercooldbuser
|
||||||
|
- MONGODB_PASSWORD=developmentPassword4db
|
||||||
|
- MONGODB_DATABASE=coolify
|
||||||
|
volumes:
|
||||||
|
- coollabs-mongodb-data:/bitnami/mongodb
|
||||||
|
networks:
|
||||||
|
- coollabs
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
coollabs-mongodb-data: {}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
coollabs:
|
||||||
|
driver: overlay
|
||||||
|
name: coollabs
|
||||||
|
external: true
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" href="/favicon.png" />
|
|
||||||
<link rel="preload" as="image" href="/favicon.png">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Coolify</title>
|
|
||||||
<link rel="dns-prefetch" href="https://cdn.coollabs.io/" />
|
|
||||||
<link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" />
|
|
||||||
<link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdn.coollabs.io/css/microtip-0.2.2.min.css" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<script type="module" src="/src/index.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
FROM node:latest
|
||||||
|
LABEL coolify-preserve=true
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
RUN curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-20.10.6.tgz | tar -xzvf - docker/docker -C . --strip-components 1
|
||||||
|
RUN mv /usr/src/app/docker /usr/bin/docker
|
||||||
|
RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o /usr/bin/envsubst
|
||||||
|
RUN curl -L https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o /usr/bin/jq
|
||||||
|
RUN chmod +x /usr/bin/envsubst /usr/bin/jq /usr/bin/docker
|
||||||
|
ADD "https://www.random.org/cgi-bin/randbyte?nbytes=10&format=h" skipcache
|
||||||
|
RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
require('dotenv').config()
|
|
||||||
const fastify = require('fastify')()
|
|
||||||
const { schema } = require('../api/schema')
|
|
||||||
|
|
||||||
checkConfig().then(() => {
|
|
||||||
console.log('Config: OK')
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log('Config: NOT OK')
|
|
||||||
console.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
function checkConfig () {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fastify.register(require('fastify-env'), {
|
|
||||||
schema,
|
|
||||||
dotenv: true
|
|
||||||
})
|
|
||||||
.ready((err) => {
|
|
||||||
if (err) reject(err)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
proxy:
|
|
||||||
image: traefik:v2.4
|
|
||||||
hostname: coollabs-proxy
|
|
||||||
ports:
|
|
||||||
- target: 80
|
|
||||||
published: 80
|
|
||||||
protocol: tcp
|
|
||||||
mode: host
|
|
||||||
- target: 443
|
|
||||||
published: 443
|
|
||||||
protocol: tcp
|
|
||||||
mode: host
|
|
||||||
- target: 8080
|
|
||||||
published: 8080
|
|
||||||
protocol: tcp
|
|
||||||
mode: host
|
|
||||||
command:
|
|
||||||
- --api.insecure=true
|
|
||||||
- --api.dashboard=true
|
|
||||||
- --api.debug=true
|
|
||||||
- --log.level=ERROR
|
|
||||||
- --providers.docker=true
|
|
||||||
- --providers.docker.swarmMode=true
|
|
||||||
- --providers.docker.exposedbydefault=false
|
|
||||||
- --providers.docker.network=${DOCKER_NETWORK}
|
|
||||||
- --providers.docker.swarmModeRefreshSeconds=1s
|
|
||||||
- --entrypoints.web.address=:80
|
|
||||||
- --entrypoints.websecure.address=:443
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
networks:
|
|
||||||
- ${DOCKER_NETWORK}
|
|
||||||
deploy:
|
|
||||||
update_config:
|
|
||||||
parallelism: 1
|
|
||||||
delay: 10s
|
|
||||||
order: start-first
|
|
||||||
replicas: 1
|
|
||||||
placement:
|
|
||||||
constraints:
|
|
||||||
- node.role == manager
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.api.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.api.service=api@internal"
|
|
||||||
- "traefik.http.routers.api.middlewares=auth"
|
|
||||||
- "traefik.http.services.traefik.loadbalancer.server.port=80"
|
|
||||||
- "traefik.http.services.traefik.loadbalancer.server.port=443"
|
|
||||||
|
|
||||||
# Global redirect www to non-www
|
|
||||||
- "traefik.http.routers.www-catchall.rule=hostregexp(`{host:www.(.+)}`)"
|
|
||||||
- "traefik.http.routers.www-catchall.entrypoints=web"
|
|
||||||
- "traefik.http.routers.www-catchall.middlewares=redirect-www-to-nonwww"
|
|
||||||
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.regex=^http://(?:www\\.)?(.+)"
|
|
||||||
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.replacement=http://$$$${1}"
|
|
||||||
|
|
||||||
# Global redirect http to https
|
|
||||||
- "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
|
|
||||||
- "traefik.http.routers.http-catchall.entrypoints=web"
|
|
||||||
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
|
|
||||||
|
|
||||||
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
|
|
||||||
- "traefik.http.middlewares.global-compress.compress=true"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
${DOCKER_NETWORK}:
|
|
||||||
driver: overlay
|
|
||||||
name: ${DOCKER_NETWORK}
|
|
||||||
external: true
|
|
||||||
|
|
||||||
@@ -44,27 +44,27 @@ services:
|
|||||||
constraints:
|
constraints:
|
||||||
- node.role == manager
|
- node.role == manager
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- 'traefik.enable=true'
|
||||||
- "traefik.http.routers.api.entrypoints=websecure"
|
- 'traefik.http.routers.api.entrypoints=websecure'
|
||||||
- "traefik.http.routers.api.service=api@internal"
|
- 'traefik.http.routers.api.service=api@internal'
|
||||||
- "traefik.http.routers.api.middlewares=auth"
|
- 'traefik.http.routers.api.middlewares=auth'
|
||||||
- "traefik.http.services.traefik.loadbalancer.server.port=80"
|
- 'traefik.http.services.traefik.loadbalancer.server.port=80'
|
||||||
- "traefik.http.services.traefik.loadbalancer.server.port=443"
|
- 'traefik.http.services.traefik.loadbalancer.server.port=443'
|
||||||
|
|
||||||
# Global redirect www to non-www
|
# Global redirect www to non-www
|
||||||
- "traefik.http.routers.www-catchall.rule=hostregexp(`{host:www.(.+)}`)"
|
- 'traefik.http.routers.www-catchall.rule=hostregexp(`{host:www.(.+)}`)'
|
||||||
- "traefik.http.routers.www-catchall.entrypoints=web"
|
- 'traefik.http.routers.www-catchall.entrypoints=web'
|
||||||
- "traefik.http.routers.www-catchall.middlewares=redirect-www-to-nonwww"
|
- 'traefik.http.routers.www-catchall.middlewares=redirect-www-to-nonwww'
|
||||||
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.regex=^http://(?:www\\.)?(.+)"
|
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.regex=^http://(?:www\\.)?(.+)"
|
||||||
- "traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.replacement=http://$$$${1}"
|
- 'traefik.http.middlewares.redirect-www-to-nonwww.redirectregex.replacement=http://$$$${1}'
|
||||||
|
|
||||||
# Global redirect http to https
|
# Global redirect http to https
|
||||||
- "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
|
- 'traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)'
|
||||||
- "traefik.http.routers.http-catchall.entrypoints=web"
|
- 'traefik.http.routers.http-catchall.entrypoints=web'
|
||||||
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
|
- 'traefik.http.routers.http-catchall.middlewares=redirect-to-https'
|
||||||
|
|
||||||
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
|
- 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'
|
||||||
- "traefik.http.middlewares.global-compress.compress=true"
|
- 'traefik.http.middlewares.global-compress.compress=true'
|
||||||
|
|
||||||
coolify:
|
coolify:
|
||||||
image: coolify
|
image: coolify
|
||||||
@@ -73,7 +73,7 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
networks:
|
networks:
|
||||||
- ${DOCKER_NETWORK}
|
- ${DOCKER_NETWORK}
|
||||||
command: "yarn start"
|
command: 'yarn start'
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
deploy:
|
deploy:
|
||||||
@@ -83,16 +83,15 @@ services:
|
|||||||
order: start-first
|
order: start-first
|
||||||
replicas: 1
|
replicas: 1
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- 'traefik.enable=true'
|
||||||
- "traefik.http.routers.coolify.entrypoints=websecure"
|
- 'traefik.http.routers.coolify.entrypoints=websecure'
|
||||||
- "traefik.http.routers.coolify.tls.certresolver=letsencrypt"
|
- 'traefik.http.routers.coolify.tls.certresolver=letsencrypt'
|
||||||
- "traefik.http.routers.coolify.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
|
- 'traefik.http.routers.coolify.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)'
|
||||||
- "traefik.http.services.coolify.loadbalancer.server.port=3000"
|
- 'traefik.http.services.coolify.loadbalancer.server.port=3000'
|
||||||
- "traefik.http.routers.coolify.middlewares=global-compress"
|
- 'traefik.http.routers.coolify.middlewares=global-compress'
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
${DOCKER_NETWORK}:
|
${DOCKER_NETWORK}:
|
||||||
driver: overlay
|
driver: overlay
|
||||||
name: ${DOCKER_NETWORK}
|
name: ${DOCKER_NETWORK}
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
+30
-49
@@ -1,55 +1,36 @@
|
|||||||
require('dotenv').config()
|
require('dotenv').config();
|
||||||
const { program } = require('commander')
|
const { program } = require('commander');
|
||||||
const fastify = require('fastify')()
|
const shell = require('shelljs');
|
||||||
const { schema } = require('../api/schema')
|
const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '');
|
||||||
const shell = require('shelljs')
|
|
||||||
const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '')
|
|
||||||
|
|
||||||
program.version('0.0.1')
|
program.version('0.0.1');
|
||||||
program
|
program
|
||||||
.option('-d, --debug', 'Debug outputs.')
|
.option('-d, --debug', 'Debug outputs.')
|
||||||
.option('-c, --check', 'Only checks configuration.')
|
.option('-c, --check', 'Only checks configuration.')
|
||||||
.option('-t, --type <type>', 'Deploy type.')
|
.option('-t, --type <type>', 'Deploy type.');
|
||||||
|
|
||||||
program.parse(process.argv)
|
program.parse(process.argv);
|
||||||
|
|
||||||
const options = program.opts()
|
const options = program.opts();
|
||||||
if (options.check) {
|
|
||||||
checkConfig().then(() => {
|
if (user !== 'root') {
|
||||||
console.log('Config: OK')
|
console.error(`Please run as root! Current user: ${user}`);
|
||||||
}).catch((err) => {
|
process.exit(1);
|
||||||
console.log('Config: NOT OK')
|
|
||||||
console.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (user !== 'root') {
|
|
||||||
console.error(`Please run as root! Current user: ${user}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
shell.exec(`docker network create ${process.env.DOCKER_NETWORK} --driver overlay`, { silent: !options.debug })
|
|
||||||
shell.exec('docker build -t coolify -f install/Dockerfile .')
|
|
||||||
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 (options.type === 'proxy') {
|
|
||||||
shell.exec('docker service rm coollabs-coolify_proxy')
|
|
||||||
}
|
|
||||||
if (options.type !== 'upgrade') {
|
|
||||||
shell.exec('set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify', { silent: !options.debug, shell: '/bin/bash' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
shell.exec(`docker network create ${process.env.DOCKER_NETWORK} --driver overlay`, {
|
||||||
function checkConfig () {
|
silent: !options.debug
|
||||||
return new Promise((resolve, reject) => {
|
});
|
||||||
fastify.register(require('fastify-env'), {
|
shell.exec('docker build -t coolify -f install/Dockerfile .');
|
||||||
schema,
|
if (options.type === 'all') {
|
||||||
dotenv: true
|
shell.exec('docker stack rm coollabs-coolify', { silent: !options.debug });
|
||||||
})
|
} else if (options.type === 'coolify') {
|
||||||
.ready((err) => {
|
shell.exec('docker service rm coollabs-coolify_coolify');
|
||||||
if (err) reject(err)
|
} else if (options.type === 'proxy') {
|
||||||
resolve()
|
shell.exec('docker service rm coollabs-coolify_proxy');
|
||||||
})
|
}
|
||||||
})
|
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' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
FROM coolify-base-nodejs
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
COPY . .
|
|
||||||
RUN pnpm install
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
FROM node:lts
|
|
||||||
LABEL coolify-preserve=true
|
|
||||||
COPY --from=coolify-binaries /usr/bin/docker /usr/bin/docker
|
|
||||||
COPY --from=coolify-binaries /usr/bin/envsubst /usr/bin/envsubst
|
|
||||||
COPY --from=coolify-binaries /usr/bin/jq /usr/bin/jq
|
|
||||||
RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm@6
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
FROM ubuntu:20.04
|
|
||||||
LABEL coolify-preserve=true
|
|
||||||
RUN apt update && apt install -y curl gnupg2 ca-certificates
|
|
||||||
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
|
|
||||||
RUN echo 'deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable' >> /etc/apt/sources.list
|
|
||||||
RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o /usr/bin/envsubst
|
|
||||||
RUN curl -L https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o /usr/bin/jq
|
|
||||||
RUN chmod +x /usr/bin/envsubst /usr/bin/jq
|
|
||||||
RUN apt update && apt install -y docker-ce-cli && apt clean all
|
|
||||||
+17
-14
@@ -1,21 +1,24 @@
|
|||||||
require('dotenv').config()
|
require('dotenv').config();
|
||||||
const { program } = require('commander')
|
const { program } = require('commander');
|
||||||
const shell = require('shelljs')
|
const shell = require('shelljs');
|
||||||
const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '')
|
const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '');
|
||||||
program.version('0.0.1')
|
program.version('0.0.1');
|
||||||
program
|
program
|
||||||
.option('-d, --debug', 'Debug outputs.')
|
.option('-d, --debug', 'Debug outputs.')
|
||||||
.option('-c, --check', 'Only checks configuration.')
|
.option('-c, --check', 'Only checks configuration.')
|
||||||
.option('-t, --type <type>', 'Deploy type.')
|
.option('-t, --type <type>', 'Deploy type.');
|
||||||
|
|
||||||
program.parse(process.argv)
|
program.parse(process.argv);
|
||||||
const options = program.opts()
|
const options = program.opts();
|
||||||
if (user !== 'root') {
|
if (user !== 'root') {
|
||||||
console.error(`Please run as root! Current user: ${user}`)
|
console.error(`Please run as root! Current user: ${user}`);
|
||||||
process.exit(1)
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.type === 'upgrade') {
|
if (options.type === 'upgrade') {
|
||||||
shell.exec('docker service rm coollabs-coolify_coolify')
|
shell.exec('docker service rm coollabs-coolify_coolify');
|
||||||
shell.exec('set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify', { silent: !options.debug, shell: '/bin/bash' })
|
shell.exec(
|
||||||
|
'set -a && source .env && set +a && envsubst < install/coolify-template.yml | docker stack deploy -c - coollabs-coolify',
|
||||||
|
{ silent: !options.debug, shell: '/bin/bash' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
+56
-66
@@ -1,68 +1,58 @@
|
|||||||
{
|
{
|
||||||
"name": "coolify",
|
"name": "coolify",
|
||||||
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
|
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
|
||||||
"version": "1.0.11",
|
"version": "1.0.12",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "standard",
|
"dev:docker:start": "docker-compose -f docker-compose-dev.yml up -d",
|
||||||
"start": "NODE_ENV=production node api/server",
|
"dev:docker:stop": "docker-compose -f docker-compose-dev.yml down",
|
||||||
"dev": "run-p dev:db dev:routify dev:svite dev:server",
|
"dev": "NODE_ENV=development svelte-kit dev --host 0.0.0.0",
|
||||||
"dev:db": "NODE_ENV=development node api/development/mongodb.js",
|
"build": "NODE_ENV=production svelte-kit build",
|
||||||
"dev:server": "nodemon -w api api/server",
|
"preview": "svelte-kit preview",
|
||||||
"dev:routify": "routify run",
|
"start": "node build",
|
||||||
"dev:svite": "svite",
|
"lint": "prettier --check . && eslint --ignore-path .gitignore .",
|
||||||
"build": "run-s build:routify build:svite",
|
"format": "prettier --write ."
|
||||||
"build:routify": "routify run -b",
|
},
|
||||||
"build:svite": "svite build"
|
"devDependencies": {
|
||||||
},
|
"@sveltejs/adapter-node": "^1.0.0-next.20",
|
||||||
"dependencies": {
|
"@sveltejs/kit": "1.0.0-next.107",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@types/dockerode": "^3.2.3",
|
||||||
"@roxi/routify": "^2.15.1",
|
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||||
"@zerodevx/svelte-toast": "^0.2.2",
|
"@typescript-eslint/parser": "^4.23.0",
|
||||||
"ajv": "^8.1.0",
|
"autoprefixer": "^10.2.5",
|
||||||
"axios": "^0.21.1",
|
"cssnano": "^5.0.2",
|
||||||
"commander": "^7.2.0",
|
"dotenv-extended": "^2.9.0",
|
||||||
"compare-versions": "^3.6.0",
|
"eslint": "^7.26.0",
|
||||||
"cuid": "^2.1.8",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"dayjs": "^1.10.4",
|
"eslint-plugin-svelte3": "^3.2.0",
|
||||||
"deepmerge": "^4.2.2",
|
"postcss": "^8.2.15",
|
||||||
"dockerode": "^3.2.1",
|
"postcss-load-config": "^3.0.1",
|
||||||
"dotenv": "^8.2.0",
|
"prettier": "~2.3.0",
|
||||||
"fastify": "^3.14.2",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"fastify-env": "^2.1.0",
|
"svelte": "^3.38.2",
|
||||||
"fastify-jwt": "^2.4.0",
|
"svelte-preprocess": "^4.7.3",
|
||||||
"fastify-plugin": "^3.0.0",
|
"tailwindcss": "canary",
|
||||||
"fastify-static": "^4.0.1",
|
"tslib": "^2.2.0",
|
||||||
"generate-password": "^1.6.0",
|
"typescript": "^4.2.4",
|
||||||
"http-errors-enhanced": "^0.7.0",
|
"vite": "^2.3.2"
|
||||||
"js-yaml": "^4.0.0",
|
},
|
||||||
"jsonwebtoken": "^8.5.1",
|
"type": "module",
|
||||||
"mongoose": "^5.12.3",
|
"dependencies": {
|
||||||
"shelljs": "^0.8.4",
|
"@iarna/toml": "^2.2.5",
|
||||||
"svelte-select": "^3.17.0",
|
"@zerodevx/svelte-toast": "^0.3.0",
|
||||||
"unique-names-generator": "^4.4.0"
|
"commander": "^7.2.0",
|
||||||
},
|
"compare-versions": "^3.6.0",
|
||||||
"devDependencies": {
|
"cookie": "^0.4.1",
|
||||||
"mongodb-memory-server-core": "^6.9.6",
|
"cuid": "^2.1.8",
|
||||||
"nodemon": "^2.0.7",
|
"dayjs": "^1.10.4",
|
||||||
"npm-run-all": "^4.1.5",
|
"dockerode": "^3.3.0",
|
||||||
"postcss": "^8.2.9",
|
"generate-password": "^1.6.0",
|
||||||
"postcss-import": "^14.0.1",
|
"js-yaml": "^4.1.0",
|
||||||
"postcss-load-config": "^3.0.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"mongoose": "^5.12.9",
|
||||||
"prettier": "2.2.1",
|
"shelljs": "^0.8.4",
|
||||||
"prettier-plugin-svelte": "^2.2.0",
|
"svelte-kit-cookie-session": "^0.4.3",
|
||||||
"standard": "^16.0.3",
|
"svelte-select": "^3.17.0",
|
||||||
"svelte": "^3.37.0",
|
"unique-names-generator": "^4.5.0"
|
||||||
"svelte-hmr": "^0.14.0",
|
}
|
||||||
"svelte-preprocess": "^4.7.0",
|
|
||||||
"svite": "0.8.1",
|
|
||||||
"tailwindcss": "2.1.1"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"svelte",
|
|
||||||
"routify",
|
|
||||||
"fastify",
|
|
||||||
"tailwind"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
Generated
+970
-4320
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,18 @@
|
|||||||
|
const tailwindcss = require('tailwindcss');
|
||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
const cssnano = require('cssnano');
|
||||||
|
|
||||||
|
const mode = process.env.NODE_ENV;
|
||||||
|
const dev = mode === 'development';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
tailwindcss,
|
||||||
|
autoprefixer,
|
||||||
|
|
||||||
|
!dev &&
|
||||||
|
cssnano({
|
||||||
|
preset: 'default'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: [
|
|
||||||
require('postcss-import'),
|
|
||||||
require('tailwindcss'),
|
|
||||||
require('postcss-preset-env')({ stage: 1 })
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<script>
|
|
||||||
window.close();
|
|
||||||
</script>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
routifyDir: '.routify',
|
|
||||||
dynamicImports: true,
|
|
||||||
extensions: ['svelte']
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { SvelteToast } from "@zerodevx/svelte-toast";
|
|
||||||
import { Router } from "@roxi/routify";
|
|
||||||
import { routes } from "../.routify/routes";
|
|
||||||
const options = {
|
|
||||||
duration: 2000
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
:global(.main) {
|
|
||||||
width: calc(100% - 4rem);
|
|
||||||
margin-left: 4rem;
|
|
||||||
}
|
|
||||||
:global(._toastMsg) {
|
|
||||||
@apply text-sm font-bold !important;
|
|
||||||
}
|
|
||||||
:global(._toastItem) {
|
|
||||||
@apply w-full border-l-2 border-green-600 !important;
|
|
||||||
}
|
|
||||||
:global(._toastBtn) {
|
|
||||||
@apply text-xs !important;
|
|
||||||
}
|
|
||||||
:global(._toastBtn:hover) {
|
|
||||||
@apply bg-gray-500 !important;
|
|
||||||
}
|
|
||||||
:global(.icon) {
|
|
||||||
@apply text-white rounded p-2 transition duration-100 !important;
|
|
||||||
}
|
|
||||||
:global(.icon:hover) {
|
|
||||||
@apply bg-warmGray-700 !important;
|
|
||||||
}
|
|
||||||
:global(input) {
|
|
||||||
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
|
|
||||||
}
|
|
||||||
:global(input:hover) {
|
|
||||||
@apply bg-warmGray-700 !important;
|
|
||||||
}
|
|
||||||
:global(textarea) {
|
|
||||||
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none resize-none !important;
|
|
||||||
}
|
|
||||||
:global(textarea:hover) {
|
|
||||||
@apply bg-warmGray-700 !important;
|
|
||||||
}
|
|
||||||
:global(select) {
|
|
||||||
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none !important;
|
|
||||||
}
|
|
||||||
:global(select:hover) {
|
|
||||||
@apply bg-warmGray-700 !important;
|
|
||||||
}
|
|
||||||
:global(label) {
|
|
||||||
@apply text-left text-base font-bold text-warmGray-400 !important;
|
|
||||||
}
|
|
||||||
:global(button) {
|
|
||||||
@apply outline-none !important;
|
|
||||||
}
|
|
||||||
:global(.button) {
|
|
||||||
@apply rounded text-sm font-bold transition-all duration-100 !important;
|
|
||||||
}
|
|
||||||
:global(.h-271) {
|
|
||||||
min-height: 271px !important;
|
|
||||||
}
|
|
||||||
:global(.repository-select-search .listItem .item),
|
|
||||||
:global(.repository-select-search .empty) {
|
|
||||||
@apply text-sm py-3 font-bold bg-warmGray-800 text-white cursor-pointer border-none hover:bg-warmGray-700 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.repository-select-search .listContainer) {
|
|
||||||
@apply bg-transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.repository-select-search .clearSelect) {
|
|
||||||
@apply text-white cursor-pointer !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.repository-select-search .selectedItem) {
|
|
||||||
@apply text-white relative cursor-pointer font-bold text-sm flex items-center !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<SvelteToast options="{options}" />
|
|
||||||
<Router routes="{routes}" />
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Coolify</title>
|
||||||
|
<link rel="dns-prefetch" href="https://cdn.coollabs.io/" />
|
||||||
|
<link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.coollabs.io/css/microtip-0.2.2.min.css" />
|
||||||
|
%svelte.head%
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="svelte">%svelte.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: rgb(22, 22, 22);
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
@apply text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--toastBackground: rgba(41, 37, 36, 0.8);
|
||||||
|
--toastProgressBackground: transparent;
|
||||||
|
--toastFont: 'Inter';
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-gradient {
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
border-image: linear-gradient(
|
||||||
|
0.25turn,
|
||||||
|
rgba(255, 249, 34),
|
||||||
|
rgba(255, 0, 128),
|
||||||
|
rgba(56, 2, 155, 0)
|
||||||
|
);
|
||||||
|
border-image-slice: 1;
|
||||||
|
}
|
||||||
|
.border-gradient-full {
|
||||||
|
border: 4px solid transparent;
|
||||||
|
border-image: linear-gradient(
|
||||||
|
0.25turn,
|
||||||
|
rgba(255, 249, 34),
|
||||||
|
rgba(255, 0, 128),
|
||||||
|
rgba(56, 2, 155, 0)
|
||||||
|
);
|
||||||
|
border-image-slice: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[aria-label][role~='tooltip']::after {
|
||||||
|
background: rgba(41, 37, 36, 0.9);
|
||||||
|
color: white;
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role~='tooltip'][data-microtip-position|='bottom']::before {
|
||||||
|
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
|
||||||
|
no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role~='tooltip'][data-microtip-position|='top']::before {
|
||||||
|
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
|
||||||
|
no-repeat;
|
||||||
|
}
|
||||||
|
[role~='tooltip'][data-microtip-position='right']::before {
|
||||||
|
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
|
||||||
|
no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role~='tooltip'][data-microtip-position='left']::before {
|
||||||
|
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
|
||||||
|
no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: calc(100% - 4rem);
|
||||||
|
margin-left: 4rem;
|
||||||
|
}
|
||||||
|
._toastMsg {
|
||||||
|
@apply text-sm font-bold !important;
|
||||||
|
}
|
||||||
|
._toastItem {
|
||||||
|
@apply w-full border-l-2 border-green-600 !important;
|
||||||
|
}
|
||||||
|
._toastBtn {
|
||||||
|
@apply text-xs !important;
|
||||||
|
}
|
||||||
|
._toastBtn:hover {
|
||||||
|
@apply bg-gray-500 !important;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
@apply text-white rounded p-2 transition duration-100;
|
||||||
|
}
|
||||||
|
.icon:hover {
|
||||||
|
@apply bg-warmGray-700;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
|
||||||
|
}
|
||||||
|
input:hover {
|
||||||
|
@apply bg-warmGray-700 !important;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none resize-none !important;
|
||||||
|
}
|
||||||
|
textarea:hover {
|
||||||
|
@apply bg-warmGray-700 !important;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none !important;
|
||||||
|
}
|
||||||
|
select:hover {
|
||||||
|
@apply bg-warmGray-700 !important;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
@apply text-left text-base font-bold text-warmGray-400 !important;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
@apply outline-none !important;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
@apply rounded text-sm font-bold transition-all duration-100 !important;
|
||||||
|
}
|
||||||
|
.h-271 {
|
||||||
|
min-height: 271px !important;
|
||||||
|
}
|
||||||
|
.repository-select-search .listItem .item,
|
||||||
|
.repository-select-search .empty {
|
||||||
|
@apply text-sm py-3 font-bold bg-warmGray-800 text-white cursor-pointer border-none hover:bg-warmGray-700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-select-search .listContainer {
|
||||||
|
@apply bg-transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-select-search .clearSelect {
|
||||||
|
@apply text-white cursor-pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-select-search .selectedItem {
|
||||||
|
@apply text-white relative cursor-pointer font-bold text-sm flex items-center !important;
|
||||||
|
}
|
||||||
|
.selectContainer {
|
||||||
|
background: transparent !important;
|
||||||
|
@apply border-0 !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
<script>
|
||||||
|
import { application } from '$store';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import TooltipInfo from '$components/TooltipInfo.svelte';
|
||||||
|
let domainInput;
|
||||||
|
const buildpacks = {
|
||||||
|
static: {
|
||||||
|
port: {
|
||||||
|
active: false,
|
||||||
|
number: 80
|
||||||
|
},
|
||||||
|
build: true
|
||||||
|
},
|
||||||
|
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(() => {
|
||||||
|
if(!$application.publish.domain) domainInput.focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="grid grid-cols-1 text-sm max-w-4xl md:mx-auto mx-6 pb-16 auto-cols-max ">
|
||||||
|
<div class="text-2xl font-bold border-gradient w-40">Build Packs</div>
|
||||||
|
<div class="flex font-bold flex-wrap justify-center pt-10">
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'static'
|
||||||
|
? 'buildpack bg-red-500'
|
||||||
|
: 'buildpack hover:border-red-500'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
Static
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'nodejs'
|
||||||
|
? 'buildpack bg-emerald-600'
|
||||||
|
: 'buildpack hover:border-emerald-600'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
NodeJS
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'vuejs'
|
||||||
|
? 'buildpack bg-green-500'
|
||||||
|
: 'buildpack hover:border-green-500'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
VueJS
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'nuxtjs'
|
||||||
|
? 'buildpack bg-green-500'
|
||||||
|
: 'buildpack hover:border-green-500'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
NuxtJS
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'react'
|
||||||
|
? 'buildpack bg-gradient-to-r from-blue-500 to-purple-500'
|
||||||
|
: 'buildpack hover:border-blue-500'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
React/Preact
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'nextjs'
|
||||||
|
? 'buildpack bg-blue-500'
|
||||||
|
: 'buildpack hover:border-blue-500'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
NextJS
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'gatsby'
|
||||||
|
? 'buildpack bg-blue-500'
|
||||||
|
: 'buildpack hover:border-blue-500'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
Gatsby
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'svelte'
|
||||||
|
? 'buildpack bg-orange-600'
|
||||||
|
: 'buildpack hover:border-orange-600'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
Svelte
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'php'
|
||||||
|
? 'buildpack bg-indigo-500'
|
||||||
|
: 'buildpack hover:border-indigo-500'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
PHP
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'rust'
|
||||||
|
? 'buildpack bg-pink-500'
|
||||||
|
: 'buildpack hover:border-pink-500'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
Rust
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={$application.build.pack === 'docker'
|
||||||
|
? 'buildpack bg-purple-500'
|
||||||
|
: 'buildpack hover:border-purple-500'}
|
||||||
|
on:click={selectBuildPack}
|
||||||
|
>
|
||||||
|
Docker
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
|
||||||
|
<div class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10">
|
||||||
|
<div class="grid grid-flow-col gap-2 items-center pb-6">
|
||||||
|
<div class="grid grid-flow-row">
|
||||||
|
<label for="Domain" class="">Domain</label>
|
||||||
|
<input
|
||||||
|
bind:this={domainInput}
|
||||||
|
class="border-2"
|
||||||
|
class:placeholder-red-500={$application.publish.domain == null ||
|
||||||
|
$application.publish.domain == ''}
|
||||||
|
class:border-red-500={$application.publish.domain == null ||
|
||||||
|
$application.publish.domain == ''}
|
||||||
|
id="Domain"
|
||||||
|
bind:value={$application.publish.domain}
|
||||||
|
placeholder="eg: coollabs.io (without www)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-flow-row">
|
||||||
|
<label for="Path"
|
||||||
|
>Path <TooltipInfo
|
||||||
|
label={`Path to deploy your application on your domain. eg: /api means it will be deployed to -> https://${
|
||||||
|
$application.publish.domain || '<yourdomain>'
|
||||||
|
}/api`}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
<input id="Path" bind:value={$application.publish.path} placeholder="/" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label for="Port" class:text-warmGray-800={!buildpacks[$application.build.pack].port.active}
|
||||||
|
>Port</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
disabled={!buildpacks[$application.build.pack].port.active}
|
||||||
|
id="Port"
|
||||||
|
class:bg-warmGray-900={!buildpacks[$application.build.pack].port.active}
|
||||||
|
class:text-warmGray-900={!buildpacks[$application.build.pack].port.active}
|
||||||
|
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].port.active}
|
||||||
|
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].port.active}
|
||||||
|
class:cursor-not-allowed={!buildpacks[$application.build.pack].port.active}
|
||||||
|
bind:value={$application.publish.port}
|
||||||
|
placeholder={buildpacks[$application.build.pack].port.number}
|
||||||
|
/>
|
||||||
|
<div class="grid grid-flow-col gap-2 items-center pt-6 pb-12">
|
||||||
|
<div class="grid grid-flow-row">
|
||||||
|
<label for="baseDir"
|
||||||
|
>Base Directory <TooltipInfo
|
||||||
|
label="The directory to use as base for every command (could be useful if you have a monorepo)."
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
<input id="baseDir" bind:value={$application.build.directory} placeholder="eg: sourcedir" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-flow-row">
|
||||||
|
<label for="publishDir"
|
||||||
|
>Publish Directory <TooltipInfo
|
||||||
|
label="The directory to deploy after running the build command. eg: dist, _site, public."
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="publishDir"
|
||||||
|
bind:value={$application.publish.directory}
|
||||||
|
placeholder="eg: dist, _site, public"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold w-40"
|
||||||
|
class:border-gradient={buildpacks[$application.build.pack].build}
|
||||||
|
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
|
||||||
|
>
|
||||||
|
Commands
|
||||||
|
</div>
|
||||||
|
<div class=" max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10 pb-32">
|
||||||
|
<div class="grid grid-flow-col gap-2 items-center">
|
||||||
|
<div class="grid grid-flow-row">
|
||||||
|
<label
|
||||||
|
for="installCommand"
|
||||||
|
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
|
||||||
|
>Install Command <TooltipInfo
|
||||||
|
label="Command to run for installing dependencies. eg: yarn install."
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="mb-6"
|
||||||
|
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
|
||||||
|
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
|
||||||
|
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
|
||||||
|
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
|
||||||
|
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
|
||||||
|
id="installCommand"
|
||||||
|
bind:value={$application.build.command.installation}
|
||||||
|
placeholder="eg: yarn install"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="buildCommand"
|
||||||
|
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
|
||||||
|
>Build Command <TooltipInfo
|
||||||
|
label="Command to run for building your application. If empty, no build phase initiated in the deploy process."
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="mb-6"
|
||||||
|
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
|
||||||
|
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
|
||||||
|
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
|
||||||
|
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
|
||||||
|
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
|
||||||
|
id="buildCommand"
|
||||||
|
bind:value={$application.build.command.build}
|
||||||
|
placeholder="eg: yarn build"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { application } from "@store";
|
import { application } from "$store";
|
||||||
|
|
||||||
let secret = {
|
let secret = {
|
||||||
name: null,
|
name: null,
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
export let loading, branches;
|
||||||
|
import { application } from '$store';
|
||||||
|
import Select from 'svelte-select';
|
||||||
|
|
||||||
|
const selectedValue = $page.path !== '/application/new' && $application.repository.branch;
|
||||||
|
|
||||||
|
function handleSelect(event) {
|
||||||
|
$application.repository.branch = null;
|
||||||
|
setTimeout(() => {
|
||||||
|
$application.repository.branch = event.detail.value;
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="grid grid-cols-1">
|
||||||
|
<label for="branch">Branch</label>
|
||||||
|
<div class="repository-select-search col-span-2">
|
||||||
|
<Select
|
||||||
|
containerClasses="w-full border-none bg-transparent"
|
||||||
|
placeholder="Loading branches..."
|
||||||
|
isDisabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="grid grid-cols-1">
|
||||||
|
<label for="branch">Branch</label>
|
||||||
|
<div class="repository-select-search col-span-2">
|
||||||
|
<Select
|
||||||
|
containerClasses="w-full border-none bg-transparent"
|
||||||
|
on:select={handleSelect}
|
||||||
|
{selectedValue}
|
||||||
|
isClearable={false}
|
||||||
|
items={branches.map((b) => ({ label: b.name, value: b.name }))}
|
||||||
|
showIndicator={$page.path === '/application/new'}
|
||||||
|
noOptionsMessage="No branches found"
|
||||||
|
placeholder="Select a branch"
|
||||||
|
isDisabled={$page.path !== '/application/new'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { request } from '$lib/api/request';
|
||||||
|
import { session } from '$app/stores';
|
||||||
|
import { githubRepositories, application, githubInstallations } from '$store';
|
||||||
|
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import Loading from '$components/Loading.svelte';
|
||||||
|
|
||||||
|
import { browser } from '$app/env';
|
||||||
|
import Branches from '$components/Application/Branches.svelte';
|
||||||
|
import Tabs from '$components/Application/Tabs.svelte';
|
||||||
|
import Repositories from '$components/Application/Repositories.svelte';
|
||||||
|
import Login from '$components/Application/Login.svelte';
|
||||||
|
let loading = {
|
||||||
|
github: false,
|
||||||
|
branches: false
|
||||||
|
};
|
||||||
|
let branches = [];
|
||||||
|
let relogin = false;
|
||||||
|
function dashify(str: string, options?: any) {
|
||||||
|
if (typeof str !== 'string') return str;
|
||||||
|
return str
|
||||||
|
.trim()
|
||||||
|
.replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-'))
|
||||||
|
.replace(/^-+|-+$/g, '')
|
||||||
|
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
async function getGithubRepos(id, page) {
|
||||||
|
return await request(
|
||||||
|
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
|
||||||
|
$session
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function loadGithubRepositories(force) {
|
||||||
|
if ($githubRepositories.length > 0 && !force) {
|
||||||
|
$application.github.installation.id = $githubInstallations.id;
|
||||||
|
$application.github.app.id = $githubInstallations.app_id;
|
||||||
|
const foundRepositoryOnGithub = $githubRepositories.find(
|
||||||
|
(r) =>
|
||||||
|
r.full_name === `${$application.repository.organization}/${$application.repository.name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (foundRepositoryOnGithub) {
|
||||||
|
$application.repository.id = foundRepositoryOnGithub.id;
|
||||||
|
$application.repository.organization = foundRepositoryOnGithub.owner.login;
|
||||||
|
$application.repository.name = foundRepositoryOnGithub.name;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
loading.github = true;
|
||||||
|
let installations = [];
|
||||||
|
try {
|
||||||
|
const data = await request('https://api.github.com/user/installations', $session);
|
||||||
|
installations = data.installations;
|
||||||
|
} catch (error) {
|
||||||
|
relogin = true;
|
||||||
|
console.log(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (installations.length === 0) {
|
||||||
|
relogin = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$application.github.installation.id = installations[0].id;
|
||||||
|
$application.github.app.id = installations[0].app_id;
|
||||||
|
$githubInstallations = installations[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
let page = 1;
|
||||||
|
let userRepos = 0;
|
||||||
|
const data = await getGithubRepos($application.github.installation.id, page);
|
||||||
|
$githubRepositories = $githubRepositories.concat(data.repositories);
|
||||||
|
userRepos = data.total_count;
|
||||||
|
if (userRepos > $githubRepositories.length) {
|
||||||
|
while (userRepos > $githubRepositories.length) {
|
||||||
|
page = page + 1;
|
||||||
|
const repos = await getGithubRepos($application.github.installation.id, page);
|
||||||
|
$githubRepositories = $githubRepositories.concat(repos.repositories);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const foundRepositoryOnGithub = $githubRepositories.find(
|
||||||
|
(r) =>
|
||||||
|
r.full_name ===
|
||||||
|
`${$application.repository.organization}/${$application.repository.name}`
|
||||||
|
);
|
||||||
|
if (foundRepositoryOnGithub) {
|
||||||
|
$application.repository.id = foundRepositoryOnGithub.id;
|
||||||
|
await loadBranches();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
loading.github = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function loadBranches() {
|
||||||
|
loading.branches = true;
|
||||||
|
const selectedRepository = $githubRepositories.find((r) => r.id === $application.repository.id);
|
||||||
|
|
||||||
|
if (selectedRepository) {
|
||||||
|
$application.repository.organization = selectedRepository.owner.login;
|
||||||
|
$application.repository.name = selectedRepository.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
branches = await request(
|
||||||
|
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/branches`,
|
||||||
|
$session
|
||||||
|
);
|
||||||
|
loading.branches = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function modifyGithubAppConfig() {
|
||||||
|
if (browser) {
|
||||||
|
const left = screen.width / 2 - 1020 / 2;
|
||||||
|
const top = screen.height / 2 - 618 / 2;
|
||||||
|
const newWindow = open(
|
||||||
|
`https://github.com/apps/${dashify(
|
||||||
|
import.meta.env.VITE_GITHUB_APP_NAME
|
||||||
|
)}/installations/new`,
|
||||||
|
'Install App',
|
||||||
|
'resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=' +
|
||||||
|
top +
|
||||||
|
', left=' +
|
||||||
|
left +
|
||||||
|
', toolbar=0, menubar=0, status=0'
|
||||||
|
);
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
if (newWindow.closed) {
|
||||||
|
clearInterval(timer);
|
||||||
|
loading.github = true;
|
||||||
|
|
||||||
|
if ($application.repository.name) {
|
||||||
|
try {
|
||||||
|
const config = await request(`/api/v1/application/config`, $session, {
|
||||||
|
body: {
|
||||||
|
name: $application.repository.name,
|
||||||
|
organization: $application.repository.organization,
|
||||||
|
branch: $application.repository.branch
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$application = { ...config };
|
||||||
|
} catch (error) {
|
||||||
|
browser && goto('/dashboard/applications', { replaceState: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
branches = [];
|
||||||
|
$githubRepositories = [];
|
||||||
|
await loadGithubRepositories(true);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div in:fade={{ duration: 100 }}>
|
||||||
|
{#if relogin}
|
||||||
|
<Login />
|
||||||
|
{:else}
|
||||||
|
{#await loadGithubRepositories(false)}
|
||||||
|
<Loading github githubLoadingText="Loading repositories..." />
|
||||||
|
{:then}
|
||||||
|
{#if loading.github}
|
||||||
|
<Loading github githubLoadingText="Loading repositories..." />
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
|
||||||
|
<Repositories
|
||||||
|
on:loadBranches={loadBranches}
|
||||||
|
on:modifyGithubAppConfig={modifyGithubAppConfig}
|
||||||
|
/>
|
||||||
|
{#if $application.repository.organization}
|
||||||
|
<Branches loading={loading.branches} {branches} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $application.repository.branch}
|
||||||
|
<Tabs />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
<style lang="postcss">
|
|
||||||
.buildpack {
|
|
||||||
@apply px-6 py-2 mx-2 my-2 bg-warmGray-800 w-48 ease-in-out transform hover:scale-105 text-center rounded border-2 border-transparent border-dashed cursor-pointer transition duration-100;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { application } from "@store";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import TooltipInfo from "../../../Tooltip/TooltipInfo.svelte";
|
|
||||||
let domainInput;
|
|
||||||
const buildpacks = {
|
|
||||||
static: {
|
|
||||||
port: {
|
|
||||||
active: false,
|
|
||||||
number: 80,
|
|
||||||
},
|
|
||||||
build: true,
|
|
||||||
},
|
|
||||||
nodejs: {
|
|
||||||
port: {
|
|
||||||
active: true,
|
|
||||||
number: 3000,
|
|
||||||
},
|
|
||||||
build: true,
|
|
||||||
},
|
|
||||||
vuejs: {
|
|
||||||
port: {
|
|
||||||
active: false,
|
|
||||||
number: 80,
|
|
||||||
},
|
|
||||||
build: true,
|
|
||||||
},
|
|
||||||
nuxtjs: {
|
|
||||||
port: {
|
|
||||||
active: true,
|
|
||||||
number: 3000,
|
|
||||||
},
|
|
||||||
build: true,
|
|
||||||
},
|
|
||||||
react: {
|
|
||||||
port: {
|
|
||||||
active: false,
|
|
||||||
number: 80,
|
|
||||||
},
|
|
||||||
build: true,
|
|
||||||
},
|
|
||||||
nextjs: {
|
|
||||||
port: {
|
|
||||||
active: true,
|
|
||||||
number: 3000,
|
|
||||||
},
|
|
||||||
build: true,
|
|
||||||
},
|
|
||||||
gatsby: {
|
|
||||||
port: {
|
|
||||||
active: true,
|
|
||||||
number: 3000,
|
|
||||||
},
|
|
||||||
build: true,
|
|
||||||
},
|
|
||||||
svelte: {
|
|
||||||
port: {
|
|
||||||
active: false,
|
|
||||||
number: 80,
|
|
||||||
},
|
|
||||||
build: true,
|
|
||||||
},
|
|
||||||
php: {
|
|
||||||
port: {
|
|
||||||
active: false,
|
|
||||||
number: 80,
|
|
||||||
},
|
|
||||||
build: false,
|
|
||||||
},
|
|
||||||
rust: {
|
|
||||||
port: {
|
|
||||||
active: true,
|
|
||||||
number: 3000,
|
|
||||||
},
|
|
||||||
build: false,
|
|
||||||
},
|
|
||||||
docker: {
|
|
||||||
port: {
|
|
||||||
active: true,
|
|
||||||
number: 3000,
|
|
||||||
},
|
|
||||||
build: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
function selectBuildPack(event) {
|
|
||||||
if (event.target.innerText === "React/Preact") {
|
|
||||||
$application.build.pack = "react";
|
|
||||||
} else {
|
|
||||||
$application.build.pack = event.target.innerText
|
|
||||||
.replace(/\./g, "")
|
|
||||||
.toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onMount(()=> {
|
|
||||||
domainInput.focus();
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-1 text-sm max-w-4xl md:mx-auto mx-6 pb-16 auto-cols-max "
|
|
||||||
>
|
|
||||||
<div class="text-2xl font-bold border-gradient w-40">Build Packs</div>
|
|
||||||
<div class="flex font-bold flex-wrap justify-center pt-10">
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'static'
|
|
||||||
? 'buildpack bg-red-500'
|
|
||||||
: 'buildpack hover:border-red-500'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
Static
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'nodejs'
|
|
||||||
? 'buildpack bg-emerald-600'
|
|
||||||
: 'buildpack hover:border-emerald-600'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
NodeJS
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'vuejs'
|
|
||||||
? 'buildpack bg-green-500'
|
|
||||||
: 'buildpack hover:border-green-500'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
VueJS
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'nuxtjs'
|
|
||||||
? 'buildpack bg-green-500'
|
|
||||||
: 'buildpack hover:border-green-500'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
NuxtJS
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'react'
|
|
||||||
? 'buildpack bg-gradient-to-r from-blue-500 to-purple-500'
|
|
||||||
: 'buildpack hover:border-blue-500'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
React/Preact
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'nextjs'
|
|
||||||
? 'buildpack bg-blue-500'
|
|
||||||
: 'buildpack hover:border-blue-500'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
NextJS
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'gatsby'
|
|
||||||
? 'buildpack bg-blue-500'
|
|
||||||
: 'buildpack hover:border-blue-500'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
Gatsby
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'svelte'
|
|
||||||
? 'buildpack bg-orange-600'
|
|
||||||
: 'buildpack hover:border-orange-600'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
Svelte
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'php'
|
|
||||||
? 'buildpack bg-indigo-500'
|
|
||||||
: 'buildpack hover:border-indigo-500'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
PHP
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'rust'
|
|
||||||
? 'buildpack bg-pink-500'
|
|
||||||
: 'buildpack hover:border-pink-500'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
Rust
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="{$application.build.pack === 'docker'
|
|
||||||
? 'buildpack bg-purple-500'
|
|
||||||
: 'buildpack hover:border-purple-500'}"
|
|
||||||
on:click="{selectBuildPack}"
|
|
||||||
>
|
|
||||||
Docker
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10"
|
|
||||||
>
|
|
||||||
<div class="grid grid-flow-col gap-2 items-center pb-6">
|
|
||||||
<div class="grid grid-flow-row">
|
|
||||||
<label for="Domain" class="">Domain</label>
|
|
||||||
<input
|
|
||||||
bind:this={domainInput}
|
|
||||||
class="border-2"
|
|
||||||
class:placeholder-red-500="{$application.publish.domain == null ||
|
|
||||||
$application.publish.domain == ''}"
|
|
||||||
class:border-red-500="{$application.publish.domain == null ||
|
|
||||||
$application.publish.domain == ''}"
|
|
||||||
id="Domain"
|
|
||||||
bind:value="{$application.publish.domain}"
|
|
||||||
placeholder="eg: coollabs.io (without www)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-flow-row">
|
|
||||||
<label for="Path"
|
|
||||||
>Path <TooltipInfo
|
|
||||||
label="{`Path to deploy your application on your domain. eg: /api means it will be deployed to -> https://${
|
|
||||||
$application.publish.domain || '<yourdomain>'
|
|
||||||
}/api`}"
|
|
||||||
/></label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="Path"
|
|
||||||
bind:value="{$application.publish.path}"
|
|
||||||
placeholder="/"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<label
|
|
||||||
for="Port"
|
|
||||||
class:text-warmGray-800="{!buildpacks[$application.build.pack].port
|
|
||||||
.active}">Port</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
disabled="{!buildpacks[$application.build.pack].port.active}"
|
|
||||||
id="Port"
|
|
||||||
class:bg-warmGray-900="{!buildpacks[$application.build.pack].port.active}"
|
|
||||||
class:text-warmGray-900="{!buildpacks[$application.build.pack].port
|
|
||||||
.active}"
|
|
||||||
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack].port
|
|
||||||
.active}"
|
|
||||||
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack].port
|
|
||||||
.active}"
|
|
||||||
class:cursor-not-allowed="{!buildpacks[$application.build.pack].port
|
|
||||||
.active}"
|
|
||||||
bind:value="{$application.publish.port}"
|
|
||||||
placeholder="{buildpacks[$application.build.pack].port.number}"
|
|
||||||
/>
|
|
||||||
<div class="grid grid-flow-col gap-2 items-center pt-6 pb-12">
|
|
||||||
<div class="grid grid-flow-row">
|
|
||||||
<label for="baseDir"
|
|
||||||
>Base Directory <TooltipInfo
|
|
||||||
label="The directory to use as base for every command (could be useful if you have a monorepo)."
|
|
||||||
/></label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="baseDir"
|
|
||||||
bind:value="{$application.build.directory}"
|
|
||||||
placeholder="eg: sourcedir"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-flow-row">
|
|
||||||
<label for="publishDir"
|
|
||||||
>Publish Directory <TooltipInfo
|
|
||||||
label="The directory to deploy after running the build command. eg: dist, _site, public."
|
|
||||||
/></label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="publishDir"
|
|
||||||
bind:value="{$application.publish.directory}"
|
|
||||||
placeholder="eg: dist, _site, public"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="text-2xl font-bold w-40"
|
|
||||||
class:border-gradient="{buildpacks[$application.build.pack].build}"
|
|
||||||
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
|
|
||||||
>
|
|
||||||
Commands
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class=" max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10 pb-32"
|
|
||||||
>
|
|
||||||
<div class="grid grid-flow-col gap-2 items-center">
|
|
||||||
<div class="grid grid-flow-row">
|
|
||||||
<label
|
|
||||||
for="installCommand"
|
|
||||||
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
|
|
||||||
>Install Command <TooltipInfo
|
|
||||||
label="Command to run for installing dependencies. eg: yarn install."
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
class="mb-6"
|
|
||||||
class:bg-warmGray-900="{!buildpacks[$application.build.pack].build}"
|
|
||||||
class:text-warmGray-900="{!buildpacks[$application.build.pack].build}"
|
|
||||||
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack]
|
|
||||||
.build}"
|
|
||||||
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack]
|
|
||||||
.build}"
|
|
||||||
class:cursor-not-allowed="{!buildpacks[$application.build.pack]
|
|
||||||
.build}"
|
|
||||||
id="installCommand"
|
|
||||||
bind:value="{$application.build.command.installation}"
|
|
||||||
placeholder="eg: yarn install"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="buildCommand"
|
|
||||||
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
|
|
||||||
>Build Command <TooltipInfo
|
|
||||||
label="Command to run for building your application. If empty, no build phase initiated in the deploy process."
|
|
||||||
/></label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="mb-6"
|
|
||||||
class:bg-warmGray-900="{!buildpacks[$application.build.pack].build}"
|
|
||||||
class:text-warmGray-900="{!buildpacks[$application.build.pack].build}"
|
|
||||||
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack]
|
|
||||||
.build}"
|
|
||||||
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack]
|
|
||||||
.build}"
|
|
||||||
class:cursor-not-allowed="{!buildpacks[$application.build.pack]
|
|
||||||
.build}"
|
|
||||||
id="buildCommand"
|
|
||||||
bind:value="{$application.build.command.build}"
|
|
||||||
placeholder="eg: yarn build"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let loading, branches;
|
|
||||||
import { application, activePage } from "@store";
|
|
||||||
import Select from "svelte-select";
|
|
||||||
|
|
||||||
const selectedValue =
|
|
||||||
$activePage.application !== "new" && $application.repository.branch;
|
|
||||||
|
|
||||||
function handleSelect(event) {
|
|
||||||
$application.repository.branch = null;
|
|
||||||
setTimeout(() => {
|
|
||||||
$application.repository.branch = event.detail.value;
|
|
||||||
}, 1);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if loading}
|
|
||||||
<div class="grid grid-cols-1">
|
|
||||||
<label for="branch">Branch</label>
|
|
||||||
<div class="repository-select-search col-span-2">
|
|
||||||
<Select
|
|
||||||
containerClasses="w-full border-none bg-transparent"
|
|
||||||
placeholder="Loading branches..."
|
|
||||||
isDisabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="grid grid-cols-1">
|
|
||||||
<label for="branch">Branch</label>
|
|
||||||
<div class="repository-select-search col-span-2">
|
|
||||||
<Select
|
|
||||||
containerClasses="w-full border-none bg-transparent"
|
|
||||||
on:select="{handleSelect}"
|
|
||||||
selectedValue="{selectedValue}"
|
|
||||||
isClearable="{false}"
|
|
||||||
items="{branches.map(b => ({ label: b.name, value: b.name }))}"
|
|
||||||
showIndicator="{$activePage.new}"
|
|
||||||
noOptionsMessage="No branches found"
|
|
||||||
placeholder="Select a branch"
|
|
||||||
isDisabled="{!$activePage.new}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { redirect, isActive } from "@roxi/routify";
|
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import {
|
|
||||||
session,
|
|
||||||
application,
|
|
||||||
fetch,
|
|
||||||
initialApplication,
|
|
||||||
githubRepositories,
|
|
||||||
githubInstallations,
|
|
||||||
activePage,
|
|
||||||
} from "@store";
|
|
||||||
|
|
||||||
import Login from "./Login.svelte";
|
|
||||||
import Loading from "../../Loading.svelte";
|
|
||||||
import Repositories from "./Repositories.svelte";
|
|
||||||
import Branches from "./Branches.svelte";
|
|
||||||
import Tabs from "./Tabs.svelte";
|
|
||||||
|
|
||||||
let loading = {
|
|
||||||
branches: false,
|
|
||||||
github: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let branches = [];
|
|
||||||
function dashify(str, options) {
|
|
||||||
if (typeof str !== "string") return str;
|
|
||||||
return str
|
|
||||||
.trim()
|
|
||||||
.replace(/\W/g, m => (/[À-ž]/.test(m) ? m : "-"))
|
|
||||||
.replace(/^-+|-+$/g, "")
|
|
||||||
.replace(/-{2,}/g, m => (options && options.condense ? "-" : m))
|
|
||||||
.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadBranches() {
|
|
||||||
loading.branches = true;
|
|
||||||
if ($activePage.new) $application.repository.branch = null;
|
|
||||||
const selectedRepository = $githubRepositories.find(
|
|
||||||
r => r.id === $application.repository.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedRepository) {
|
|
||||||
$application.repository.organization = selectedRepository.owner.login;
|
|
||||||
$application.repository.name = selectedRepository.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
branches = await $fetch(
|
|
||||||
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/branches`,
|
|
||||||
);
|
|
||||||
loading.branches = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getGithubRepos(id, page) {
|
|
||||||
const data = await $fetch(
|
|
||||||
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadGithub() {
|
|
||||||
if ($githubRepositories.length > 0) {
|
|
||||||
$application.github.installation.id = $githubInstallations.id;
|
|
||||||
$application.github.app.id = $githubInstallations.app_id;
|
|
||||||
const foundRepositoryOnGithub = $githubRepositories.find(
|
|
||||||
r =>
|
|
||||||
r.full_name ===
|
|
||||||
`${$application.repository.organization}/${$application.repository.name}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (foundRepositoryOnGithub) {
|
|
||||||
$application.repository.id = foundRepositoryOnGithub.id;
|
|
||||||
$application.repository.organization = foundRepositoryOnGithub.owner.login;
|
|
||||||
$application.repository.name = foundRepositoryOnGithub.name;
|
|
||||||
// await loadBranches();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loading.github = true;
|
|
||||||
try {
|
|
||||||
const { installations } = await $fetch(
|
|
||||||
"https://api.github.com/user/installations",
|
|
||||||
);
|
|
||||||
if (installations.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$application.github.installation.id = installations[0].id;
|
|
||||||
$application.github.app.id = installations[0].app_id;
|
|
||||||
$githubInstallations = installations[0];
|
|
||||||
|
|
||||||
let page = 1;
|
|
||||||
let userRepos = 0;
|
|
||||||
const data = await getGithubRepos(
|
|
||||||
$application.github.installation.id,
|
|
||||||
page,
|
|
||||||
);
|
|
||||||
|
|
||||||
$githubRepositories = $githubRepositories.concat(data.repositories);
|
|
||||||
userRepos = data.total_count;
|
|
||||||
|
|
||||||
if (userRepos > $githubRepositories.length) {
|
|
||||||
while (userRepos > $githubRepositories.length) {
|
|
||||||
page = page + 1;
|
|
||||||
const repos = await getGithubRepos(
|
|
||||||
$application.github.installation.id,
|
|
||||||
page,
|
|
||||||
);
|
|
||||||
$githubRepositories = $githubRepositories.concat(repos.repositories);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const foundRepositoryOnGithub = $githubRepositories.find(
|
|
||||||
r =>
|
|
||||||
r.full_name ===
|
|
||||||
`${$application.repository.organization}/${$application.repository.name}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (foundRepositoryOnGithub) {
|
|
||||||
$application.repository.id = foundRepositoryOnGithub.id;
|
|
||||||
await loadBranches();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
loading.github = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function modifyGithubAppConfig() {
|
|
||||||
const left = screen.width / 2 - 1020 / 2;
|
|
||||||
const top = screen.height / 2 - 618 / 2;
|
|
||||||
const newWindow = open(
|
|
||||||
`https://github.com/apps/${dashify(
|
|
||||||
import.meta.env.VITE_GITHUB_APP_NAME,
|
|
||||||
)}/installations/new`,
|
|
||||||
"Install App",
|
|
||||||
"resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=" +
|
|
||||||
top +
|
|
||||||
", left=" +
|
|
||||||
left +
|
|
||||||
", toolbar=0, menubar=0, status=0",
|
|
||||||
);
|
|
||||||
const timer = setInterval(async () => {
|
|
||||||
if (newWindow.closed) {
|
|
||||||
clearInterval(timer);
|
|
||||||
loading.github = true;
|
|
||||||
if (!$activePage.new) {
|
|
||||||
try {
|
|
||||||
const config = await $fetch(`/api/v1/config`, {
|
|
||||||
body: {
|
|
||||||
name: $application.repository.name,
|
|
||||||
organization: $application.repository.organization,
|
|
||||||
branch: $application.repository.branch,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
$application = { ...config };
|
|
||||||
} catch (error) {
|
|
||||||
$redirect("/dashboard/applications");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
|
||||||
}
|
|
||||||
branches = [];
|
|
||||||
$githubRepositories = [];
|
|
||||||
await loadGithub();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if !$activePage.new}
|
|
||||||
<div class="min-h-full text-white">
|
|
||||||
<div
|
|
||||||
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
|
|
||||||
>
|
|
||||||
{$application.publish.domain
|
|
||||||
? `${$application.publish.domain}${
|
|
||||||
$application.publish.path !== "/" ? $application.publish.path : ""
|
|
||||||
}`
|
|
||||||
: "example.com"}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
class="icon mx-2"
|
|
||||||
href="{'https://' +
|
|
||||||
$application.publish.domain +
|
|
||||||
$application.publish.path}"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
||||||
></path>
|
|
||||||
</svg></a
|
|
||||||
>
|
|
||||||
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
class="icon"
|
|
||||||
href="{`https://github.com/${$application.repository.organization}/${$application.repository.name}`}"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
><path
|
|
||||||
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
|
||||||
></path></svg
|
|
||||||
></a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if $activePage.new}
|
|
||||||
<div class="min-h-full text-white">
|
|
||||||
<div
|
|
||||||
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
|
|
||||||
>
|
|
||||||
New Application
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div in:fade="{{ duration: 100 }}">
|
|
||||||
{#if !$session.githubAppToken}
|
|
||||||
<Login />
|
|
||||||
{:else}
|
|
||||||
{#await loadGithub()}
|
|
||||||
<Loading github githubLoadingText="Loading repositories..." />
|
|
||||||
{:then}
|
|
||||||
{#if loading.github}
|
|
||||||
<Loading github githubLoadingText="Loading repositories..." />
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="space-y-2 max-w-4xl mx-auto px-6"
|
|
||||||
in:fade="{{ duration: 100 }}"
|
|
||||||
>
|
|
||||||
<Repositories
|
|
||||||
on:loadBranches="{loadBranches}"
|
|
||||||
on:modifyGithubAppConfig="{modifyGithubAppConfig}"
|
|
||||||
/>
|
|
||||||
{#if $application.repository.organization}
|
|
||||||
<Branches loading="{loading.branches}" branches="{branches}" />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $application.repository.branch}
|
|
||||||
<Tabs />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/await}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { session } from "@store";
|
|
||||||
function login() {
|
|
||||||
const left = screen.width / 2 - 1020 / 2;
|
|
||||||
const top = screen.height / 2 - 618 / 2;
|
|
||||||
const newWindow = open(
|
|
||||||
`https://github.com/login/oauth/authorize?client_id=${
|
|
||||||
import.meta.env.VITE_GITHUB_APP_CLIENTID
|
|
||||||
}`,
|
|
||||||
"Authenticate",
|
|
||||||
"resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=" +
|
|
||||||
top +
|
|
||||||
", left=" +
|
|
||||||
left +
|
|
||||||
", toolbar=0, menubar=0, status=0",
|
|
||||||
);
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
if (newWindow.closed) {
|
|
||||||
clearInterval(timer);
|
|
||||||
const ghToken = new URL(newWindow.document.URL).searchParams.get(
|
|
||||||
"ghToken",
|
|
||||||
);
|
|
||||||
if (ghToken) {
|
|
||||||
$session.githubAppToken = ghToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="text-center text-white">
|
|
||||||
<div class="text-2xl font-bold text-center pb-4">
|
|
||||||
Choose your Git provider
|
|
||||||
</div>
|
|
||||||
<button on:click="{login}" class="hover:scale-110 transform duration-100 transition">
|
|
||||||
<svg
|
|
||||||
class="w-16"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
><path
|
|
||||||
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { application, githubRepositories, activePage } from "@store";
|
|
||||||
import Select from "svelte-select";
|
|
||||||
function handleSelect(event) {
|
|
||||||
$application.build.pack = 'static'
|
|
||||||
$application.repository.id = parseInt(event.detail.value, 10);
|
|
||||||
dispatch("loadBranches");
|
|
||||||
}
|
|
||||||
|
|
||||||
let items = $githubRepositories.map(repo => ({
|
|
||||||
label: `${repo.owner.login}/${repo.name}`,
|
|
||||||
value: repo.id.toString(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const selectedValue =
|
|
||||||
!$activePage.new &&
|
|
||||||
`${$application.repository.organization}/${$application.repository.name}`;
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
const modifyGithubAppConfig = () => dispatch("modifyGithubAppConfig");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 pt-4">
|
|
||||||
{#if $githubRepositories.length !== 0}
|
|
||||||
<label for="repository">Organization / Repository</label>
|
|
||||||
<div class="grid grid-cols-3 ">
|
|
||||||
<div class="repository-select-search col-span-2">
|
|
||||||
<Select
|
|
||||||
isFocused="true"
|
|
||||||
containerClasses="w-full border-none bg-transparent"
|
|
||||||
on:select="{handleSelect}"
|
|
||||||
selectedValue="{selectedValue}"
|
|
||||||
isClearable="{false}"
|
|
||||||
items="{items}"
|
|
||||||
showIndicator="{$activePage.new}"
|
|
||||||
noOptionsMessage="No Repositories found"
|
|
||||||
placeholder="Select a Repository"
|
|
||||||
isDisabled="{!$activePage.new}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
|
|
||||||
on:click="{modifyGithubAppConfig}">Configure on Github</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white py-2"
|
|
||||||
on:click="{modifyGithubAppConfig}">Add repositories on Github</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { redirect } from "@roxi/routify";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { toast } from "@zerodevx/svelte-toast";
|
|
||||||
import templates from "../../../utils/templates";
|
|
||||||
import { application, fetch, deployments, activePage } from "@store";
|
|
||||||
import General from "./ActiveTab/General.svelte";
|
|
||||||
import Secrets from "./ActiveTab/Secrets.svelte";
|
|
||||||
import Loading from "../../Loading.svelte";
|
|
||||||
|
|
||||||
let activeTab = {
|
|
||||||
general: true,
|
|
||||||
buildStep: false,
|
|
||||||
secrets: false,
|
|
||||||
};
|
|
||||||
function activateTab(tab) {
|
|
||||||
if (activeTab.hasOwnProperty(tab)) {
|
|
||||||
activeTab = {
|
|
||||||
general: false,
|
|
||||||
buildStep: false,
|
|
||||||
secrets: false,
|
|
||||||
};
|
|
||||||
activeTab[tab] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function load() {
|
|
||||||
const found = $deployments?.applications?.deployed.find(deployment => {
|
|
||||||
if (
|
|
||||||
deployment.configuration.repository.organization ===
|
|
||||||
$application.repository.organization &&
|
|
||||||
deployment.configuration.repository.name ===
|
|
||||||
$application.repository.name &&
|
|
||||||
deployment.configuration.repository.branch ===
|
|
||||||
$application.repository.branch
|
|
||||||
) {
|
|
||||||
return deployment;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (found) {
|
|
||||||
$application = { ...found.configuration };
|
|
||||||
if ($activePage.new) {
|
|
||||||
$activePage.new = false;
|
|
||||||
toast.push(
|
|
||||||
"This repository & branch is already defined. Redirecting...",
|
|
||||||
);
|
|
||||||
$redirect(`/application/:organization/:name/:branch/configuration`, {
|
|
||||||
name: $application.repository.name,
|
|
||||||
organization: $application.repository.organization,
|
|
||||||
branch: $application.repository.branch,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!$activePage.new) {
|
|
||||||
const config = await $fetch(`/api/v1/config`, {
|
|
||||||
body: {
|
|
||||||
name: $application.repository.name,
|
|
||||||
organization: $application.repository.organization,
|
|
||||||
branch: $application.repository.branch,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
$application = { ...config };
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const dir = await $fetch(
|
|
||||||
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/contents/?ref=${$application.repository.branch}`,
|
|
||||||
);
|
|
||||||
const packageJson = dir.find(
|
|
||||||
f => f.type === "file" && f.name === "package.json",
|
|
||||||
);
|
|
||||||
const Dockerfile = dir.find(
|
|
||||||
f => f.type === "file" && f.name === "Dockerfile",
|
|
||||||
);
|
|
||||||
const CargoToml = dir.find(
|
|
||||||
f => f.type === "file" && f.name === "Cargo.toml",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (packageJson) {
|
|
||||||
const { content } = await $fetch(packageJson.git_url);
|
|
||||||
const packageJsonContent = JSON.parse(atob(content));
|
|
||||||
const checkPackageJSONContents = dep => {
|
|
||||||
return (
|
|
||||||
packageJsonContent?.dependencies?.hasOwnProperty(dep) ||
|
|
||||||
packageJsonContent?.devDependencies?.hasOwnProperty(dep)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Object.keys(templates).map(dep => {
|
|
||||||
if (checkPackageJSONContents(dep)) {
|
|
||||||
const config = templates[dep];
|
|
||||||
$application.build.pack = config.pack;
|
|
||||||
if (config.installation)
|
|
||||||
$application.build.command.installation = config.installation;
|
|
||||||
if (config.port) $application.publish.port = config.port;
|
|
||||||
if (config.directory)
|
|
||||||
$application.publish.directory = config.directory;
|
|
||||||
|
|
||||||
if (
|
|
||||||
packageJsonContent.scripts.hasOwnProperty("build") &&
|
|
||||||
config.build
|
|
||||||
) {
|
|
||||||
$application.build.command.build = config.build;
|
|
||||||
}
|
|
||||||
toast.push(`${config.name} detected. Default values set.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (CargoToml) {
|
|
||||||
$application.build.pack = "rust";
|
|
||||||
toast.push(`Rust language detected. Default values set.`);
|
|
||||||
} else if (Dockerfile) {
|
|
||||||
$application.build.pack = "docker";
|
|
||||||
toast.push("Custom Dockerfile found. Build pack set to docker.");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Nothing detected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#await load()}
|
|
||||||
<Loading github githubLoadingText="Scanning repository..." />
|
|
||||||
{:then}
|
|
||||||
<div class="block text-center py-8">
|
|
||||||
<nav
|
|
||||||
class="flex space-x-4 justify-center font-bold text-md text-white"
|
|
||||||
aria-label="Tabs"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
on:click="{() => activateTab('general')}"
|
|
||||||
class:text-green-500="{activeTab.general}"
|
|
||||||
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
|
|
||||||
>
|
|
||||||
General
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
on:click="{() => activateTab('secrets')}"
|
|
||||||
class:text-green-500="{activeTab.secrets}"
|
|
||||||
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
|
|
||||||
>
|
|
||||||
Secrets
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-4xl mx-auto">
|
|
||||||
<div class="h-full">
|
|
||||||
{#if activeTab.general}
|
|
||||||
<General />
|
|
||||||
{:else if activeTab.secrets}
|
|
||||||
<Secrets />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script>
|
||||||
|
function login() {
|
||||||
|
const left = screen.width / 2 - 1020 / 2;
|
||||||
|
const top = screen.height / 2 - 618 / 2;
|
||||||
|
const newWindow = open(
|
||||||
|
`https://github.com/login/oauth/authorize?client_id=${
|
||||||
|
import.meta.env.VITE_GITHUB_APP_CLIENTID
|
||||||
|
}`,
|
||||||
|
'Authenticate',
|
||||||
|
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
||||||
|
top +
|
||||||
|
', left=' +
|
||||||
|
left +
|
||||||
|
', toolbar=0, menubar=0, status=0'
|
||||||
|
);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (newWindow.closed) {
|
||||||
|
clearInterval(timer);
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="text-center text-white">
|
||||||
|
<div class="text-2xl font-bold text-center pb-4">Choose your Git provider</div>
|
||||||
|
<button on:click={login} class="hover:scale-110 transform duration-100 transition">
|
||||||
|
<svg
|
||||||
|
class="w-16"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><path
|
||||||
|
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -1,195 +1,178 @@
|
|||||||
<script>
|
<script>
|
||||||
import { params, goto, redirect } from "@roxi/routify";
|
import { application, initialApplication, initConf } from '$store';
|
||||||
import {
|
import { onDestroy } from 'svelte';
|
||||||
application,
|
import { toast } from '@zerodevx/svelte-toast';
|
||||||
fetch,
|
import Tooltip from '$components/Tooltip.svelte';
|
||||||
initialApplication,
|
import { request } from '$lib/api/request';
|
||||||
initConf,
|
import { page, session } from '$app/stores';
|
||||||
activePage,
|
import { goto } from '$app/navigation';
|
||||||
} from "@store";
|
import { browser } from '$app/env';
|
||||||
import { onDestroy } from "svelte";
|
async function removeApplication() {
|
||||||
import { toast } from "@zerodevx/svelte-toast";
|
await request(`/api/v1/application/remove`, $session, {
|
||||||
import Tooltip from "../../components/Tooltip/Tooltip.svelte";
|
body: {
|
||||||
|
organization: $application.repository.organization,
|
||||||
|
name: $application.repository.name,
|
||||||
|
branch: $application.repository.branch
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$application.repository.organization = $params.organization;
|
browser && toast.push('Application removed.');
|
||||||
$application.repository.name = $params.name;
|
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||||
$application.repository.branch = $params.branch;
|
browser && goto(`/dashboard/applications`, { replaceState: true });
|
||||||
|
}
|
||||||
|
|
||||||
async function removeApplication() {
|
onDestroy(() => {
|
||||||
await $fetch(`/api/v1/application/remove`, {
|
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||||
body: {
|
});
|
||||||
organization: $params.organization,
|
|
||||||
name: $params.name,
|
|
||||||
branch: $params.branch,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.push("Application removed.");
|
async function deploy() {
|
||||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
try {
|
||||||
$redirect(`/dashboard/applications`);
|
browser && toast.push('Checking configuration.');
|
||||||
}
|
await request(`/api/v1/application/check`, $session, {
|
||||||
|
body: $application
|
||||||
onDestroy(() => {
|
});
|
||||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
const { nickname, name, deployId } = await request(`/api/v1/application/deploy`, $session, {
|
||||||
});
|
body: $application
|
||||||
|
});
|
||||||
async function deploy() {
|
$application.general.nickname = nickname;
|
||||||
try {
|
$application.build.container.name = name;
|
||||||
toast.push("Checking configuration.");
|
$application.general.deployId = deployId;
|
||||||
await $fetch(`/api/v1/application/check`, {
|
$initConf = JSON.parse(JSON.stringify($application));
|
||||||
body: $application,
|
if (browser) {
|
||||||
});
|
toast.push('Application deployment queued.');
|
||||||
const { nickname, name, deployId } = await $fetch(
|
goto(
|
||||||
`/api/v1/application/deploy`,
|
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs/${$application.general.deployId}`,
|
||||||
{
|
{ replaceState: true }
|
||||||
body: $application,
|
);
|
||||||
},
|
}
|
||||||
);
|
} catch (error) {
|
||||||
$application.general.nickname = nickname;
|
// console.log(error);
|
||||||
$application.build.container.name = name;
|
// toast.push(error.error || error || 'Ooops something went wrong.');
|
||||||
$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>
|
</script>
|
||||||
|
|
||||||
<nav
|
<nav class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50">
|
||||||
class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50"
|
<Tooltip position="bottom" label="Deploy">
|
||||||
>
|
<button
|
||||||
<Tooltip position="bottom" label="Deploy">
|
disabled={$application.publish.domain === '' || $application.publish.domain === null}
|
||||||
<button
|
class:cursor-not-allowed={$application.publish.domain === '' ||
|
||||||
disabled="{$application.publish.domain === '' ||
|
$application.publish.domain === null}
|
||||||
$application.publish.domain === null}"
|
class:hover:bg-green-500={$application.publish.domain}
|
||||||
class:cursor-not-allowed="{$application.publish.domain === '' ||
|
class:bg-green-600={$application.publish.domain}
|
||||||
$application.publish.domain === null}"
|
class:hover:bg-transparent={!$application.publish.domain && $page.path === '/application/new'}
|
||||||
class:hover:bg-green-500="{$application.publish.domain}"
|
class:text-warmGray-700={$application.publish.domain === '' ||
|
||||||
class:bg-green-600="{$application.publish.domain}"
|
$application.publish.domain === null}
|
||||||
class:hover:bg-transparent="{$activePage.new}"
|
class="icon"
|
||||||
class:text-warmGray-700="{$application.publish.domain === '' ||
|
on:click={deploy}
|
||||||
$application.publish.domain === null}"
|
>
|
||||||
class="icon"
|
<svg
|
||||||
on:click="{deploy}"
|
class="w-6"
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg
|
viewBox="0 0 24 24"
|
||||||
class="w-6"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
stroke-width="2"
|
||||||
fill="none"
|
stroke-linecap="round"
|
||||||
stroke="currentColor"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
><polyline points="16 16 12 12 8 16" /><line x1="12" y1="12" x2="12" y2="21" /><path
|
||||||
stroke-linecap="round"
|
d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"
|
||||||
stroke-linejoin="round"
|
/><polyline points="16 16 12 12 8 16" /></svg
|
||||||
><polyline points="16 16 12 12 8 16"></polyline><line
|
>
|
||||||
x1="12"
|
</button>
|
||||||
y1="12"
|
</Tooltip>
|
||||||
x2="12"
|
<Tooltip position="bottom" label="Delete">
|
||||||
y2="21"></line><path
|
<button
|
||||||
d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"
|
disabled={$application.publish.domain === '' ||
|
||||||
></path><polyline points="16 16 12 12 8 16"></polyline></svg
|
$application.publish.domain === null ||
|
||||||
>
|
$page.path === '/application/new'}
|
||||||
</button>
|
class:cursor-not-allowed={$application.publish.domain === '' ||
|
||||||
</Tooltip>
|
$application.publish.domain === null ||
|
||||||
<Tooltip position="bottom" label="Delete">
|
$page.path === '/application/new'}
|
||||||
<button
|
class:hover:text-red-500={$application.publish.domain && $page.path !== '/application/new'}
|
||||||
disabled="{$application.publish.domain === '' ||
|
class:hover:bg-warmGray-700={$application.publish.domain && $page.path !== '/application/new'}
|
||||||
$application.publish.domain === null ||
|
class:hover:bg-transparent={$page.path === '/application/new'}
|
||||||
$activePage.new}"
|
class:text-warmGray-700={$application.publish.domain === '' ||
|
||||||
class:cursor-not-allowed="{$application.publish.domain === '' ||
|
$application.publish.domain === null ||
|
||||||
$application.publish.domain === null ||
|
$page.path === '/application/new'}
|
||||||
$activePage.new}"
|
class="icon"
|
||||||
class:hover:text-red-500="{$application.publish.domain &&
|
on:click={removeApplication}
|
||||||
!$activePage.new}"
|
>
|
||||||
class:hover:bg-warmGray-700="{$application.publish.domain &&
|
<svg
|
||||||
!$activePage.new}"
|
class="w-6"
|
||||||
class:hover:bg-transparent="{$activePage.new}"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class:text-warmGray-700="{$application.publish.domain === '' ||
|
fill="none"
|
||||||
$application.publish.domain === null ||
|
viewBox="0 0 24 24"
|
||||||
$activePage.new}"
|
stroke="currentColor"
|
||||||
class="icon"
|
>
|
||||||
on:click="{removeApplication}"
|
<path
|
||||||
>
|
stroke-linecap="round"
|
||||||
<svg
|
stroke-linejoin="round"
|
||||||
class="w-6"
|
stroke-width="2"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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"
|
||||||
fill="none"
|
/>
|
||||||
viewBox="0 0 24 24"
|
</svg>
|
||||||
stroke="currentColor"
|
</button>
|
||||||
>
|
</Tooltip>
|
||||||
<path
|
<div class="border border-warmGray-700 h-8" />
|
||||||
stroke-linecap="round"
|
<Tooltip position="bottom" label="Logs">
|
||||||
stroke-linejoin="round"
|
<button
|
||||||
stroke-width="2"
|
class="icon"
|
||||||
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"
|
class:text-warmGray-700={$page.path === '/application/new'}
|
||||||
></path>
|
disabled={$page.path === '/application/new'}
|
||||||
</svg>
|
class:hover:text-blue-400={$page.path !== '/application/new'}
|
||||||
</button>
|
class:hover:bg-transparent={$page.path === '/application/new'}
|
||||||
</Tooltip>
|
class:cursor-not-allowed={$page.path === '/application/new'}
|
||||||
<div class="border border-warmGray-700 h-8"></div>
|
class:text-blue-400={/logs\/*/.test($page.path)}
|
||||||
<Tooltip position="bottom" label="Logs">
|
class:bg-warmGray-700={/logs\/*/.test($page.path)}
|
||||||
<button
|
on:click={() =>
|
||||||
class="icon"
|
goto(
|
||||||
class:text-warmGray-700="{$activePage.new}"
|
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`
|
||||||
disabled="{$activePage.new}"
|
)}
|
||||||
class:hover:text-blue-400="{!$activePage.new}"
|
>
|
||||||
class:hover:bg-transparent="{$activePage.new}"
|
<svg
|
||||||
class:cursor-not-allowed="{$activePage.new}"
|
class="w-6"
|
||||||
class:text-blue-400="{$activePage.application === 'logs'}"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class:bg-warmGray-700="{$activePage.application === 'logs'}"
|
fill="none"
|
||||||
on:click="{() =>
|
viewBox="0 0 24 24"
|
||||||
$goto(
|
stroke="currentColor"
|
||||||
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
|
>
|
||||||
)}"
|
<path
|
||||||
>
|
stroke-linecap="round"
|
||||||
<svg
|
stroke-linejoin="round"
|
||||||
class="w-6"
|
stroke-width="2"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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"
|
||||||
fill="none"
|
/>
|
||||||
viewBox="0 0 24 24"
|
</svg>
|
||||||
stroke="currentColor"
|
</button>
|
||||||
>
|
</Tooltip>
|
||||||
<path
|
<Tooltip position="bottom-left" label="Configuration">
|
||||||
stroke-linecap="round"
|
<button
|
||||||
stroke-linejoin="round"
|
class="icon hover:text-yellow-400"
|
||||||
stroke-width="2"
|
disabled={$page.path === '/application/new'}
|
||||||
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"
|
class:text-yellow-400={$page.path.endsWith('configuration') ||
|
||||||
></path>
|
$page.path === '/application/new'}
|
||||||
</svg>
|
class:bg-warmGray-700={$page.path.endsWith('configuration') ||
|
||||||
</button>
|
$page.path === '/application/new'}
|
||||||
</Tooltip>
|
on:click={() =>
|
||||||
<Tooltip position="bottom-left" label="Configuration">
|
goto(
|
||||||
<button
|
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`
|
||||||
class="icon hover:text-yellow-400"
|
)}
|
||||||
disabled="{$activePage.new}"
|
>
|
||||||
class:text-yellow-400="{$activePage.application === 'configuration' ||
|
<svg
|
||||||
$activePage.new}"
|
class="w-6"
|
||||||
class:bg-warmGray-700="{$activePage.application === 'configuration' ||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
$activePage.new}"
|
fill="none"
|
||||||
on:click="{() =>
|
viewBox="0 0 24 24"
|
||||||
$goto(
|
stroke="currentColor"
|
||||||
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
|
>
|
||||||
)}"
|
<path
|
||||||
>
|
stroke-linecap="round"
|
||||||
<svg
|
stroke-linejoin="round"
|
||||||
class="w-6"
|
stroke-width="2"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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"
|
||||||
fill="none"
|
/>
|
||||||
viewBox="0 0 24 24"
|
</svg>
|
||||||
stroke="currentColor"
|
</button>
|
||||||
>
|
</Tooltip>
|
||||||
<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>
|
</nav>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { application, githubRepositories } from '$store';
|
||||||
|
import Select from 'svelte-select';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
function handleSelect(event) {
|
||||||
|
$application.build.pack = 'static';
|
||||||
|
$application.repository.id = parseInt(event.detail.value, 10);
|
||||||
|
$application.repository.branch = null
|
||||||
|
dispatch('loadBranches');
|
||||||
|
}
|
||||||
|
const path = $page.path === '/application/new';
|
||||||
|
let items = $githubRepositories.map((repo) => ({
|
||||||
|
label: `${repo.owner.login}/${repo.name}`,
|
||||||
|
value: repo.id.toString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectedValue =
|
||||||
|
!path && `${$application.repository.organization}/${$application.repository.name}`;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const modifyGithubAppConfig = () => dispatch('modifyGithubAppConfig');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 pt-4">
|
||||||
|
{#if $githubRepositories.length !== 0}
|
||||||
|
<label for="repository">Organization / Repository</label>
|
||||||
|
<div class="grid grid-cols-3 ">
|
||||||
|
<div class="repository-select-search col-span-2">
|
||||||
|
<Select
|
||||||
|
isFocused="{true}"
|
||||||
|
containerClasses="w-full border-none bg-transparent"
|
||||||
|
on:select={handleSelect}
|
||||||
|
{selectedValue}
|
||||||
|
isClearable={false}
|
||||||
|
{items}
|
||||||
|
showIndicator={path}
|
||||||
|
noOptionsMessage="No Repositories found"
|
||||||
|
placeholder="Select a Repository"
|
||||||
|
isDisabled={!path}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
|
||||||
|
on:click={modifyGithubAppConfig}>Configure on Github</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white py-2"
|
||||||
|
on:click={modifyGithubAppConfig}>Add repositories on Github</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<script>
|
||||||
|
import { toast } from '@zerodevx/svelte-toast';
|
||||||
|
import templates from '$lib/api/applications/templates';
|
||||||
|
import { application, dashboard } from '$store';
|
||||||
|
import General from '$components/Application/ActiveTab/General.svelte';
|
||||||
|
import Secrets from '$components/Application/ActiveTab/Secrets.svelte';
|
||||||
|
import Loading from '$components/Loading.svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page, session } from '$app/stores';
|
||||||
|
import { request } from '$lib/api/request';
|
||||||
|
import { browser } from '$app/env';
|
||||||
|
|
||||||
|
let activeTab = {
|
||||||
|
general: true,
|
||||||
|
buildStep: false,
|
||||||
|
secrets: false
|
||||||
|
};
|
||||||
|
function activateTab(tab) {
|
||||||
|
if (activeTab.hasOwnProperty(tab)) {
|
||||||
|
activeTab = {
|
||||||
|
general: false,
|
||||||
|
buildStep: false,
|
||||||
|
secrets: false
|
||||||
|
};
|
||||||
|
activeTab[tab] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function load() {
|
||||||
|
const found = $dashboard?.applications?.deployed.find((deployment) => {
|
||||||
|
if (
|
||||||
|
deployment.configuration.repository.organization === $application.repository.organization &&
|
||||||
|
deployment.configuration.repository.name === $application.repository.name &&
|
||||||
|
deployment.configuration.repository.branch === $application.repository.branch
|
||||||
|
) {
|
||||||
|
return deployment;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (found) {
|
||||||
|
$application = { ...found.configuration };
|
||||||
|
if ($page.path === '/application/new') {
|
||||||
|
if (browser) {
|
||||||
|
toast.push('This repository & branch is already defined. Redirecting...');
|
||||||
|
goto(
|
||||||
|
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
|
||||||
|
{ replaceState: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($page.path !== '/application/new') {
|
||||||
|
const config = await request(`/api/v1/application/config`, $session, {
|
||||||
|
body: {
|
||||||
|
name: $application.repository.name,
|
||||||
|
organization: $application.repository.organization,
|
||||||
|
branch: $application.repository.branch
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$application = { ...config };
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const dir = await request(
|
||||||
|
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/contents/?ref=${$application.repository.branch}`,
|
||||||
|
$session
|
||||||
|
);
|
||||||
|
const packageJson = dir.find((f) => f.type === 'file' && f.name === 'package.json');
|
||||||
|
const Dockerfile = dir.find((f) => f.type === 'file' && f.name === 'Dockerfile');
|
||||||
|
const CargoToml = dir.find((f) => f.type === 'file' && f.name === 'Cargo.toml');
|
||||||
|
|
||||||
|
if (packageJson) {
|
||||||
|
const { content } = await request(packageJson.git_url, $session);
|
||||||
|
const packageJsonContent = JSON.parse(atob(content));
|
||||||
|
const checkPackageJSONContents = (dep) => {
|
||||||
|
return (
|
||||||
|
packageJsonContent?.dependencies?.hasOwnProperty(dep) ||
|
||||||
|
packageJsonContent?.devDependencies?.hasOwnProperty(dep)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Object.keys(templates).map((dep) => {
|
||||||
|
if (checkPackageJSONContents(dep)) {
|
||||||
|
const config = templates[dep];
|
||||||
|
$application.build.pack = config.pack;
|
||||||
|
if (config.installation)
|
||||||
|
$application.build.command.installation = config.installation;
|
||||||
|
if (config.port) $application.publish.port = config.port;
|
||||||
|
if (config.directory) $application.publish.directory = config.directory;
|
||||||
|
|
||||||
|
if (packageJsonContent.scripts.hasOwnProperty('build') && config.build) {
|
||||||
|
$application.build.command.build = config.build;
|
||||||
|
}
|
||||||
|
browser && toast.push(`${config.name} detected. Default values set.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (CargoToml) {
|
||||||
|
$application.build.pack = 'rust';
|
||||||
|
browser && toast.push(`Rust language detected. Default values set.`);
|
||||||
|
} else if (Dockerfile) {
|
||||||
|
$application.build.pack = 'docker';
|
||||||
|
browser && toast.push('Custom Dockerfile found. Build pack set to docker.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Nothing detected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await load()}
|
||||||
|
<Loading github githubLoadingText="Scanning repository..." />
|
||||||
|
{:then}
|
||||||
|
<div class="block text-center py-8">
|
||||||
|
<nav class="flex space-x-4 justify-center font-bold text-md text-white" aria-label="Tabs">
|
||||||
|
<div
|
||||||
|
on:click={() => activateTab('general')}
|
||||||
|
class:text-green-500={activeTab.general}
|
||||||
|
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
|
||||||
|
>
|
||||||
|
General
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
on:click={() => activateTab('secrets')}
|
||||||
|
class:text-green-500={activeTab.secrets}
|
||||||
|
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
|
||||||
|
>
|
||||||
|
Secrets
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="h-full">
|
||||||
|
{#if activeTab.general}
|
||||||
|
<General />
|
||||||
|
{:else if activeTab.secrets}
|
||||||
|
<Secrets />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<script>
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { toast } from '@zerodevx/svelte-toast';
|
||||||
|
import MongoDb from './SVGs/MongoDb.svelte';
|
||||||
|
import Postgresql from './SVGs/Postgresql.svelte';
|
||||||
|
import Mysql from './SVGs/Mysql.svelte';
|
||||||
|
import CouchDb from './SVGs/CouchDb.svelte';
|
||||||
|
import { page, session } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { request } from '$lib/api/request';
|
||||||
|
import { browser } from '$app/env';
|
||||||
|
|
||||||
|
let type;
|
||||||
|
let defaultDatabaseName;
|
||||||
|
|
||||||
|
async function deploy() {
|
||||||
|
try {
|
||||||
|
await request(`/api/v1/databases/deploy`, $session, {
|
||||||
|
body: {
|
||||||
|
type,
|
||||||
|
defaultDatabaseName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
toast.push('Database deployment queued.');
|
||||||
|
goto(`/dashboard/databases`, { replaceState: true });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="text-center space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
|
||||||
|
{#if $page.path === '/database/new'}
|
||||||
|
<div class="flex justify-center space-x-4 font-bold pb-6">
|
||||||
|
<div
|
||||||
|
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-green-600 p-2 rounded bg-warmGray-800 w-32"
|
||||||
|
class:border-green-600={type === 'mongodb'}
|
||||||
|
on:click={() => (type = 'mongodb')}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center my-2">
|
||||||
|
<MongoDb customClass="w-6" />
|
||||||
|
</div>
|
||||||
|
<div class="text-white">MongoDB</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-red-600 p-2 rounded bg-warmGray-800 w-32"
|
||||||
|
class:border-red-600={type === 'couchdb'}
|
||||||
|
on:click={() => (type = 'couchdb')}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center my-2">
|
||||||
|
<CouchDb customClass="w-12 text-red-600 fill-current" />
|
||||||
|
</div>
|
||||||
|
<div class="text-white">Couchdb</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-600 p-2 rounded bg-warmGray-800 w-32"
|
||||||
|
class:border-blue-600={type === 'postgresql'}
|
||||||
|
on:click={() => (type = 'postgresql')}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center my-2">
|
||||||
|
<Postgresql customClass="w-12" />
|
||||||
|
</div>
|
||||||
|
<div class="text-white">PostgreSQL</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-orange-600 p-2 rounded bg-warmGray-800 w-32"
|
||||||
|
class:border-orange-600={type === 'mysql'}
|
||||||
|
on:click={() => (type = 'mysql')}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<Mysql customClass="w-10" />
|
||||||
|
</div>
|
||||||
|
<div class="text-white">MySQL</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <button
|
||||||
|
class="button bg-gray-500 p-2 text-white hover:bg-yellow-500 cursor-pointer w-32"
|
||||||
|
on:click="{() => (type = 'clickhouse')}"
|
||||||
|
class:bg-yellow-500="{type === 'clickhouse'}"
|
||||||
|
>
|
||||||
|
Clickhouse
|
||||||
|
</button> -->
|
||||||
|
</div>
|
||||||
|
{#if type}
|
||||||
|
<div class="flex justify-center space-x-4 items-center">
|
||||||
|
<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'}
|
||||||
|
class:bg-blue-600={type === 'postgresql'}
|
||||||
|
class:hover:bg-blue-500={type === 'postgresql'}
|
||||||
|
class:bg-orange-600={type === 'mysql'}
|
||||||
|
class:hover:bg-orange-500={type === 'mysql'}
|
||||||
|
class:bg-red-600={type === 'couchdb'}
|
||||||
|
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
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 227 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user