Compare commits

...

44 Commits

Author SHA1 Message Date
Andras Bacsai
ad3044dce1 Merge pull request #234 from coollabsio/v2.2.0
v2.2.0
2022-03-27 22:48:04 +02:00
Andras Bacsai
e40541d831 design: Colors on svelte-select 2022-03-27 22:40:36 +02:00
Andras Bacsai
2786e7dbaf Merge pull request #235 from SaraVieira/add-better-repo-select
Add search in repo and branch select
2022-03-27 22:34:15 +02:00
Andras Bacsai
196d681a63 fix: Ghost icon, remove console.log 2022-03-27 22:32:31 +02:00
Andras Bacsai
d2353e3c35 fix: Ghost logo size 2022-03-27 22:28:39 +02:00
Andras Bacsai
2475031f88 feat: Ghost service 2022-03-27 22:03:21 +02:00
Sara Vieira
cd15e68adc remove console.log 2022-03-27 21:56:23 +02:00
Sara Vieira
27431f779d clean css 2022-03-27 21:50:28 +02:00
Sara Vieira
b9b5a2faeb add search in repo and branch select 2022-03-27 21:48:25 +02:00
Andras Bacsai
e471b11d3b Add icons 2022-03-27 14:37:32 +02:00
Andras Bacsai
a742a3d2e3 feat: Add update kuma service 2022-03-27 14:05:36 +02:00
Andras Bacsai
c615f6c07e chore: Version ++ 2022-03-27 13:49:13 +02:00
Andras Bacsai
a6ebfb08f7 feat: Add n8n.io service 2022-03-27 13:49:04 +02:00
Andras Bacsai
2b0d162226 Merge pull request #232 from coollabsio/v2.1.1
v2.1.1
2022-03-25 15:41:26 +01:00
Andras Bacsai
2c5f09a8bb fix: Cleanup only 2 hours+ old images 2022-03-25 15:34:14 +01:00
Andras Bacsai
ef073e586b Test nocheck proxy 2022-03-25 10:48:11 +01:00
Andras Bacsai
82bfdb87e3 UI fixes 2022-03-25 10:36:47 +01:00
Andras Bacsai
767e7b80cb chore: version++ 2022-03-25 09:14:55 +01:00
Andras Bacsai
8d26ea9063 Update packages 2022-03-25 09:14:32 +01:00
Andras Bacsai
1a7c4310d0 Merge pull request #230 from coollabsio/v2.1.0
v2.1.0
2022-03-23 11:54:15 +01:00
Andras Bacsai
4e8fe79e2b feat: Be able to redeploy PRs 2022-03-23 11:49:40 +01:00
Andras Bacsai
a8c5551292 fix: Volumes 2022-03-23 11:14:38 +01:00
Andras Bacsai
2bf73109b2 feat: Use compose instead of normal docker cmd 2022-03-23 10:25:32 +01:00
Andras Bacsai
f0ab3750bd Disable PHP modules, as the new image has all activated by default 2022-03-22 15:56:03 +01:00
Andras Bacsai
58a11e37fe Add schema 2022-03-22 14:58:08 +01:00
Andras Bacsai
927bf46304 fix: skip ssl cert in case of error 2022-03-22 10:37:33 +01:00
Andras Bacsai
6b89857697 chore: version++ 2022-03-22 10:24:52 +01:00
Andras Bacsai
b72e5ccef6 Merge branch 'main' into v2.0.32 2022-03-22 10:23:11 +01:00
Andras Bacsai
6617b7811b log ssl errors 2022-03-22 10:22:20 +01:00
Andras Bacsai
e1c1988db4 Merge pull request #229 from coollabsio/importer-error
Add debug for GH importer
2022-03-22 09:42:49 +01:00
Andras Bacsai
af99ea4678 Add debug for GH importer 2022-03-22 09:35:24 +01:00
Andras Bacsai
a6d5316090 WIP - Persistent storage 2022-03-21 21:46:49 +01:00
Andras Bacsai
f5e7a84fa6 Update buildpacks for static sites 2022-03-21 21:25:01 +01:00
Andras Bacsai
c013764b61 WIP Persistent storage 2022-03-21 16:58:13 +01:00
Andras Bacsai
2320ab0dfc WIP - Persistent storage 2022-03-20 23:51:50 +01:00
Andras Bacsai
1281a0f7e4 Merge pull request #226 from coollabsio/v2.0.31
v2.0.31
2022-03-20 15:21:57 +01:00
Andras Bacsai
d8350cd4ee Migration file 2022-03-20 15:14:33 +01:00
Andras Bacsai
e3b7c23ed9 chore: Version++ 2022-03-20 15:05:05 +01:00
Andras Bacsai
eae1ea21d6 fix: Add nginx + htaccess files 2022-03-20 15:03:24 +01:00
Andras Bacsai
541aa76b64 fix: Only cleanup same app 2022-03-20 14:21:11 +01:00
Andras Bacsai
7b8555d524 fix: Cleanup old builds 2022-03-20 14:20:29 +01:00
Andras Bacsai
fdf998c181 css fix for select 2022-03-20 14:03:52 +01:00
Andras Bacsai
3d6b343adc remove mysql 2022-03-19 23:47:05 +01:00
Andras Bacsai
e338cecc14 feat: Add PHP modules 2022-03-19 23:46:33 +01:00
87 changed files with 1907 additions and 870 deletions

View File

@@ -1,10 +1,10 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.0.30", "version": "2.2.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev --host 0.0.0.0", "dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev",
"dev:stop": "docker-compose -f docker-compose-dev.yaml down", "dev:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10", "dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio", "studio": "npx prisma studio",
@@ -25,57 +25,59 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.70", "@sveltejs/adapter-node": "1.0.0-next.73",
"@sveltejs/adapter-static": "1.0.0-next.28", "@sveltejs/kit": "1.0.0-next.303",
"@sveltejs/kit": "1.0.0-next.288",
"@types/bcrypt": "5.0.0", "@types/bcrypt": "5.0.0",
"@types/js-cookie": "3.0.1", "@types/js-cookie": "3.0.1",
"@types/node": "17.0.21", "@types/js-yaml": "^4.0.5",
"@types/node-forge": "1.0.0", "@types/node": "17.0.23",
"@types/node-forge": "1.0.1",
"@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/eslint-plugin": "4.31.1",
"@typescript-eslint/parser": "4.31.1", "@typescript-eslint/parser": "4.31.1",
"@zerodevx/svelte-toast": "0.7.0", "@zerodevx/svelte-toast": "0.7.1",
"autoprefixer": "10.4.2", "autoprefixer": "10.4.4",
"cross-var": "1.1.0", "cross-var": "1.1.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-prettier": "8.4.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "3.4.1", "eslint-plugin-svelte3": "3.4.1",
"husky": "7.0.4", "husky": "7.0.4",
"lint-staged": "12.3.4", "lint-staged": "12.3.7",
"postcss": "8.4.7", "postcss": "8.4.12",
"prettier": "2.5.1", "prettier": "2.6.1",
"prettier-plugin-svelte": "2.6.0", "prettier-plugin-svelte": "2.6.0",
"prettier-plugin-tailwindcss": "0.1.8", "prettier-plugin-tailwindcss": "0.1.8",
"prisma": "3.10.0", "prisma": "3.11.1",
"svelte": "3.46.4", "svelte": "3.46.4",
"svelte-check": "2.4.5", "svelte-check": "2.4.6",
"svelte-preprocess": "4.10.4", "svelte-preprocess": "4.10.4",
"svelte-select": "^4.4.7",
"tailwindcss": "3.0.23", "tailwindcss": "3.0.23",
"ts-node": "10.6.0", "ts-node": "10.7.0",
"tslib": "2.3.1", "tslib": "2.3.1",
"typescript": "4.6.2" "typescript": "4.6.3"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@prisma/client": "3.10.0", "@prisma/client": "3.11.1",
"@sentry/node": "6.18.1", "@sentry/node": "6.19.2",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",
"bullmq": "1.76.0", "bullmq": "1.78.1",
"compare-versions": "4.1.3", "compare-versions": "4.1.3",
"cookie": "0.4.2", "cookie": "0.4.2",
"cooltipz-css": "^2.1.0",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.10.8", "dayjs": "1.11.0",
"dockerode": "3.3.1", "dockerode": "3.3.1",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"generate-password": "1.7.0", "generate-password": "1.7.0",
"get-port": "6.1.2", "get-port": "6.1.2",
"got": "12.0.1", "got": "12.0.2",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"node-forge": "1.2.1", "node-forge": "1.3.0",
"svelte-kit-cookie-session": "2.1.2", "svelte-kit-cookie-session": "2.1.2",
"tailwindcss-scrollbar": "^0.1.0", "tailwindcss-scrollbar": "^0.1.0",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"

537
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "phpModules" TEXT;

View File

@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "ApplicationPersistentStorage" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"path" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationPersistentStorage_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_key" ON "ApplicationPersistentStorage"("applicationId");
-- CreateIndex
CREATE UNIQUE INDEX "ApplicationPersistentStorage_path_key" ON "ApplicationPersistentStorage"("path");
-- CreateIndex
CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_path_key" ON "ApplicationPersistentStorage"("applicationId", "path");

View File

@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "Ghost" (
"id" TEXT NOT NULL PRIMARY KEY,
"defaultEmail" TEXT NOT NULL,
"defaultPassword" TEXT NOT NULL,
"mariadbUser" TEXT NOT NULL,
"mariadbPassword" TEXT NOT NULL,
"mariadbRootUser" TEXT NOT NULL,
"mariadbRootUserPassword" TEXT NOT NULL,
"mariadbDatabase" TEXT,
"mariadbPublicPort" INTEGER,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Ghost_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Ghost_serviceId_key" ON "Ghost"("serviceId");

View File

@@ -72,9 +72,9 @@ model TeamInvitation {
} }
model Application { model Application {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? @unique fqdn String? @unique
repository String? repository String?
configHash String? configHash String?
branch String? branch String?
@@ -86,15 +86,17 @@ model Application {
startCommand String? startCommand String?
baseDirectory String? baseDirectory String?
publishDirectory String? publishDirectory String?
createdAt DateTime @default(now()) phpModules String?
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settings ApplicationSettings? settings ApplicationSettings?
teams Team[] teams Team[]
destinationDockerId String? destinationDockerId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
gitSourceId String? gitSourceId String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
secrets Secret[] secrets Secret[]
persistentStorage ApplicationPersistentStorage[]
} }
model ApplicationSettings { model ApplicationSettings {
@@ -109,6 +111,17 @@ model ApplicationSettings {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model ApplicationPersistentStorage {
id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id])
applicationId String @unique
path String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([applicationId, path])
}
model Secret { model Secret {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
@@ -265,6 +278,7 @@ model Service {
minio Minio? minio Minio?
vscodeserver Vscodeserver? vscodeserver Vscodeserver?
wordpress Wordpress? wordpress Wordpress?
ghost Ghost?
serviceSecret ServiceSecret[] serviceSecret ServiceSecret[]
} }
@@ -319,3 +333,19 @@ model Wordpress {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Ghost {
id String @id @default(cuid())
defaultEmail String
defaultPassword String
mariadbUser String
mariadbPassword String
mariadbRootUser String
mariadbRootUserPassword String
mariadbDatabase String?
mariadbPublicPort Int?
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

8
src/app.d.ts vendored
View File

@@ -7,7 +7,13 @@ declare namespace App {
} }
interface Platform {} interface Platform {}
interface Session extends SessionData {} interface Session extends SessionData {}
interface Stuff {} interface Stuff {
service: any;
application: any;
isRunning: boolean;
appId: string;
readOnly: boolean;
}
} }
interface SessionData { interface SessionData {

View File

@@ -29,10 +29,10 @@ export function makeLabelForStandaloneApplication({
fqdn = `${protocol}://${pullmergeRequestId}.${domain}`; fqdn = `${protocol}://${pullmergeRequestId}.${domain}`;
} }
return [ return [
'--label coolify.managed=true', 'coolify.managed=true',
`--label coolify.version=${version}`, `coolify.version=${version}`,
`--label coolify.type=standalone-application`, `coolify.type=standalone-application`,
`--label coolify.configuration=${base64Encode( `coolify.configuration=${base64Encode(
JSON.stringify({ JSON.stringify({
applicationId, applicationId,
fqdn, fqdn,
@@ -135,6 +135,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
RewriteRule ^(.+)$ index.php [QSA,L] RewriteRule ^(.+)$ index.php [QSA,L]
` `
); );
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
saveBuildLog({ line: 'Copied default configuration file for PHP.', buildId, applicationId }); saveBuildLog({ line: 'Copied default configuration file for PHP.', buildId, applicationId });
} else if (staticDeployments.includes(buildPack)) { } else if (staticDeployments.includes(buildPack)) {
await fs.writeFile( await fs.writeFile(
@@ -142,27 +143,35 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
`user nginx; `user nginx;
worker_processes auto; worker_processes auto;
error_log /var/log/nginx/error.log warn; error_log /docker.stdout;
pid /var/run/nginx.pid; pid /run/nginx.pid;
events { events {
worker_connections 1024; worker_connections 1024;
} }
http { http {
include /etc/nginx/mime.types; log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
access_log off; '"$http_user_agent" "$http_x_forwarded_for"';
sendfile on;
#tcp_nopush on; access_log /docker.stdout main;
keepalive_timeout 65;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server { server {
listen 80; listen 80;
server_name localhost; server_name localhost;
location / { location / {
root /usr/share/nginx/html; root /app;
index index.html; index index.html;
try_files $uri $uri/index.html $uri/ /index.html =404; try_files $uri $uri/index.html $uri/ /index.html =404;
} }
@@ -173,7 +182,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
# #
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;
location = /50x.html { location = /50x.html {
root /usr/share/nginx/html; root /app;
} }
} }

View File

@@ -6,17 +6,17 @@ const createDockerfile = async (data, imageforBuild): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageforBuild}`); Dockerfile.push(`FROM ${imageforBuild}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild); await buildCacheImageWithNode(data, imageForBuild);

View File

@@ -7,15 +7,13 @@ const createDockerfile = async (data, image): Promise<void> => {
const isPnpm = startCommand.includes('pnpm'); const isPnpm = startCommand.includes('pnpm');
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (isPnpm) { if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
Dockerfile.push('RUN pnpm add -g pnpm'); Dockerfile.push('RUN pnpm add -g pnpm');
} }
Dockerfile.push( Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`);
`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${baseDirectory || ''} ./`
);
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);

View File

@@ -16,7 +16,7 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {

View File

@@ -17,7 +17,7 @@ const createDockerfile = async (data, image): Promise<void> => {
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {

View File

@@ -16,7 +16,7 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {

View File

@@ -4,21 +4,19 @@ import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, baseDirectory } = data; const { workdir, baseDirectory } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('RUN a2enmod rewrite'); Dockerfile.push('WORKDIR /app');
Dockerfile.push('WORKDIR /var/www/html'); Dockerfile.push(`COPY .${baseDirectory || ''} /app`);
Dockerfile.push(`COPY .${baseDirectory || ''} /var/www/html`); Dockerfile.push(`COPY /.htaccess .`);
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["apache2-foreground"]');
Dockerfile.push('RUN chown -R www-data /var/www/html');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'php:apache'; const image = 'webdevops/php-nginx';
await createDockerfile(data, image); await createDockerfile(data, image);
await buildImage(data); await buildImage(data);
} catch (error) { } catch (error) {

View File

@@ -7,16 +7,16 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild); await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image); await createDockerfile(data, image);

View File

@@ -7,23 +7,21 @@ const createDockerfile = async (data, image, name): Promise<void> => {
const { workdir, port, applicationId, tag } = data; const { workdir, port, applicationId, tag } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/target target`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target target`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`);
Dockerfile.push(`COPY . .`); Dockerfile.push(`COPY . .`);
Dockerfile.push(`RUN cargo build --release --bin ${name}`); Dockerfile.push(`RUN cargo build --release --bin ${name}`);
Dockerfile.push('FROM debian:buster-slim'); Dockerfile.push('FROM debian:buster-slim');
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push( Dockerfile.push(
`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 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/*`
); );
Dockerfile.push(`RUN update-ca-certificates`); Dockerfile.push(`RUN update-ca-certificates`);
Dockerfile.push( Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target/release/${name} ${name}`);
`COPY --from=${applicationId}:${tag}-cache /usr/src/app/target/release/${name} ${name}`
);
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ["/usr/src/app/${name}"]`); Dockerfile.push(`CMD ["/app/${name}"]`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -15,7 +15,7 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
@@ -33,20 +33,18 @@ const createDockerfile = async (data, image): Promise<void> => {
}); });
} }
if (buildCommand) { if (buildCommand) {
Dockerfile.push( Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`
);
} else { } else {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
} }
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
if (data.buildCommand) await buildCacheImageWithNode(data, imageForBuild); if (data.buildCommand) await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image); await createDockerfile(data, image);

View File

@@ -6,17 +6,17 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild); await buildCacheImageWithNode(data, imageForBuild);

View File

@@ -6,17 +6,17 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
try { try {
const image = 'nginx:stable-alpine'; const image = 'webdevops/nginx:alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild); await buildCacheImageWithNode(data, imageForBuild);
await createDockerfile(data, image); await createDockerfile(data, image);

View File

@@ -9,7 +9,16 @@ export const dateOptions: DateTimeFormatOptions = {
hour12: false hour12: false
}; };
export const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby', 'php']; export const staticDeployments = [
'react',
'vuejs',
'static',
'svelte',
'gatsby',
'php',
'astro',
'eleventy'
];
export const notNodeDeployments = ['php', 'docker', 'rust']; export const notNodeDeployments = ['php', 'docker', 'rust'];
export function getDomain(domain) { export function getDomain(domain) {

View File

@@ -0,0 +1,9 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="ghost logo"
class={isAbsolute ? 'w-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 mx-auto'}
src="/ghost.png"
/>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
viewBox="0 0 220 105"
>
<g>
<path
fill="#FF6D5A"
d="M183.9,0.2c-9.8,0-18,6.7-20.3,15.8h-29.2c-11.5,0-20.8,9.3-20.8,20.8c0,5.7-4.7,10.4-10.4,10.4H99
c-2.3-9.1-10.5-15.8-20.3-15.8c-9.8,0-18,6.7-20.3,15.8H41.7c-2.3-9.1-10.5-15.8-20.3-15.8c-11.6,0-21,9.4-21,21
c0,11.6,9.4,21,21,21c9.8,0,18-6.7,20.3-15.8h16.7c2.3,9.1,10.5,15.8,20.3,15.8c9.7,0,17.9-6.6,20.3-15.6h4.2
c5.7,0,10.4,4.7,10.4,10.4c0,11.5,9.3,20.8,20.8,20.8h6.8c2.3,9.1,10.5,15.8,20.3,15.8c11.6,0,21-9.4,21-21c0-11.6-9.4-21-21-21
c-9.8,0-18,6.7-20.3,15.8h-6.8c-5.7,0-10.4-4.7-10.4-10.4c0-6.3-2.8-11.9-7.2-15.7c4.4-3.8,7.2-9.4,7.2-15.7
c0-5.7,4.7-10.4,10.4-10.4h29.2c2.3,9.1,10.5,15.8,20.3,15.8c11.6,0,21-9.4,21-21C204.9,9.6,195.5,0.2,183.9,0.2z M21.4,63
c-5.8,0-10.6-4.8-10.6-10.6s4.8-10.6,10.6-10.6S32,46.6,32,52.4S27.3,63,21.4,63z M78.7,63c-5.8,0-10.6-4.8-10.6-10.6
s4.8-10.6,10.6-10.6s10.6,4.8,10.6,10.6S84.6,63,78.7,63z M161.5,73.2c5.8,0,10.6,4.8,10.6,10.6s-4.8,10.6-10.6,10.6
s-10.6-4.8-10.6-10.6C150.9,77.9,155.7,73.2,161.5,73.2z M183.9,31.8c-5.8,0-10.6-4.8-10.6-10.6s4.8-10.6,10.6-10.6
s10.6,4.8,10.6,10.6C194.5,27,189.8,31.8,183.9,31.8z"
/>
</g>
</svg>

View File

@@ -0,0 +1,159 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-10 h-10 mx-auto'}
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 640 640"
width="640"
height="640"
><defs
><path
d="M407.55 916.24C471.25 916.24 522.89 967.88 522.89 1031.57C522.89 1113.88 522.89 1245.44 522.89 1327.74C522.89 1391.44 471.25 1443.08 407.55 1443.08C325.25 1443.08 193.68 1443.08 111.38 1443.08C47.69 1443.08 -3.95 1391.44 -3.95 1327.74C-3.95 1245.44 -3.95 1113.88 -3.95 1031.57C-3.95 967.88 47.69 916.24 111.38 916.24C193.68 916.24 325.25 916.24 407.55 916.24Z"
id="a1LdTs1gvU"
/><linearGradient
id="gradientcoH7TNh19"
gradientUnits="userSpaceOnUse"
x1="256.07"
y1="1132.14"
x2="609.11"
y2="1480.42"
><stop style="stop-color: #c2efd2;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #8ff0e5;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M-467.41 394.63C-467.41 554.76 -597.42 684.76 -757.55 684.76C-917.68 684.76 -1047.69 554.76 -1047.69 394.63C-1047.69 234.5 -917.68 104.49 -757.55 104.49C-597.42 104.49 -467.41 234.5 -467.41 394.63Z"
id="a1uaEBd4xM"
/><path
d="M-96.99 -586.14C-57.24 -619.85 -5.79 -604.75 19.26 -580.46C31.43 -568.66 56.57 -546.36 40.97 -491.67C32.76 -462.87 10.41 -436.4 -26.05 -412.27C-15.07 -377.85 -5.6 -344.76 2.36 -313C14.29 -265.36 13.55 -189.67 -26.05 -155.4C-67.27 -119.73 -166.91 -104.09 -234.24 -103.09C-301.57 -102.1 -406.19 -113.09 -461.6 -155.4C-517.01 -197.7 -512.24 -257.07 -498.04 -313C-488.58 -350.28 -476.43 -383.38 -461.6 -412.27C-505.54 -441.3 -530.54 -467.76 -536.6 -491.67C-545.68 -527.54 -530.93 -565.61 -501.12 -586.14C-471.31 -606.67 -435.18 -606.9 -400.45 -586.14C-377.3 -572.3 -354.79 -542.13 -332.92 -495.62C-287.85 -505.25 -254.96 -509.57 -234.24 -508.6C-214.74 -507.68 -186.57 -503.36 -149.72 -495.62C-135.81 -537.95 -118.23 -568.12 -96.99 -586.14Z"
id="f8p7QlEjN3"
/><linearGradient
id="gradienta4Tg99ZOOp"
gradientUnits="userSpaceOnUse"
x1="-440.25"
y1="-388.59"
x2="-100.49"
y2="-147.33"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #7ae6a1;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M-86.03 -10.69C-61.35 -10.69 -41.34 9.32 -41.34 34.01C-41.34 119.07 -41.34 329.58 -41.34 414.65C-41.34 439.33 -61.35 459.34 -86.03 459.34C-136.01 459.34 -241.25 459.34 -291.23 459.34C-315.92 459.34 -335.93 439.33 -335.93 414.65C-335.93 329.58 -335.93 119.07 -335.93 34.01C-335.93 9.32 -315.92 -10.69 -291.23 -10.69C-241.25 -10.69 -136.01 -10.69 -86.03 -10.69Z"
id="d32ZZRxd1S"
/><linearGradient
id="gradientb1JxIe4xUm"
gradientUnits="userSpaceOnUse"
x1="-791.65"
y1="-33.27"
x2="892.1"
y2="418.94"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #5ae98f;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M-257.95 458.12C-247.92 449.62 -234.93 453.43 -228.61 459.56C-225.54 462.54 -219.19 468.17 -223.13 481.97C-225.2 489.24 -230.84 495.92 -240.05 502.01C-237.27 510.7 -234.88 519.06 -232.88 527.07C-229.86 539.1 -230.05 558.21 -240.05 566.86C-250.45 575.86 -275.6 579.81 -292.6 580.06C-309.6 580.31 -336.01 577.54 -349.99 566.86C-363.98 556.18 -362.77 541.19 -359.19 527.07C-356.8 517.66 -353.73 509.31 -349.99 502.01C-361.08 494.69 -367.39 488.01 -368.92 481.97C-371.22 472.92 -367.49 463.31 -359.97 458.12C-352.44 452.94 -343.32 452.88 -334.56 458.12C-328.71 461.62 -323.03 469.23 -317.51 480.97C-306.13 478.54 -297.83 477.45 -292.6 477.7C-287.68 477.93 -280.56 479.02 -271.26 480.97C-267.75 470.29 -263.32 462.67 -257.95 458.12Z"
id="b19LRRbPrG"
/><path
d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.26 407.74 99.26 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z"
id="bN5StdyPU"
/><linearGradient
id="gradientb1HT15TsY0"
gradientUnits="userSpaceOnUse"
x1="259.78"
y1="261.15"
x2="463.85"
y2="456.49"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #86e6a9;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M393.81 -775.89C428.26 -748.09 439.99 -725.54 429 -708.22C412.51 -682.24 353.16 -646.07 324.5 -657.93C305.39 -665.83 294.22 -687.32 290.97 -722.41C292.69 -748.43 304.61 -767.19 326.73 -778.69C348.85 -790.19 371.21 -789.26 393.81 -775.89Z"
id="arh6miPP2"
/><linearGradient
id="gradientc2g6rBSAiq"
gradientUnits="userSpaceOnUse"
x1="330.1"
y1="-733.26"
x2="419.69"
y2="-707.1"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #86e6a9;stop-opacity: 1"
offset="100%"
/></linearGradient
><path
d="M675.36 -369.24C669.97 -325.31 657.02 -303.43 636.51 -303.61C605.74 -303.87 543.67 -335.15 538.59 -365.74C535.2 -386.14 547.54 -406.99 575.61 -428.29C598.61 -440.58 620.83 -440.37 642.29 -427.67C663.74 -414.97 674.77 -395.49 675.36 -369.24Z"
id="a2VENFzCvL"
/><linearGradient
id="gradientc18GuJy4sZ"
gradientUnits="userSpaceOnUse"
x1="605.5"
y1="-400.8"
x2="630.64"
y2="-310.92"
><stop style="stop-color: #5cdd8b;stop-opacity: 1" offset="0%" /><stop
style="stop-color: #86e6a9;stop-opacity: 1"
offset="100%"
/></linearGradient
></defs
><g
><g
><g><use xlink:href="#a1LdTs1gvU" opacity="1" fill="url(#gradientcoH7TNh19)" /></g><g
><use xlink:href="#a1uaEBd4xM" opacity="1" fill="#ebf0ed" fill-opacity="1" /></g
><g
><use xlink:href="#f8p7QlEjN3" opacity="1" fill="url(#gradienta4Tg99ZOOp)" /><g
><use
xlink:href="#f8p7QlEjN3"
opacity="1"
fill-opacity="0"
stroke="#ffffff"
stroke-width="98"
stroke-opacity="0.57"
/></g
></g
><g
><use xlink:href="#d32ZZRxd1S" opacity="1" fill="url(#gradientb1JxIe4xUm)" /><g
><use
xlink:href="#d32ZZRxd1S"
opacity="1"
fill-opacity="0"
stroke="#f2f2f2"
stroke-width="60"
stroke-opacity="0.51"
/></g
></g
><g
><use xlink:href="#b19LRRbPrG" opacity="1" fill="#d8ad9a" fill-opacity="1" /><g
><use
xlink:href="#b19LRRbPrG"
opacity="1"
fill-opacity="0"
stroke="#ffffff"
stroke-width="17"
stroke-opacity="1"
/></g
></g
><g
><use xlink:href="#bN5StdyPU" opacity="1" fill="url(#gradientb1HT15TsY0)" /><g
><use
xlink:href="#bN5StdyPU"
opacity="1"
fill-opacity="0"
stroke="#f2f2f2"
stroke-width="200"
stroke-opacity="0.51"
/></g
></g
><g><use xlink:href="#arh6miPP2" opacity="1" fill="url(#gradientc2g6rBSAiq)" /></g><g
><use xlink:href="#a2VENFzCvL" opacity="1" fill="url(#gradientc18GuJy4sZ)" /></g
></g
></g
></svg
>

View File

@@ -66,6 +66,7 @@ export async function removeApplication({ id, teamId }) {
await prisma.buildLog.deleteMany({ where: { applicationId: id } }); await prisma.buildLog.deleteMany({ where: { applicationId: id } });
await prisma.build.deleteMany({ where: { applicationId: id } }); await prisma.build.deleteMany({ where: { applicationId: id } });
await prisma.secret.deleteMany({ where: { applicationId: id } }); await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
} }
@@ -134,7 +135,8 @@ export async function getApplication({ id, teamId }) {
destinationDocker: true, destinationDocker: true,
settings: true, settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } }, gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true secrets: true,
persistentStorage: true
} }
}); });
@@ -263,3 +265,7 @@ export async function createBuild({
} }
}); });
} }
export async function getPersistentStorage(id) {
return await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } });
}

View File

@@ -107,6 +107,7 @@ export const supportedServiceTypesAndVersions = [
name: 'plausibleanalytics', name: 'plausibleanalytics',
fancyName: 'Plausible Analytics', fancyName: 'Plausible Analytics',
baseImage: 'plausible/analytics', baseImage: 'plausible/analytics',
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
versions: ['latest'], versions: ['latest'],
ports: { ports: {
main: 8000 main: 8000
@@ -143,6 +144,7 @@ export const supportedServiceTypesAndVersions = [
name: 'wordpress', name: 'wordpress',
fancyName: 'Wordpress', fancyName: 'Wordpress',
baseImage: 'wordpress', baseImage: 'wordpress',
images: ['bitnami/mysql:5.7'],
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'], versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
ports: { ports: {
main: 80 main: 80
@@ -165,6 +167,34 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 8010 main: 8010
} }
},
{
name: 'n8n',
fancyName: 'n8n',
baseImage: 'n8nio/n8n',
versions: ['latest'],
ports: {
main: 5678
}
},
{
name: 'uptimekuma',
fancyName: 'Uptime Kuma',
baseImage: 'louislam/uptime-kuma',
versions: ['latest'],
ports: {
main: 3001
}
},
{
name: 'ghost',
fancyName: 'Ghost',
baseImage: 'bitnami/ghost',
images: ['bitnami/mariadb'],
versions: ['latest'],
ports: {
main: 2368
}
} }
]; ];
@@ -189,6 +219,13 @@ export function getServiceImage(type) {
} }
return ''; return '';
} }
export function getServiceImages(type) {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.images;
}
return [];
}
export function generateDatabaseConfiguration(database) { export function generateDatabaseConfiguration(database) {
const { const {
id, id,

View File

@@ -1,3 +1,4 @@
import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import cuid from 'cuid'; import cuid from 'cuid';
import { generatePassword } from '.'; import { generatePassword } from '.';
@@ -20,6 +21,7 @@ export async function getService({ id, teamId }) {
minio: true, minio: true,
vscodeserver: true, vscodeserver: true,
wordpress: true, wordpress: true,
ghost: true,
serviceSecret: true serviceSecret: true
} }
}); });
@@ -43,12 +45,18 @@ export async function getService({ id, teamId }) {
if (body.wordpress?.mysqlRootUserPassword) if (body.wordpress?.mysqlRootUserPassword)
body.wordpress.mysqlRootUserPassword = decrypt(body.wordpress.mysqlRootUserPassword); body.wordpress.mysqlRootUserPassword = decrypt(body.wordpress.mysqlRootUserPassword);
if (body.ghost?.mariadbPassword) body.ghost.mariadbPassword = decrypt(body.ghost.mariadbPassword);
if (body.ghost?.mariadbRootUserPassword)
body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword);
if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword);
if (body?.serviceSecret.length > 0) { if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => { body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value); s.value = decrypt(s.value);
return s; return s;
}); });
} }
return { ...body }; return { ...body };
} }
@@ -119,6 +127,44 @@ export async function configureServiceType({ id, type }) {
type type
} }
}); });
} else if (type === 'n8n') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'uptimekuma') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'ghost') {
const defaultEmail = `${cuid()}@coolify.io`;
const defaultPassword = encrypt(generatePassword());
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword());
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
ghost: {
create: {
defaultEmail,
defaultPassword,
mariadbUser,
mariadbPassword,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
} }
} }
export async function setServiceVersion({ id, version }) { export async function setServiceVersion({ id, version }) {
@@ -139,7 +185,7 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, usernam
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } }); await prisma.service.update({ where: { id }, data: { name, fqdn } });
} }
export async function updateNocoDbOrMinioService({ id, fqdn, name }) { export async function updateService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateLanguageToolService({ id, fqdn, name }) { export async function updateLanguageToolService({ id, fqdn, name }) {
@@ -160,8 +206,15 @@ export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConf
export async function updateMinioService({ id, publicPort }) { export async function updateMinioService({ id, publicPort }) {
return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
} }
export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) {
return await prisma.service.update({
where: { id },
data: { fqdn, name, ghost: { update: { mariadbDatabase } } }
});
}
export async function removeService({ id }) { export async function removeService({ id }) {
await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });

View File

@@ -20,7 +20,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
const isPnpm = checkPnpm(installCommand, buildCommand); const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
@@ -65,14 +65,14 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
} = data; } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`); Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef'); Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push('COPY . .'); Dockerfile.push('COPY . .');
Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json'); Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json');
Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef'); Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push(`COPY --from=planner-${applicationId} /usr/src/app/recipe.json recipe.json`); Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json'); Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug }); await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });

View File

@@ -82,7 +82,7 @@ backend backend-certbot
# updatedAt={{updatedAt}} # updatedAt={{updatedAt}}
backend {{domain}} backend {{domain}}
option forwardfor option forwardfor
server {{id}} {{id}}:{{port}} check server {{id}} {{id}}:{{port}}
{{/isRunning}} {{/isRunning}}
{{/applications}} {{/applications}}
@@ -91,7 +91,7 @@ backend {{domain}}
# updatedAt={{updatedAt}} # updatedAt={{updatedAt}}
backend {{domain}} backend {{domain}}
option forwardfor option forwardfor
server {{id}} {{id}}:{{port}} check server {{id}} {{id}}:{{port}}
{{/isRunning}} {{/isRunning}}
{{/services}} {{/services}}

View File

@@ -45,6 +45,7 @@ export default async function ({
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', ''); return commit.replace('\n', '');
} catch (error) { } catch (error) {
console.log({ error });
return ErrorHandler(error); return ErrorHandler(error);
} }
} }

View File

@@ -103,34 +103,40 @@ export async function generateSSLCerts() {
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
}); });
for (const application of applications) { for (const application of applications) {
const { try {
fqdn, if (application.fqdn && application.destinationDockerId) {
id, const {
destinationDocker: { engine, network }, fqdn,
settings: { previews } id,
} = application; destinationDocker: { engine, network },
const isRunning = await checkContainer(engine, id); settings: { previews }
const domain = getDomain(fqdn); } = application;
const isHttps = fqdn.startsWith('https://'); const isRunning = await checkContainer(engine, id);
if (isRunning) { const domain = getDomain(fqdn);
if (isHttps) ssls.push({ domain, id, isCoolify: false }); const isHttps = fqdn.startsWith('https://');
} if (isRunning) {
if (previews) { if (isHttps) ssls.push({ domain, id, isCoolify: false });
const host = getEngine(engine); }
const { stdout } = await asyncExecShell( if (previews) {
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` const host = getEngine(engine);
); const { stdout } = await asyncExecShell(
const containers = stdout `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
.trim() );
.split('\n') const containers = stdout
.filter((a) => a) .trim()
.map((c) => c.replace(/"/g, '')); .split('\n')
if (containers.length > 0) { .filter((a) => a)
for (const container of containers) { .map((c) => c.replace(/"/g, ''));
let previewDomain = `${container.split('-')[1]}.${domain}`; if (containers.length > 0) {
if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false }); for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false });
}
}
} }
} }
} catch (error) {
console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`);
} }
} }
const services = await db.prisma.service.findMany({ const services = await db.prisma.service.findMany({
@@ -139,26 +145,33 @@ export async function generateSSLCerts() {
minio: true, minio: true,
plausibleAnalytics: true, plausibleAnalytics: true,
vscodeserver: true, vscodeserver: true,
wordpress: true wordpress: true,
ghost: true
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
}); });
for (const service of services) { for (const service of services) {
const { try {
fqdn, if (service.fqdn && service.destinationDockerId) {
id, const {
type, fqdn,
destinationDocker: { engine } id,
} = service; type,
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); destinationDocker: { engine }
if (found) { } = service;
const domain = getDomain(fqdn); const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
const isHttps = fqdn.startsWith('https://'); if (found) {
const isRunning = await checkContainer(engine, id); const domain = getDomain(fqdn);
if (isRunning) { const isHttps = fqdn.startsWith('https://');
if (isHttps) ssls.push({ domain, id, isCoolify: false }); const isRunning = await checkContainer(engine, id);
if (isRunning) {
if (isHttps) ssls.push({ domain, id, isCoolify: false });
}
}
} }
} catch (error) {
console.log(`Error during generateSSLCerts with ${service.fqdn}: ${error}`);
} }
} }
const { fqdn } = await db.prisma.setting.findFirst(); const { fqdn } = await db.prisma.setting.findFirst();

View File

@@ -19,6 +19,7 @@ import {
makeLabelForStandaloneApplication, makeLabelForStandaloneApplication,
setDefaultConfiguration setDefaultConfiguration
} from '$lib/buildPacks/common'; } from '$lib/buildPacks/common';
import yaml from 'js-yaml';
export default async function (job) { export default async function (job) {
/* /*
@@ -45,17 +46,33 @@ export default async function (job) {
publishDirectory, publishDirectory,
projectId, projectId,
secrets, secrets,
phpModules,
type, type,
pullmergeRequestId = null, pullmergeRequestId = null,
sourceBranch = null, sourceBranch = null,
settings settings,
persistentStorage
} = job.data; } = job.data;
const { debug } = settings; const { debug } = settings;
await asyncSleep(1000);
await asyncSleep(500);
await db.prisma.build.updateMany({
where: {
status: 'queued',
id: { not: buildId },
applicationId,
createdAt: { lt: new Date(new Date().getTime() - 60 * 60 * 1000) }
},
data: { status: 'failed' }
});
let imageId = applicationId; let imageId = applicationId;
let domain = getDomain(fqdn); let domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://'); let volumes =
persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${
buildPack !== 'docker' ? '/app' : ''
}${storage.path}`;
}) || [];
// Previews, we need to get the source branch and set subdomain // Previews, we need to get the source branch and set subdomain
if (pullmergeRequestId) { if (pullmergeRequestId) {
branch = sourceBranch; branch = sourceBranch;
@@ -101,6 +118,9 @@ export default async function (job) {
deployKeyId: gitSource.gitlabApp?.deployKeyId || null, deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null
}); });
if (!commit) {
throw new Error('No commit found?');
}
let tag = commit.slice(0, 7); let tag = commit.slice(0, 7);
if (pullmergeRequestId) { if (pullmergeRequestId) {
tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`; tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`;
@@ -179,7 +199,8 @@ export default async function (job) {
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
secrets secrets,
phpModules
}); });
else { else {
saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
@@ -239,14 +260,53 @@ export default async function (job) {
} }
try { try {
saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const { stderr } = await asyncExecShell( // for await (const volume of volumes) {
`DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join( // const id = volume.split(':')[0];
' ' // try {
)} --name ${imageId} --network ${ // await asyncExecShell(`DOCKER_HOST=${host} docker volume inspect ${id}`);
docker.network // } catch (error) {
} --restart always -d ${applicationId}:${tag}` // await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}`);
// }
// }
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const compose = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network],
labels: labels,
depends_on: [],
restart: 'always'
}
},
networks: {
[docker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(compose));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
); );
if (stderr) console.log(stderr);
// const { stderr } = await asyncExecShell(
// `DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join(
// ' '
// )} --name ${imageId} --network ${docker.network} --restart always ${volumes.length > 0 ? volumes : ''
// } -d ${applicationId}:${tag}`
// );
saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) { } catch (error) {
saveBuildLog({ line: error, buildId, applicationId }); saveBuildLog({ line: error, buildId, applicationId });

View File

@@ -24,7 +24,7 @@ export default async function () {
console.log(error); console.log(error);
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f`); await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -4,6 +4,7 @@ export default async function () {
try { try {
return await generateSSLCerts(); return await generateSSLCerts();
} catch (error) { } catch (error) {
console.log(error);
throw error; throw error;
} }
} }

View File

@@ -88,7 +88,14 @@
try { try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application }); const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
toast.push('Deployment queued.'); toast.push('Deployment queued.');
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`); console.log($page.url);
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
replaceState: true
});
}
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
@@ -271,6 +278,35 @@
</svg></button </svg></button
></a ></a
> >
<a
href="/applications/{id}/storage"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storage`}
>
<button
title="Persistent Storage"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Persistent Storage"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
<a <a
href="/applications/{id}/previews" href="/applications/{id}/previews"
sveltekit:prefetch sveltekit:prefetch

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
export let application; export let application;
import Select from 'svelte-select';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
@@ -35,6 +35,9 @@
Authorization: `token ${$gitTokens.githubToken}` Authorization: `token ${$gitTokens.githubToken}`
}); });
} }
let reposSelectOptions;
let branchSelectOptions;
async function loadRepositories() { async function loadRepositories() {
let page = 1; let page = 1;
let reposCount = 0; let reposCount = 0;
@@ -49,8 +52,13 @@
} }
} }
loading.repositories = false; loading.repositories = false;
reposSelectOptions = repositories.map((repo) => ({
value: repo.full_name,
label: repo.name
}));
} }
async function loadBranches() { async function loadBranches(event) {
selected.repository = event.detail.value;
loading.branches = true; loading.branches = true;
selected.branch = undefined; selected.branch = undefined;
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id; selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
@@ -58,6 +66,10 @@
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, { branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
Authorization: `token ${$gitTokens.githubToken}` Authorization: `token ${$gitTokens.githubToken}`
}); });
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
return; return;
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
@@ -65,7 +77,8 @@
loading.branches = false; loading.branches = false;
} }
} }
async function isBranchAlreadyUsed() { async function isBranchAlreadyUsed(event) {
selected.branch = event.detail.value;
try { try {
const data = await get( const data = await get(
`/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}` `/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}`
@@ -151,49 +164,35 @@
<a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a> <a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a>
</div> </div>
{:else} {:else}
<form on:submit|preventDefault={handleSubmit}> <form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center">
<div> <div class="flex-col space-y-3 md:space-y-0 space-x-1">
{#if loading.repositories} <div class="flex gap-4">
<select name="repository" disabled class="w-96"> <div class="custom-select-wrapper">
<option selected value="">Loading repositories...</option> <Select
</select> placeholder={loading.repositories
{:else} ? 'Loading repositories ...'
<select : 'Please select a repository'}
name="repository" id="repository"
class="w-96" on:select={loadBranches}
bind:value={selected.repository} items={reposSelectOptions}
on:change={loadBranches} isDisabled={loading.repositories}
> />
<option value="" disabled selected>Please select a repository</option> </div>
{#each repositories as repository} <input class="hidden" bind:value={selected.projectId} name="projectId" />
<option value={repository.full_name}>{repository.name}</option> <div class="custom-select-wrapper">
{/each} <Select
</select> placeholder={loading.branches
{/if} ? 'Loading branches ...'
<input class="hidden" bind:value={selected.projectId} name="projectId" /> : !selected.repository
{#if loading.branches} ? 'Please select a repository first'
<select name="branch" disabled class="w-96"> : 'Please select a branch'}
<option selected value="">Loading branches...</option> id="repository"
</select> on:select={isBranchAlreadyUsed}
{:else} items={branchSelectOptions}
<select isDisabled={loading.branches || !selected.repository}
name="branch" />
class="w-96" </div>
disabled={!selected.repository} </div>
bind:value={selected.branch}
on:change={isBranchAlreadyUsed}
>
{#if !selected.repository}
<option value="" disabled selected>Select a repository first</option>
{:else}
<option value="" disabled selected>Please select a branch</option>
{/if}
{#each branches as branch}
<option value={branch.name}>{branch.name}</option>
{/each}
</select>
{/if}
</div> </div>
<div class="pt-5 flex-col flex justify-center items-center space-y-4"> <div class="pt-5 flex-col flex justify-center items-center space-y-4">
<button <button

View File

@@ -41,7 +41,7 @@
gitlabApp: Prisma.GitlabApp; gitlabApp: Prisma.GitlabApp;
githubApp: Prisma.GithubApp; githubApp: Prisma.GithubApp;
}; };
sources = sources.filter( const filteredSources = sources.filter(
(source) => (source) =>
(source.type === 'github' && source.githubAppId && source.githubApp.installationId) || (source.type === 'github' && source.githubAppId && source.githubApp.installationId) ||
(source.type === 'gitlab' && source.gitlabAppId) (source.type === 'gitlab' && source.gitlabAppId)
@@ -59,8 +59,8 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div> <div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
</div> </div>
<div class="flex justify-center"> <div class="flex flex-col justify-center">
{#if !sources || sources.length === 0} {#if !filteredSources || filteredSources.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2">No configurable Git Source found</div> <div class="pb-2">No configurable Git Source found</div>
<div class="flex justify-center"> <div class="flex justify-center">
@@ -83,7 +83,7 @@
</div> </div>
{:else} {:else}
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each sources as source} {#each filteredSources as source}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}> <form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button <button

View File

@@ -11,6 +11,7 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const { pullmergeRequestId = null, branch } = await event.request.json();
try { try {
const buildId = cuid(); const buildId = cuid();
const applicationFound = await db.getApplication({ id, teamId }); const applicationFound = await db.getApplication({ id, teamId });
@@ -42,7 +43,17 @@ export const post: RequestHandler = async (event) => {
type: 'manual' type: 'manual'
} }
}); });
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound }); if (pullmergeRequestId) {
await buildQueue.add(buildId, {
build_id: buildId,
type: 'manual',
...applicationFound,
sourceBranch: branch,
pullmergeRequestId
});
} else {
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound });
}
return { return {
status: 200, status: 200,
body: { body: {

View File

@@ -57,7 +57,6 @@
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy; let autodeploy = application.settings.autodeploy;
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`; application.fqdn = `http://${cuid()}.demo.coolify.io`;
} }
@@ -110,7 +109,7 @@
try { try {
await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave }); await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave });
await post(`/applications/${id}.json`, { ...application }); await post(`/applications/${id}.json`, { ...application });
return window.location.reload(); return toast.push('Configurations saved.');
} catch ({ error }) { } catch ({ error }) {
if (error.startsWith('DNS not set')) { if (error.startsWith('DNS not set')) {
forceSave = true; forceSave = true;
@@ -363,7 +362,6 @@
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100" <label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"

View File

@@ -84,7 +84,7 @@
<LoadingLogs /> <LoadingLogs />
{/if} {/if}
{#if currentStatus === 'queued'} {#if currentStatus === 'queued'}
<div class="text-center">Queued and waiting for execution.</div> <div class="text-center font-bold text-xl">Queued and waiting for execution.</div>
{:else} {:else}
<div class="flex justify-end sticky top-0 p-2"> <div class="flex justify-end sticky top-0 p-2">
<button <button

View File

@@ -54,7 +54,6 @@
return build; return build;
}); });
return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }

View File

@@ -26,15 +26,28 @@
export let applicationSecrets; export let applicationSecrets;
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import Secret from '../secrets/_Secret.svelte'; import Secret from '../secrets/_Secret.svelte';
import { get } from '$lib/api'; import { get, post } from '$lib/api';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params; const { id } = $page.params;
async function refreshSecrets() { async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets.json`); const data = await get(`/applications/${id}/secrets.json`);
PRMRSecrets = [...data.secrets]; PRMRSecrets = [...data.secrets];
} }
async function redeploy(container) {
try {
await post(`/applications/${id}/deploy.json`, {
pullmergeRequestId: container.pullmergeRequestId,
branch: container.branch
});
toast.push('Application redeployed queued.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@@ -90,6 +103,11 @@
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div> <div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
</div> </div>
</a> </a>
<div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>Redeploy</button
>
</div>
{/each} {/each}
{:else} {:else}
<div class="flex-col"> <div class="flex-col">

View File

@@ -0,0 +1,73 @@
<script lang="ts">
export let isNew = false;
export let storage = {
id: null,
path: null
};
import { del, post } from '$lib/api';
import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) {
try {
if (!storage.path) return errorNotification('Path is required.');
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
storage.path.replace(/\/\//g, '/');
await post(`/applications/${id}/storage.json`, {
path: storage.path,
storageId: storage.id,
newStorage
});
dispatch('refresh');
if (isNew) {
storage.path = null;
storage.id = null;
}
if (newStorage) toast.push('Storage saved.');
else toast.push('Storage updated.');
} catch ({ error }) {
return errorNotification(error);
}
}
async function removeStorage() {
try {
await del(`/applications/${id}/storage.json`, { path: storage.path });
dispatch('refresh');
toast.push('Storage deleted.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<td>
<input
bind:value={storage.path}
required
placeholder="eg: /sqlite.db"
class=" border border-dashed border-coolgray-300"
/>
</td>
<td>
{#if isNew}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}>Add</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="" on:click={() => saveStorage(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}>Remove</button>
</div>
</div>
{/if}
</td>

View File

@@ -0,0 +1,63 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event, false);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const persistentStorages = await db.getPersistentStorage(id);
return {
body: {
persistentStorages
}
};
} catch (error) {
return ErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { path, newStorage, storageId } = await event.request.json();
try {
if (newStorage) {
await db.prisma.applicationPersistentStorage.create({
data: { path, application: { connect: { id } } }
});
} else {
await db.prisma.applicationPersistentStorage.update({
where: { id: storageId },
data: { path }
});
}
return {
status: 201
};
} catch (error) {
return ErrorHandler(error);
}
};
export const del: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { path } = await event.request.json();
try {
await db.prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id, path } });
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,73 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff }) => {
let endpoint = `/applications/${params.id}/storage.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
application: stuff.application,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts">
export let application;
export let persistentStorages;
import { getDomain } from '$lib/components/common';
import { page } from '$app/stores';
import Storage from './_Storage.svelte';
import { get } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
async function refreshStorage() {
const data = await get(`/applications/${id}/storage.json`);
persistentStorages = [...data.persistentStorages];
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Persistent storage for <a href={application.fqdn} target="_blank"
>{getDomain(application.fqdn)}</a
>
</div>
</div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={'You can specify any folder that you want to be persistent across deployments. <br>This is useful for storing data such as a database (SQLite) or a cache.'}
/>
</div>
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Path</th>
</tr>
</thead>
<tbody>
{#each persistentStorages as storage}
{#key storage.id}
<tr>
<Storage on:refresh={refreshStorage} {storage} />
</tr>
{/key}
{/each}
<tr>
<Storage on:refresh={refreshStorage} isNew />
</tr>
</tbody>
</table>
</div>

View File

@@ -1,34 +1,19 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const endpoint = '/applications.json';
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts"> <script lang="ts">
export let applications: Array<Application>; export let applications: Array<Application>;
import { session } from '$app/stores'; import { session } from '$app/stores';
import Application from './_Application.svelte'; import Application from './_Application.svelte';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
async function newApplication() {
const { id } = await post('/applications/new', {});
return await goto(`/applications/${id}`, { replaceState: true });
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl ">Applications</div> <div class="mr-4 text-2xl ">Applications</div>
{#if $session.isAdmin} {#if $session.isAdmin}
<a href="/new/application" class="add-icon bg-green-600 hover:bg-green-500"> <div on:click={newApplication} class="add-icon cursor-pointer bg-green-600 hover:bg-green-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -42,7 +27,7 @@
d="M12 6v6m0 0v6m0-6h6m-6 0H6" d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg /></svg
> >
</a> </div>
{/if} {/if}
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">

View File

@@ -6,10 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const name = uniqueName();
const { name } = await event.request.json();
if (!name) return { status: 400, body: { error: 'Missing name.' } };
try { try {
const { id } = await db.newApplication({ name, teamId }); const { id } = await db.newApplication({ name, teamId });
return { status: 201, body: { id } }; return { status: 201, body: { id } };

View File

@@ -8,12 +8,22 @@ export const get: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
try { try {
const applicationsCount = await (await db.listApplications(teamId)).length; const applicationsCount = await db.prisma.application.count({
const sourcesCount = await (await db.listSources(teamId)).length; where: { teams: { some: { id: teamId } } }
const destinationsCount = await (await db.listDestinations(teamId)).length; });
const teamsCount = await (await db.getMyTeams({ userId })).length; const sourcesCount = await db.prisma.gitSource.count({
const databasesCount = await (await db.listDatabases(teamId)).length; where: { teams: { some: { id: teamId } } }
const servicesCount = await (await db.listServices(teamId)).length; });
const destinationsCount = await db.prisma.destinationDocker.count({
where: { teams: { some: { id: teamId } } }
});
const teamsCount = await db.prisma.permission.count({ where: { userId } });
const databasesCount = await db.prisma.database.count({
where: { teams: { some: { id: teamId } } }
});
const servicesCount = await db.prisma.service.count({
where: { teams: { some: { id: teamId } } }
});
return { return {
body: { body: {
applicationsCount, applicationsCount,

View File

@@ -1,24 +1,3 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const url = `/databases.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts"> <script lang="ts">
export let databases; export let databases;
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
@@ -27,11 +6,18 @@
import MySQL from '$lib/components/svg/databases/MySQL.svelte'; import MySQL from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte'; import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
async function newDatabase() {
const { id } = await post('/databases/new', {});
return await goto(`/databases/${id}`, { replaceState: true });
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Databases</div> <div class="mr-4 text-2xl tracking-tight">Databases</div>
<a href="/new/database" class="add-icon bg-purple-600 hover:bg-purple-500"> <div on:click={newDatabase} class="add-icon cursor-pointer bg-purple-600 hover:bg-purple-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -45,7 +31,7 @@
d="M12 6v6m0 0v6m0-6h6m-6 0H6" d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg /></svg
> >
</a> </div>
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">

View File

@@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
try { try {
const databases = await db.listDatabases(teamId); const databases = await db.listDatabases(teamId);
return { return {

View File

@@ -1,4 +1,4 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails, uniqueName } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
@@ -6,9 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const name = uniqueName();
const { name } = await event.request.json();
try { try {
const { id } = await db.newDatabase({ name, teamId }); const { id } = await db.newDatabase({ name, teamId });
return { status: 201, body: { id } }; return { status: 201, body: { id } };

View File

@@ -103,7 +103,7 @@
} }
async function forceRestartProxy() { async function forceRestartProxy() {
const sure = confirm( const sure = confirm(
'Are you sure you want to restart the proxy? Everyting will be reconfigured in ~10 sec.' 'Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.'
); );
if (sure) { if (sure) {
try { try {

View File

@@ -106,7 +106,7 @@
} }
async function forceRestartProxy() { async function forceRestartProxy() {
const sure = confirm( const sure = confirm(
'Are you sure you want to restart the proxy? Everyting will be reconfigured in ~10 sec.' 'Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.'
); );
if (sure) { if (sure) {
try { try {

View File

@@ -1,53 +0,0 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const url = `/common/getUniqueName.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let name;
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { errorNotification } from '$lib/form';
let nameEl: HTMLInputElement;
onMount(() => {
nameEl.focus();
});
async function handleSubmit() {
try {
const { id } = await post('/new/application.json', { name });
return await goto(`/applications/${id}`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Application</div>
</div>
<div class="pt-10">
<form on:submit|preventDefault={handleSubmit}>
<div class="flex flex-col items-center space-y-4">
<input name="name" placeholder="Application name" bind:this={nameEl} bind:value={name} />
<button type="submit" class="bg-green-600 hover:bg-green-500">Save</button>
</div>
</form>
</div>

View File

@@ -1,59 +0,0 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, session }) => {
const url = `/common/getUniqueName.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let name;
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
let autofocus;
onMount(() => {
autofocus.focus();
});
async function handleSubmit() {
try {
const { id } = await post('/new/database.json', { name });
return await goto(`/databases/${id}`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Database</div>
</div>
<div class="pt-10">
<form on:submit|preventDefault={handleSubmit}>
<div class="flex flex-col items-center space-y-4">
<input
name="name"
placeholder="Database name"
required
bind:this={autofocus}
bind:value={name}
/>
<button type="submit" class="bg-purple-600 hover:bg-purple-500">Save</button>
</div>
</form>
</div>

View File

@@ -1,59 +0,0 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, session }) => {
const url = `/common/getUniqueName.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let name;
import { enhance, errorNotification } from '$lib/form';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
let autofocus;
onMount(() => {
autofocus.focus();
});
async function handleSubmit() {
try {
const { id } = await post(`/new/service.json`, { name });
return await goto(`/services/${id}`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Service</div>
</div>
<div class="pt-10">
<form on:submit|preventDefault={handleSubmit}>
<div class="flex flex-col items-center space-y-4">
<input
name="name"
placeholder="Service name"
required
bind:this={autofocus}
bind:value={name}
/>
<button type="submit" class="bg-pink-600 hover:bg-pink-500">Save</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,90 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
export let readOnly;
export let service;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Ghost</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="email">Default Email Address</label>
<input
name="email"
id="email"
disabled
readonly
placeholder="Email address"
value={service.ghost.defaultEmail}
required
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="defaultPassword">Default Password</label>
<CopyPasswordField
id="defaultPassword"
isPasswordField
readonly
disabled
name="defaultPassword"
value={service.ghost.defaultPassword}
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbUser">Username</label>
<CopyPasswordField
name="mariadbUser"
id="mariadbUser"
value={service.ghost.mariadbUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbPassword">Password</label>
<CopyPasswordField
id="mariadbPassword"
isPasswordField
readonly
disabled
name="mariadbPassword"
value={service.ghost.mariadbPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbDatabase">Database</label>
<input
name="mariadbDatabase"
id="mariadbDatabase"
required
readonly={readOnly}
disabled={readOnly}
bind:value={service.ghost.mariadbDatabase}
placeholder="eg: ghost_db"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUser">Root DB User</label>
<CopyPasswordField
id="mariadbRootUser"
isPasswordField
readonly
disabled
name="mariadbRootUser"
value={service.ghost.mariadbRootUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUserPassword">Root DB Password</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField
readonly
disabled
name="mariadbRootUserPassword"
value={service.ghost.mariadbRootUserPassword}
/>
</div>

View File

@@ -10,6 +10,7 @@
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import Ghost from './_Ghost.svelte';
import MinIo from './_MinIO.svelte'; import MinIo from './_MinIO.svelte';
import PlausibleAnalytics from './_PlausibleAnalytics.svelte'; import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
import VsCodeServer from './_VSCodeServer.svelte'; import VsCodeServer from './_VSCodeServer.svelte';
@@ -142,6 +143,8 @@
<VsCodeServer {service} /> <VsCodeServer {service} />
{:else if service.type === 'wordpress'} {:else if service.type === 'wordpress'}
<Wordpress bind:service {isRunning} {readOnly} /> <Wordpress bind:service {isRunning} {readOnly} />
{:else if service.type === 'ghost'}
<Ghost bind:service {readOnly} />
{/if} {/if}
</div> </div>
</form> </form>

View File

@@ -35,6 +35,7 @@
} }
if (service.plausibleAnalytics?.email && service.plausibleAnalytics.username) readOnly = true; if (service.plausibleAnalytics?.email && service.plausibleAnalytics.username) readOnly = true;
if (service.wordpress?.mysqlDatabase) readOnly = true; if (service.wordpress?.mysqlDatabase) readOnly = true;
if (service.ghost?.mariadbDatabase && service.ghost.mariadbDatabase) readOnly = true;
return { return {
props: { props: {

View File

@@ -38,6 +38,9 @@
import { post } from '$lib/api'; import { post } from '$lib/api';
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte'; import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
import N8n from '$lib/components/svg/services/N8n.svelte';
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -77,6 +80,12 @@
<VaultWarden isAbsolute /> <VaultWarden isAbsolute />
{:else if type.name === 'languagetool'} {:else if type.name === 'languagetool'}
<LanguageTool isAbsolute /> <LanguageTool isAbsolute />
{:else if type.name === 'n8n'}
<N8n isAbsolute />
{:else if type.name === 'uptimekuma'}
<UptimeKuma isAbsolute />
{:else if type.name === 'ghost'}
<Ghost isAbsolute />
{/if}{type.fancyName} {/if}{type.fancyName}
</button> </button>
</form> </form>

View File

@@ -0,0 +1,23 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let {
name,
fqdn,
ghost: { mariadbDatabase }
} = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
await db.updateGhostService({ id, fqdn, name, mariadbDatabase });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,133 @@
import {
asyncExecShell,
createDirectories,
getDomain,
getEngine,
getUserDetails
} from '$lib/common';
import * as db from '$lib/database';
import { promises as fs } from 'fs';
import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const {
type,
version,
destinationDockerId,
destinationDocker,
serviceSecret,
fqdn,
ghost: {
defaultEmail,
defaultPassword,
mariadbRootUser,
mariadbRootUserPassword,
mariadbDatabase,
mariadbPassword,
mariadbUser
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const domain = getDomain(fqdn);
const config = {
ghost: {
image: `${image}:${version}`,
volume: `${id}-ghost:/bitnami/ghost`,
environmentVariables: {
GHOST_HOST: domain,
GHOST_EMAIL: defaultEmail,
GHOST_PASSWORD: defaultPassword,
GHOST_DATABASE_HOST: `${id}-mariadb`,
GHOST_DATABASE_USER: mariadbUser,
GHOST_DATABASE_PASSWORD: mariadbPassword,
GHOST_DATABASE_NAME: mariadbDatabase,
GHOST_DATABASE_PORT_NUMBER: 3306
}
},
mariadb: {
image: `bitnami/mariadb:latest`,
volume: `${id}-mariadb:/bitnami/mariadb`,
environmentVariables: {
MARIADB_USER: mariadbUser,
MARIADB_PASSWORD: mariadbPassword,
MARIADB_DATABASE: mariadbDatabase,
MARIADB_ROOT_USER: mariadbRootUser,
MARIADB_ROOT_PASSWORD: mariadbRootUserPassword
}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.ghost.environmentVariables[secret.name] = secret.value;
});
}
const composeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.ghost.image,
networks: [network],
volumes: [config.ghost.volume],
environment: config.ghost.environmentVariables,
restart: 'always',
labels: makeLabelForServices('ghost'),
depends_on: [`${id}-mariadb`]
},
[`${id}-mariadb`]: {
container_name: `${id}-mariadb`,
image: config.mariadb.image,
networks: [network],
volumes: [config.mariadb.volume],
environment: config.mariadb.environmentVariables,
restart: 'always'
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.ghost.volume.split(':')[0]]: {
name: config.ghost.volume.split(':')[0]
},
[config.mariadb.volume.split(':')[0]]: {
name: config.mariadb.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,39 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
let found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
found = await checkContainer(engine, `${id}-mariadb`);
if (found) {
await removeDestinationDocker({ id: `${id}-mariadb`, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -4,7 +4,8 @@ import {
generateDatabaseConfiguration, generateDatabaseConfiguration,
getServiceImage, getServiceImage,
getVersions, getVersions,
ErrorHandler ErrorHandler,
getServiceImages
} from '$lib/database'; } from '$lib/database';
import { dockerInstance } from '$lib/docker'; import { dockerInstance } from '$lib/docker';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
@@ -23,7 +24,13 @@ export const get: RequestHandler = async (event) => {
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const docker = dockerInstance({ destinationDocker }); const docker = dockerInstance({ destinationDocker });
const baseImage = getServiceImage(type); const baseImage = getServiceImage(type);
const images = getServiceImages(type);
docker.engine.pull(`${baseImage}:${version}`); docker.engine.pull(`${baseImage}:${version}`);
if (images?.length > 0) {
for (const image of images) {
docker.engine.pull(`${image}:latest`);
}
}
try { try {
const { stdout } = await asyncExecShell( const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}` `DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`

View File

@@ -39,6 +39,9 @@
import cuid from 'cuid'; import cuid from 'cuid';
import { browser } from '$app/env'; import { browser } from '$app/env';
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
import N8n from '$lib/components/svg/services/N8n.svelte';
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte';
export let service; export let service;
export let isRunning; export let isRunning;
@@ -109,6 +112,18 @@
<a href="https://languagetool.org/dev" target="_blank"> <a href="https://languagetool.org/dev" target="_blank">
<LanguageTool /> <LanguageTool />
</a> </a>
{:else if service.type === 'n8n'}
<a href="https://n8n.io" target="_blank">
<N8n />
</a>
{:else if service.type === 'uptimekuma'}
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
<UptimeKuma />
</a>
{:else if service.type === 'ghost'}
<a href="https://ghost.org" target="_blank">
<Ghost />
</a>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -41,7 +41,7 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
volumes: [`${id}-ngrams:/ngrams`], volumes: [config.volume],
labels: makeLabelForServices('languagetool') labels: makeLabelForServices('languagetool')
} }
}, },
@@ -51,20 +51,20 @@ export const post: RequestHandler = async (event) => {
} }
}, },
volumes: { volumes: {
[`${id}-ngrams`]: { [config.volume.split(':')[0]]: {
external: true name: config.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}-ngrams`);
} catch (error) {
console.log(error);
}
try { try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateNocoDbOrMinioService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -76,19 +76,13 @@ export const post: RequestHandler = async (event) => {
}, },
volumes: { volumes: {
[config.volume.split(':')[0]]: { [config.volume.split(':')[0]]: {
external: true name: config.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}`
);
} catch (error) {
console.log(error);
}
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
await db.updateMinioService({ id, publicPort }); await db.updateMinioService({ id, publicPort });

View File

@@ -4,25 +4,16 @@ import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn, port, buildCommand, startCommand, installCommand } = await event.request.json(); let { name, fqdn } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
if (port) port = Number(port);
try { try {
const { id } = await db.importApplication({ await db.updateService({ id, fqdn, name });
name, return { status: 201 };
teamId,
fqdn,
port,
buildCommand,
startCommand,
installCommand
});
return { status: 201, body: { id } };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);
} }

View File

@@ -0,0 +1,77 @@
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { promises as fs } from 'fs';
import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
image: `${image}:${version}`,
volume: `${id}-n8n:/root/.n8n`,
environmentVariables: {}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.environmentVariables[secret.name] = secret.value;
});
}
const composeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.image,
networks: [network],
volumes: [config.volume],
environment: config.environmentVariables,
restart: 'always',
labels: makeLabelForServices('n8n')
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,35 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -12,7 +12,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateNocoDbOrMinioService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -52,6 +52,11 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -158,29 +158,21 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
}, },
volumes: { volumes: {
[config.postgresql.volume.split(':')[0]]: { [config.postgresql.volume.split(':')[0]]: {
external: true name: config.postgresql.volume.split(':')[0]
}, },
[config.clickhouse.volume.split(':')[0]]: { [config.clickhouse.volume.split(':')[0]]: {
external: true name: config.clickhouse.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { if (version === 'latest') {
await asyncExecShell( await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
`DOCKER_HOST=${host} docker volume create ${config.postgresql.volume.split(':')[0]}`
);
await asyncExecShell(
`DOCKER_HOST=${host} docker volume create ${config.clickhouse.volume.split(':')[0]}`
);
} catch (error) {
console.log(error);
} }
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d` `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d`
); );
return { return {
status: 200 status: 200
}; };

View File

@@ -0,0 +1,20 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
await db.updateService({ id, fqdn, name });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,77 @@
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { promises as fs } from 'fs';
import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
image: `${image}:${version}`,
volume: `${id}-uptimekuma:/app/data`,
environmentVariables: {}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.environmentVariables[secret.name] = secret.value;
});
}
const composeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.image,
networks: [network],
volumes: [config.volume],
environment: config.environmentVariables,
restart: 'always',
labels: makeLabelForServices('uptimekuma')
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,35 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -52,20 +52,18 @@ export const post: RequestHandler = async (event) => {
}, },
volumes: { volumes: {
[config.volume.split(':')[0]]: { [config.volume.split(':')[0]]: {
external: true name: config.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
await asyncExecShell( if (version === 'latest') {
`DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}` await asyncExecShell(
); `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
} catch (error) { );
console.log(error); }
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -61,29 +61,20 @@ export const post: RequestHandler = async (event) => {
}, },
volumes: { volumes: {
[config.volume.split(':')[0]]: { [config.volume.split(':')[0]]: {
external: true name: config.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { if (version === 'latest') {
await asyncExecShell( await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
`DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}`
);
} catch (error) {
console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
} }
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);
} }

View File

@@ -72,6 +72,7 @@ export const post: RequestHandler = async (event) => {
container_name: id, container_name: id,
image: config.wordpress.image, image: config.wordpress.image,
environment: config.wordpress.environmentVariables, environment: config.wordpress.environmentVariables,
volumes: [config.wordpress.volume],
networks: [network], networks: [network],
restart: 'always', restart: 'always',
depends_on: [`${id}-mysql`], depends_on: [`${id}-mysql`],
@@ -80,6 +81,7 @@ export const post: RequestHandler = async (event) => {
[`${id}-mysql`]: { [`${id}-mysql`]: {
container_name: `${id}-mysql`, container_name: `${id}-mysql`,
image: config.mysql.image, image: config.mysql.image,
volumes: [config.mysql.volume],
environment: config.mysql.environmentVariables, environment: config.mysql.environmentVariables,
networks: [network], networks: [network],
restart: 'always' restart: 'always'
@@ -91,29 +93,22 @@ export const post: RequestHandler = async (event) => {
} }
}, },
volumes: { volumes: {
[config.mysql.volume.split(':')[0]]: {
external: true
},
[config.wordpress.volume.split(':')[0]]: { [config.wordpress.volume.split(':')[0]]: {
external: true name: config.wordpress.volume.split(':')[0]
},
[config.mysql.volume.split(':')[0]]: {
name: config.mysql.volume.split(':')[0]
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker volume create ${config.mysql.volume.split(':')[0]}`
);
await asyncExecShell(
`DOCKER_HOST=${host} docker volume create ${config.wordpress.volume.split(':')[0]}`
);
} catch (error) {
console.log(error);
}
try { try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@@ -1,24 +1,3 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const url = `/services.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts"> <script lang="ts">
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte'; import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
import NocoDb from '$lib/components/svg/services/NocoDB.svelte'; import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
@@ -27,13 +6,22 @@
import Wordpress from '$lib/components/svg/services/Wordpress.svelte'; import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte'; import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import N8n from '$lib/components/svg/services/N8n.svelte';
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte';
export let services; export let services;
async function newService() {
const { id } = await post('/services/new', {});
return await goto(`/services/${id}`, { replaceState: true });
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Services</div> <div class="mr-4 text-2xl tracking-tight">Services</div>
<a href="/new/service" class="add-icon bg-pink-600 hover:bg-pink-500"> <div on:click={newService} class="add-icon cursor-pointer bg-pink-600 hover:bg-pink-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -47,7 +35,7 @@
d="M12 6v6m0 0v6m0-6h6m-6 0H6" d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg /></svg
> >
</a> </div>
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
@@ -73,6 +61,12 @@
<VaultWarden isAbsolute /> <VaultWarden isAbsolute />
{:else if service.type === 'languagetool'} {:else if service.type === 'languagetool'}
<LanguageTool isAbsolute /> <LanguageTool isAbsolute />
{:else if service.type === 'n8n'}
<N8n isAbsolute />
{:else if service.type === 'uptimekuma'}
<UptimeKuma isAbsolute />
{:else if service.type === 'ghost'}
<Ghost isAbsolute />
{/if} {/if}
<div class="font-bold text-xl text-center truncate"> <div class="font-bold text-xl text-center truncate">
{service.name} {service.name}

View File

@@ -6,9 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const name = uniqueName();
const { name } = await event.request.json();
try { try {
const { id } = await db.newService({ name, teamId }); const { id } = await db.newService({ name, teamId });
return { status: 201, body: { id } }; return { status: 201, body: { id } };

View File

@@ -91,23 +91,21 @@
</div> </div>
</div> </div>
{/if} {/if}
<div class="mx-auto max-w-2xl"> <div class="flex flex-wrap justify-center">
<div class="flex flex-wrap justify-center"> {#each teams as team}
{#each teams as team} <a href="/teams/{team.teamId}" class="w-96 p-2 no-underline">
<a href="/teams/{team.teamId}" class="w-96 p-2 no-underline"> <div
<div class="box-selection relative"
class="box-selection relative" class:hover:bg-cyan-600={team.team?.id !== '0'}
class:hover:bg-cyan-600={team.team?.id !== '0'} class:hover:bg-red-500={team.team?.id === '0'}
class:hover:bg-red-500={team.team?.id === '0'} >
> <div class="truncate text-center text-xl font-bold">
<div class="truncate text-center text-xl font-bold">{team.team.name}</div> {team.team.name}
<div class="text-center text-xs"> {team.team?.id === '0' ? '(root)' : ''}
({team.team?.id === '0' ? 'root team - ' : ''}{team.permission})
</div>
<div class="mt-1 text-center">{team.team._count.users} member(s)</div>
</div> </div>
</a>
{/each} <div class="mt-1 text-center">{team.team._count.users} member(s)</div>
</div> </div>
</a>
{/each}
</div> </div>

View File

@@ -2,23 +2,19 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* poppins-regular - latin-ext_latin_devanagari */
@font-face { @font-face {
font-family: 'Poppins'; font-family: 'Poppins';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-regular.woff2') format('woff2'), src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-regular.woff2') format('woff2'),
/* Chrome 26+, Opera 23+, Firefox 39+ */ url('/poppins-v19-latin-ext_latin_devanagari-regular.woff') format('woff');
url('/poppins-v19-latin-ext_latin_devanagari-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
} }
/* poppins-500 - latin-ext_latin_devanagari */
@font-face { @font-face {
font-family: 'Poppins'; font-family: 'Poppins';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-500.woff2') format('woff2'), src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-500.woff2') format('woff2'),
/* Chrome 26+, Opera 23+, Firefox 39+ */ url('/poppins-v19-latin-ext_latin_devanagari-500.woff') url('/poppins-v19-latin-ext_latin_devanagari-500.woff') format('woff');
format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
} }
html { html {
@@ -41,8 +37,51 @@ textarea {
@apply min-w-[24rem] rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm; @apply min-w-[24rem] rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm;
} }
#svelte .custom-select-wrapper .selectContainer.disabled input {
@apply placeholder:text-stone-600;
}
#svelte .custom-select-wrapper .selectContainer input {
@apply text-white;
}
#svelte .custom-select-wrapper .selectContainer {
@apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 text-xs font-bold tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm;
}
#svelte .listContainer {
@apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-coollabs scrollbar-track-coolgray-200;
}
#svelte .item.hover {
@apply bg-coolgray-100 text-white;
}
#svelte .item.active {
@apply bg-coollabs text-white;
}
select { select {
@apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm; @apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm;
}
.svelte-select {
--background: rgb(32 32 32);
--inputColor: white;
--multiItemPadding: 0;
--multiSelectPadding: 0 0.5rem 0 0.5rem;
--border: none;
--placeholderColor: rgb(87 83 78);
--listBackground: rgb(32 32 32);
--itemColor: white;
--itemHoverBG: rgb(107 22 237);
--multiItemBG: rgb(32 32 32);
--multiClearHoverBG: transparent;
--multiClearHoverFill: rgb(239 68 68);
--multiItemActiveBG: transparent;
--multiClearBG: transparent;
--clearSelectFocusColor: white;
--clearSelectHoverColor: rgb(239 68 68);
--multiItemBorderRadius: 0.25rem;
--listShadow: none;
} }
label { label {
@@ -69,7 +108,7 @@ a {
} }
.nav-side { .nav-side {
@apply relative right-0 top-0 z-50 m-5 flex flex-wrap items-center justify-end space-x-2 bg-coolblack/40 text-white sm:absolute; @apply absolute right-0 top-0 z-50 m-5 flex flex-wrap items-center justify-end space-x-2 bg-coolblack/40 text-white;
} }
.add-icon { .add-icon {

BIN
static/ghost.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,4 +1,5 @@
{ {
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"moduleResolution": "node", "moduleResolution": "node",
"module": "es2020", "module": "es2020",