From ad696ea54a37d51e64abcca39d22635dc976cc2c Mon Sep 17 00:00:00 2001 From: Samuel Date: Sun, 21 Jul 2024 11:05:44 +0300 Subject: [PATCH 01/61] feat: add jellyfin template --- public/templates/jellyfin.svg | 34 +++++++++++++++++++++++++++ templates/jellyfin/docker-compose.yml | 25 ++++++++++++++++++++ templates/jellyfin/index.ts | 19 +++++++++++++++ templates/templates.ts | 15 ++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 public/templates/jellyfin.svg create mode 100644 templates/jellyfin/docker-compose.yml create mode 100644 templates/jellyfin/index.ts diff --git a/public/templates/jellyfin.svg b/public/templates/jellyfin.svg new file mode 100644 index 00000000..4227a706 --- /dev/null +++ b/public/templates/jellyfin.svg @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/templates/jellyfin/docker-compose.yml b/templates/jellyfin/docker-compose.yml new file mode 100644 index 00000000..b1af0ef7 --- /dev/null +++ b/templates/jellyfin/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3.8' +services: + jellyfin: + image: jellyfin/jellyfin + networks: + - dokploy-network + labels: + - "traefik.enable=true" + - "traefik.http.routers.${HASH}.rule=Host(`${JELLYFIN_HOST}`)" + - "traefik.http.services.${HASH}.loadbalancer.server.port" + volumes: + - ./config:/config + - ./cache:/cache + - ./media:/media + restart: 'unless-stopped' + # Optional - alternative address used for autodiscovery + environment: + - JELLYFIN_PublishedServerUrl=http://${JELLYFIN_HOST} + # Optional - may be necessary for docker healthcheck to pass if running in host network mode + extra_hosts: + - 'host.docker.internal:host-gateway' + +networks: + dokploy-network: + external: true diff --git a/templates/jellyfin/index.ts b/templates/jellyfin/index.ts new file mode 100644 index 00000000..7af9884f --- /dev/null +++ b/templates/jellyfin/index.ts @@ -0,0 +1,19 @@ +// EXAMPLE +import { + generateHash, + generateRandomDomain, + type Template, + type Schema, +} from "../utils"; + +export function generate(schema: Schema): Template { + const randomDomain = generateRandomDomain(schema); + + const envs = [ + `JELLYFIN_HOST=${randomDomain}`, + ]; + + return { + envs, + }; +} diff --git a/templates/templates.ts b/templates/templates.ts index 0063013e..a2327346 100644 --- a/templates/templates.ts +++ b/templates/templates.ts @@ -378,4 +378,19 @@ export const templates: TemplateData[] = [ tags: ["analytics"], load: () => import("./umami/index").then((m) => m.generate), }, + { + id: "jellyfin", + name: "jellyfin", + version: "v10.9.7", + description: + "Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. ", + logo: "jellyfin.svg", + links: { + github: "https://github.com/jellyfin/jellyfin", + website: "https://jellyfin.org/", + docs: "https://jellyfin.org/docs/", + }, + tags: ["media system"], + load: () => import("./jellyfin/index").then((m) => m.generate), + }, ]; From 54aaa511d5672b1f742a89f95b495fb5e8d9fba2 Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 22 Jul 2024 09:20:16 +0300 Subject: [PATCH 02/61] feat: add jellyfin template --- templates/jellyfin/docker-compose.yml | 15 +++++++++------ templates/jellyfin/index.ts | 5 ++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/templates/jellyfin/docker-compose.yml b/templates/jellyfin/docker-compose.yml index b1af0ef7..bdfcf261 100644 --- a/templates/jellyfin/docker-compose.yml +++ b/templates/jellyfin/docker-compose.yml @@ -1,17 +1,17 @@ version: '3.8' services: jellyfin: - image: jellyfin/jellyfin + image: jellyfin/jellyfin:10 networks: - dokploy-network labels: - "traefik.enable=true" - "traefik.http.routers.${HASH}.rule=Host(`${JELLYFIN_HOST}`)" - - "traefik.http.services.${HASH}.loadbalancer.server.port" + - "traefik.http.services.${HASH}.loadbalancer.server.port=${JELLYFIN_PORT}" volumes: - - ./config:/config - - ./cache:/cache - - ./media:/media + - config:/config + - cache:/cache + - media:/media restart: 'unless-stopped' # Optional - alternative address used for autodiscovery environment: @@ -19,7 +19,10 @@ services: # Optional - may be necessary for docker healthcheck to pass if running in host network mode extra_hosts: - 'host.docker.internal:host-gateway' - +volumes: + config: + cache: + media: networks: dokploy-network: external: true diff --git a/templates/jellyfin/index.ts b/templates/jellyfin/index.ts index 7af9884f..61bd0f08 100644 --- a/templates/jellyfin/index.ts +++ b/templates/jellyfin/index.ts @@ -7,10 +7,13 @@ import { } from "../utils"; export function generate(schema: Schema): Template { + const mainServiceHash = generateHash(schema.projectName); const randomDomain = generateRandomDomain(schema); - + const port = 8096; const envs = [ `JELLYFIN_HOST=${randomDomain}`, + `HASH=${mainServiceHash}`, + `JELLYFIN_PORT=${port}` ]; return { From 7240ff38f132b8b6d341b3a1206314cd57b0261a Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 09:58:49 +0200 Subject: [PATCH 03/61] build: pkg push command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3d7359be..02626017 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "migration:run": "tsx -r dotenv/config migration.ts", "migration:up": "drizzle-kit up --config ./server/db/drizzle.config.ts", "migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts", - "db:push": "drizzle-kit --config ./server/db/drizzle.config.ts", + "db:push": "drizzle-kit push --config ./server/db/drizzle.config.ts", "db:truncate": "tsx -r dotenv/config ./server/db/reset.ts", "db:studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", "check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true", From 655e29d96bebb653aeaf6adbb11086dedeb80d5b Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 09:59:12 +0200 Subject: [PATCH 04/61] feat: buildargs column --- drizzle/0025_lying_mephisto.sql | 1 + drizzle/meta/0025_snapshot.json | 2938 +++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + server/db/schema/application.ts | 1 + 4 files changed, 2947 insertions(+) create mode 100644 drizzle/0025_lying_mephisto.sql create mode 100644 drizzle/meta/0025_snapshot.json diff --git a/drizzle/0025_lying_mephisto.sql b/drizzle/0025_lying_mephisto.sql new file mode 100644 index 00000000..25627413 --- /dev/null +++ b/drizzle/0025_lying_mephisto.sql @@ -0,0 +1 @@ +ALTER TABLE "application" ADD COLUMN "buildArgs" text; \ No newline at end of file diff --git a/drizzle/meta/0025_snapshot.json b/drizzle/meta/0025_snapshot.json new file mode 100644 index 00000000..15ba3294 --- /dev/null +++ b/drizzle/meta/0025_snapshot.json @@ -0,0 +1,2938 @@ +{ + "id": "771c1412-7931-4f79-a789-055b0f51cf0f", + "prevId": "2c69b31b-ecee-4e20-9959-6dc0c76656fb", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKey": { + "name": "customGitSSHKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_projectId_project_projectId_fk": { + "name": "application_projectId_project_projectId_fk", + "tableFrom": "application", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_projectId_project_projectId_fk": { + "name": "postgres_projectId_project_projectId_fk", + "tableFrom": "postgres", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authId": { + "name": "authId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_adminId_admin_adminId_fk": { + "name": "user_adminId_admin_adminId_fk", + "tableFrom": "user", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_authId_auth_id_fk": { + "name": "user_authId_auth_id_fk", + "tableFrom": "user", + "tableTo": "auth", + "columnsFrom": [ + "authId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.admin": { + "name": "admin", + "schema": "", + "columns": { + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "authId": { + "name": "authId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "admin_authId_auth_id_fk": { + "name": "admin_authId_auth_id_fk", + "tableFrom": "admin", + "tableTo": "auth", + "columnsFrom": [ + "authId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.auth": { + "name": "auth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rol": { + "name": "rol", + "type": "Roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_email_unique": { + "name": "auth_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_adminId_admin_adminId_fk": { + "name": "project_adminId_admin_adminId_fk", + "tableFrom": "project", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 80 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + } + }, + "indexes": {}, + "foreignKeys": { + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_projectId_project_projectId_fk": { + "name": "mariadb_projectId_project_projectId_fk", + "tableFrom": "mariadb", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_projectId_project_projectId_fk": { + "name": "mongo_projectId_project_projectId_fk", + "tableFrom": "mongo", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_projectId_project_projectId_fk": { + "name": "mysql_projectId_project_projectId_fk", + "tableFrom": "mysql", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "destination_adminId_admin_adminId_fk": { + "name": "destination_adminId_admin_adminId_fk", + "tableFrom": "destination", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + } + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_auth_id_fk": { + "name": "session_user_id_auth_id_fk", + "tableFrom": "session", + "tableTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + } + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redis_projectId_project_projectId_fk": { + "name": "redis_projectId_project_projectId_fk", + "tableFrom": "redis", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKey": { + "name": "customGitSSHKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "compose_projectId_project_projectId_fk": { + "name": "compose_projectId_project_projectId_fk", + "tableFrom": "compose", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_adminId_admin_adminId_fk": { + "name": "registry_adminId_admin_adminId_fk", + "tableFrom": "registry", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "drop" + ] + }, + "public.Roles": { + "name": "Roles", + "schema": "public", + "values": [ + "admin", + "user" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "raw" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 14fa2400..b024ee01 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -176,6 +176,13 @@ "when": 1721603595092, "tag": "0024_dapper_supernaut", "breakpoints": true + }, + { + "idx": 25, + "version": "6", + "when": 1721633853118, + "tag": "0025_lying_mephisto", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/db/schema/application.ts b/server/db/schema/application.ts index 8b3c4b11..5f96b407 100644 --- a/server/db/schema/application.ts +++ b/server/db/schema/application.ts @@ -107,6 +107,7 @@ export const applications = pgTable("application", { .unique(), description: text("description"), env: text("env"), + buildArgs: text("buildArgs"), memoryReservation: integer("memoryReservation"), memoryLimit: integer("memoryLimit"), cpuReservation: integer("cpuReservation"), From e00cbaeb8ac536bd660b824eafdfc37b55391c8e Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 09:59:53 +0200 Subject: [PATCH 05/61] build: ignore vscode --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4e788d91..7acbe51a 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ yarn-error.log* # otros /.data /.main +.vscode *.lockb *.rdb From b3b74396175cab6567bc7d950e7138030873ae41 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 11:17:59 +0200 Subject: [PATCH 06/61] feat: add build-time variables form --- .../application/environment/show.tsx | 183 ++++++------------ components/ui/secrets.tsx | 92 +++++++++ server/api/routers/application.ts | 9 + server/db/schema/application.ts | 7 + 4 files changed, 170 insertions(+), 121 deletions(-) create mode 100644 components/ui/secrets.tsx diff --git a/components/dashboard/application/environment/show.tsx b/components/dashboard/application/environment/show.tsx index 359142c5..fe5853ce 100644 --- a/components/dashboard/application/environment/show.tsx +++ b/components/dashboard/application/environment/show.tsx @@ -1,42 +1,30 @@ -import { CodeEditor } from "@/components/shared/code-editor"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/components/ui/form"; -import { Toggle } from "@/components/ui/toggle"; +import { Form } from "@/components/ui/form"; +import { Secrets } from "@/components/ui/secrets"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { EyeIcon, EyeOffIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; const addEnvironmentSchema = z.object({ - environment: z.string(), + env: z.string(), }); type EnvironmentSchema = z.infer; +const addBuildArgsSchema = z.object({ + buildArgs: z.string(), +}); + +type BuildArgsSchema = z.infer; + interface Props { applicationId: string; } export const ShowEnvironment = ({ applicationId }: Props) => { - const [isEnvVisible, setIsEnvVisible] = useState(true); - const { mutateAsync, isLoading } = - api.application.saveEnvironment.useMutation(); + const saveEnvironmentMutation = api.application.saveEnvironment.useMutation(); + const saveBuildArgsMutation = api.application.saveBuildArgs.useMutation(); const { data, refetch } = api.application.one.useQuery( { @@ -46,26 +34,27 @@ export const ShowEnvironment = ({ applicationId }: Props) => { enabled: !!applicationId, }, ); - const form = useForm({ + + const envForm = useForm({ defaultValues: { - environment: "", + env: data?.env || "", }, resolver: zodResolver(addEnvironmentSchema), }); - useEffect(() => { - if (data) { - form.reset({ - environment: data.env || "", - }); - } - }, [form.reset, data, form]); + const buildArgsForm = useForm({ + defaultValues: { + buildArgs: data?.buildArgs || "", + }, + resolver: zodResolver(addBuildArgsSchema), + }); - const onSubmit = async (data: EnvironmentSchema) => { - mutateAsync({ - env: data.environment, - applicationId, - }) + const onEnvSubmit = async (data: EnvironmentSchema) => { + saveEnvironmentMutation + .mutateAsync({ + env: data.env, + applicationId, + }) .then(async () => { toast.success("Environments Added"); await refetch(); @@ -74,94 +63,46 @@ export const ShowEnvironment = ({ applicationId }: Props) => { toast.error("Error to add environment"); }); }; - useEffect(() => { - if (isEnvVisible) { - if (data?.env) { - const maskedLines = data.env - .split("\n") - .map((line) => "*".repeat(line.length)) - .join("\n"); - form.reset({ - environment: maskedLines, - }); - } else { - form.reset({ - environment: "", - }); - } - } else { - form.reset({ - environment: data?.env || "", + + const onBuildArgsSubmit = async (data: BuildArgsSchema) => { + saveBuildArgsMutation + .mutateAsync({ + buildArgs: data.buildArgs, + applicationId, + }) + .then(async () => { + toast.success("Buildargs Added"); + await refetch(); + }) + .catch(() => { + toast.error("Error to add build-args"); }); - } - }, [form.reset, data, form, isEnvVisible]); + }; return (
- - -
- Environment Settings - - You can add environment variables to your resource. - -
- - - {isEnvVisible ? ( - - ) : ( - - )} - -
- -
- - ( - - - - - - - - )} - /> - -
- -
- - -
-
+
+ + + + +
+ + + +
); }; diff --git a/components/ui/secrets.tsx b/components/ui/secrets.tsx new file mode 100644 index 00000000..acb804e8 --- /dev/null +++ b/components/ui/secrets.tsx @@ -0,0 +1,92 @@ +import { CodeEditor } from "@/components/shared/code-editor"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { Toggle } from "@/components/ui/toggle"; +import { EyeIcon, EyeOffIcon } from "lucide-react"; +import { type CSSProperties, useEffect, useState } from "react"; +import { useFormContext } from "react-hook-form"; + +interface Props { + name: string; + title: string; + description: string; + placeholder: string; + isLoading: boolean; +} + +export const Secrets = (props: Props) => { + const [isVisible, setIsVisible] = useState(true); + const form = useFormContext>(); + + return ( + + +
+ {props.title} + {props.description} +
+ + + {isVisible ? ( + + ) : ( + + )} + +
+ + ( + + + + + + + + )} + /> + +
+ +
+
+
+ ); +}; diff --git a/server/api/routers/application.ts b/server/api/routers/application.ts index cb325e5a..6fd5e2eb 100644 --- a/server/api/routers/application.ts +++ b/server/api/routers/application.ts @@ -9,6 +9,7 @@ import { apiFindMonitoringStats, apiFindOneApplication, apiReloadApplication, + apiSaveBuildArgs, apiSaveBuildType, apiSaveDockerProvider, apiSaveEnvironmentVariables, @@ -190,6 +191,14 @@ export const applicationRouter = createTRPCRouter({ }); return true; }), + saveBuildArgs: protectedProcedure + .input(apiSaveBuildArgs) + .mutation(async ({ input }) => { + await updateApplication(input.applicationId, { + buildArgs: input.buildArgs, + }); + return true; + }), saveBuildType: protectedProcedure .input(apiSaveBuildType) .mutation(async ({ input }) => { diff --git a/server/db/schema/application.ts b/server/db/schema/application.ts index 5f96b407..607c5275 100644 --- a/server/db/schema/application.ts +++ b/server/db/schema/application.ts @@ -379,6 +379,13 @@ export const apiSaveEnvironmentVariables = createSchema }) .required(); +export const apiSaveBuildArgs = createSchema + .pick({ + applicationId: true, + buildArgs: true, + }) + .required(); + export const apiFindMonitoringStats = createSchema .pick({ appName: true, From a15eb3b229fbc2268df158b2ce73dd1c6bf054a9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 22 Jul 2024 03:45:45 -0600 Subject: [PATCH 07/61] refactor: update jobs --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0d93e547..5ac4ef1c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,7 +46,7 @@ jobs: combine-manifests: docker: - - image: cimg/base:stable + - image: cimg/node:18.18.0 steps: - checkout - setup_remote_docker From 3601abc4c128efdaa2ca550797642696bdc58d5d Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 11:52:20 +0200 Subject: [PATCH 08/61] feat: apply buildargs --- server/utils/builders/docker-file.ts | 7 +++---- server/utils/builders/utils.ts | 12 ------------ server/utils/docker/utils.ts | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 16 deletions(-) delete mode 100644 server/utils/builders/utils.ts diff --git a/server/utils/builders/docker-file.ts b/server/utils/builders/docker-file.ts index 62562085..940a623a 100644 --- a/server/utils/builders/docker-file.ts +++ b/server/utils/builders/docker-file.ts @@ -1,15 +1,15 @@ import type { WriteStream } from "node:fs"; import { docker } from "@/server/constants"; +import { prepareBuildArgs } from "@/server/utils/docker/utils"; import * as tar from "tar-fs"; import type { ApplicationNested } from "."; import { getBuildAppDirectory } from "../filesystem/directory"; -import { createEnvFile } from "./utils"; export const buildCustomDocker = async ( application: ApplicationNested, writeStream: WriteStream, ) => { - const { appName, env } = application; + const { appName, buildArgs } = application; const dockerFilePath = getBuildAppDirectory(application); try { const image = `${appName}`; @@ -17,10 +17,9 @@ export const buildCustomDocker = async ( dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; const tarStream = tar.pack(contextPath); - createEnvFile(dockerFilePath, env); - const stream = await docker.buildImage(tarStream, { t: image, + buildargs: prepareBuildArgs(buildArgs), dockerfile: dockerFilePath.substring(dockerFilePath.lastIndexOf("/") + 1), }); diff --git a/server/utils/builders/utils.ts b/server/utils/builders/utils.ts deleted file mode 100644 index a5ce5362..00000000 --- a/server/utils/builders/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { prepareEnvironmentVariables } from "../docker/utils"; - -export const createEnvFile = (directory: string, env: string | null) => { - const envFilePath = join(dirname(directory), ".env"); - if (!existsSync(dirname(envFilePath))) { - mkdirSync(dirname(envFilePath), { recursive: true }); - } - const envFileContent = prepareEnvironmentVariables(env).join("\n"); - writeFileSync(envFilePath, envFileContent); -}; diff --git a/server/utils/docker/utils.ts b/server/utils/docker/utils.ts index 256a1f96..b1eb8973 100644 --- a/server/utils/docker/utils.ts +++ b/server/utils/docker/utils.ts @@ -161,6 +161,21 @@ export const removeService = async (appName: string) => { export const prepareEnvironmentVariables = (env: string | null) => Object.entries(parse(env ?? "")).map(([key, value]) => `${key}=${value}`); +export const prepareBuildArgs = (input: string | null) => { + const pairs = (input ?? "").split(" "); + + const jsonObject: Record = {}; + + for (const pair of pairs) { + const [key, value] = pair.split("="); + if (key && value) { + jsonObject[key] = value; + } + } + + return jsonObject; +}; + export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => { if (!mounts || mounts.length === 0) { return []; From ff47a157c77c31be5f52d0122ce8ec84532257fe Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 12:14:07 +0200 Subject: [PATCH 09/61] fix: split by new line char --- server/utils/docker/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/docker/utils.ts b/server/utils/docker/utils.ts index b1eb8973..84628e2c 100644 --- a/server/utils/docker/utils.ts +++ b/server/utils/docker/utils.ts @@ -162,7 +162,7 @@ export const prepareEnvironmentVariables = (env: string | null) => Object.entries(parse(env ?? "")).map(([key, value]) => `${key}=${value}`); export const prepareBuildArgs = (input: string | null) => { - const pairs = (input ?? "").split(" "); + const pairs = (input ?? "").split("\n"); const jsonObject: Record = {}; From 6780fa96882160d765f56528e713e861a8b6c907 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 12:21:51 +0200 Subject: [PATCH 10/61] feat: add doc link --- .../dashboard/application/environment/show.tsx | 15 ++++++++++++++- components/ui/secrets.tsx | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/components/dashboard/application/environment/show.tsx b/components/dashboard/application/environment/show.tsx index fe5853ce..9f4de047 100644 --- a/components/dashboard/application/environment/show.tsx +++ b/components/dashboard/application/environment/show.tsx @@ -98,7 +98,20 @@ export const ShowEnvironment = ({ applicationId }: Props) => { name="buildArgs" isLoading={saveBuildArgsMutation.isLoading} title="Build-time Variables" - description="Available only at build-time. See documentation here." + description={ + + Available only at build-time. See documentation  + + here + + . + + } placeholder="NPM_TOKEN=xyz" /> diff --git a/components/ui/secrets.tsx b/components/ui/secrets.tsx index acb804e8..2ad5e020 100644 --- a/components/ui/secrets.tsx +++ b/components/ui/secrets.tsx @@ -15,13 +15,13 @@ import { } from "@/components/ui/form"; import { Toggle } from "@/components/ui/toggle"; import { EyeIcon, EyeOffIcon } from "lucide-react"; -import { type CSSProperties, useEffect, useState } from "react"; +import { type CSSProperties, type ReactNode, useState } from "react"; import { useFormContext } from "react-hook-form"; interface Props { name: string; title: string; - description: string; + description: ReactNode; placeholder: string; isLoading: boolean; } From 59b072e7e06d26ed82234e92cfc302b71a4070b9 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 12:24:12 +0200 Subject: [PATCH 11/61] feat: scope to dockerfile buildtype --- .../application/environment/show.tsx | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/components/dashboard/application/environment/show.tsx b/components/dashboard/application/environment/show.tsx index 9f4de047..e1873458 100644 --- a/components/dashboard/application/environment/show.tsx +++ b/components/dashboard/application/environment/show.tsx @@ -92,30 +92,32 @@ export const ShowEnvironment = ({ applicationId }: Props) => { /> -
- - - Available only at build-time. See documentation  - - here - - . - - } - placeholder="NPM_TOKEN=xyz" - /> - - + {data?.buildType === "dockerfile" && ( +
+ + + Available only at build-time. See documentation  + + here + + . + + } + placeholder="NPM_TOKEN=xyz" + /> + + + )} ); }; From 7c981b2aac1072276898dab224887d3d01993239 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 17:33:42 +0200 Subject: [PATCH 12/61] fix: compose containers --- components/dashboard/compose/logs/show.tsx | 4 +++- server/api/routers/docker.ts | 5 ++++- server/api/services/docker.ts | 12 ++++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/components/dashboard/compose/logs/show.tsx b/components/dashboard/compose/logs/show.tsx index 546b7cc7..19a7c4e6 100644 --- a/components/dashboard/compose/logs/show.tsx +++ b/components/dashboard/compose/logs/show.tsx @@ -30,12 +30,14 @@ export const DockerLogs = dynamic( interface Props { appName: string; + appType: "stack" | "docker-compose"; } -export const ShowDockerLogsCompose = ({ appName }: Props) => { +export const ShowDockerLogsCompose = ({ appName, appType }: Props) => { const { data } = api.docker.getContainersByAppNameMatch.useQuery( { appName, + appType, }, { enabled: !!appName, diff --git a/server/api/routers/docker.ts b/server/api/routers/docker.ts index c8bb9517..208ba6d1 100644 --- a/server/api/routers/docker.ts +++ b/server/api/routers/docker.ts @@ -25,11 +25,14 @@ export const dockerRouter = createTRPCRouter({ getContainersByAppNameMatch: protectedProcedure .input( z.object({ + appType: z + .union([z.literal("stack"), z.literal("docker-compose")]) + .optional(), appName: z.string().min(1), }), ) .query(async ({ input }) => { - return await getContainersByAppNameMatch(input.appName); + return await getContainersByAppNameMatch(input.appName, input.appType); }), getContainersByAppLabel: protectedProcedure diff --git a/server/api/services/docker.ts b/server/api/services/docker.ts index 8c951e8d..927c94f6 100644 --- a/server/api/services/docker.ts +++ b/server/api/services/docker.ts @@ -66,10 +66,18 @@ export const getConfig = async (containerId: string) => { } catch (error) {} }; -export const getContainersByAppNameMatch = async (appName: string) => { +export const getContainersByAppNameMatch = async ( + appName: string, + appType?: "stack" | "docker-compose", +) => { try { + const cmd = + "docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'"; + const { stdout, stderr } = await execAsync( - `docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}' | grep ${appName}`, + appType === "docker-compose" + ? `${cmd} --filter='label=com.docker.compose.project=${appName}'` + : `${cmd} | grep ${appName}`, ); if (stderr) { From b85639f98efcc88049e664c9d6afecc36d88822c Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 17:42:31 +0200 Subject: [PATCH 13/61] fix: add apptype param --- .../project/[projectId]/services/compose/[composeId].tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index dc32325e..22ef7505 100644 --- a/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -156,7 +156,10 @@ const Service = (
- +
From 94bcea36c6af834f83782df6e868fdb8ce26cc47 Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 22 Jul 2024 20:34:26 +0300 Subject: [PATCH 14/61] feat: add jellyfin template --- templates/jellyfin/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/jellyfin/docker-compose.yml b/templates/jellyfin/docker-compose.yml index bdfcf261..16ae6be9 100644 --- a/templates/jellyfin/docker-compose.yml +++ b/templates/jellyfin/docker-compose.yml @@ -4,6 +4,8 @@ services: image: jellyfin/jellyfin:10 networks: - dokploy-network + ports: + - ${JELLYFIN_PORT} labels: - "traefik.enable=true" - "traefik.http.routers.${HASH}.rule=Host(`${JELLYFIN_HOST}`)" From c13eb65b5a424968d3a4b3960278fab8f28ebb72 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 22 Jul 2024 12:58:14 -0600 Subject: [PATCH 15/61] refactor: format --- templates/jellyfin/index.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/templates/jellyfin/index.ts b/templates/jellyfin/index.ts index 61bd0f08..dc33b121 100644 --- a/templates/jellyfin/index.ts +++ b/templates/jellyfin/index.ts @@ -1,22 +1,22 @@ // EXAMPLE import { - generateHash, - generateRandomDomain, - type Template, - type Schema, + type Schema, + type Template, + generateHash, + generateRandomDomain, } from "../utils"; export function generate(schema: Schema): Template { - const mainServiceHash = generateHash(schema.projectName); - const randomDomain = generateRandomDomain(schema); - const port = 8096; - const envs = [ - `JELLYFIN_HOST=${randomDomain}`, - `HASH=${mainServiceHash}`, - `JELLYFIN_PORT=${port}` - ]; + const mainServiceHash = generateHash(schema.projectName); + const randomDomain = generateRandomDomain(schema); + const port = 8096; + const envs = [ + `JELLYFIN_HOST=${randomDomain}`, + `HASH=${mainServiceHash}`, + `JELLYFIN_PORT=${port}`, + ]; - return { - envs, - }; + return { + envs, + }; } From 6d71eac2219efb92ce9482ed69d89740976e425d Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 21:42:07 +0200 Subject: [PATCH 16/61] revert: env file --- server/utils/builders/docker-file.ts | 5 ++++- server/utils/builders/utils.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 server/utils/builders/utils.ts diff --git a/server/utils/builders/docker-file.ts b/server/utils/builders/docker-file.ts index 940a623a..d8ead600 100644 --- a/server/utils/builders/docker-file.ts +++ b/server/utils/builders/docker-file.ts @@ -4,12 +4,13 @@ import { prepareBuildArgs } from "@/server/utils/docker/utils"; import * as tar from "tar-fs"; import type { ApplicationNested } from "."; import { getBuildAppDirectory } from "../filesystem/directory"; +import { createEnvFile } from "./utils"; export const buildCustomDocker = async ( application: ApplicationNested, writeStream: WriteStream, ) => { - const { appName, buildArgs } = application; + const { appName, env, buildArgs } = application; const dockerFilePath = getBuildAppDirectory(application); try { const image = `${appName}`; @@ -17,6 +18,8 @@ export const buildCustomDocker = async ( dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; const tarStream = tar.pack(contextPath); + createEnvFile(dockerFilePath, env); + const stream = await docker.buildImage(tarStream, { t: image, buildargs: prepareBuildArgs(buildArgs), diff --git a/server/utils/builders/utils.ts b/server/utils/builders/utils.ts new file mode 100644 index 00000000..a5ce5362 --- /dev/null +++ b/server/utils/builders/utils.ts @@ -0,0 +1,12 @@ +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { prepareEnvironmentVariables } from "../docker/utils"; + +export const createEnvFile = (directory: string, env: string | null) => { + const envFilePath = join(dirname(directory), ".env"); + if (!existsSync(dirname(envFilePath))) { + mkdirSync(dirname(envFilePath), { recursive: true }); + } + const envFileContent = prepareEnvironmentVariables(env).join("\n"); + writeFileSync(envFilePath, envFileContent); +}; From 8b7df6ce169bc5acdb9c1a8e4a3137cf092af9ab Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 22:17:24 +0200 Subject: [PATCH 17/61] refactor: single form --- .../application/environment/show.tsx | 70 +++++++------------ components/ui/secrets.tsx | 16 +---- server/api/routers/application.ts | 8 --- server/db/schema/application.ts | 7 +- 4 files changed, 30 insertions(+), 71 deletions(-) diff --git a/components/dashboard/application/environment/show.tsx b/components/dashboard/application/environment/show.tsx index e1873458..fb344d78 100644 --- a/components/dashboard/application/environment/show.tsx +++ b/components/dashboard/application/environment/show.tsx @@ -1,3 +1,5 @@ +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; import { Form } from "@/components/ui/form"; import { Secrets } from "@/components/ui/secrets"; import { api } from "@/utils/api"; @@ -8,15 +10,10 @@ import { z } from "zod"; const addEnvironmentSchema = z.object({ env: z.string(), -}); - -type EnvironmentSchema = z.infer; - -const addBuildArgsSchema = z.object({ buildArgs: z.string(), }); -type BuildArgsSchema = z.infer; +type EnvironmentSchema = z.infer; interface Props { applicationId: string; @@ -24,7 +21,6 @@ interface Props { export const ShowEnvironment = ({ applicationId }: Props) => { const saveEnvironmentMutation = api.application.saveEnvironment.useMutation(); - const saveBuildArgsMutation = api.application.saveBuildArgs.useMutation(); const { data, refetch } = api.application.one.useQuery( { @@ -38,21 +34,16 @@ export const ShowEnvironment = ({ applicationId }: Props) => { const envForm = useForm({ defaultValues: { env: data?.env || "", - }, - resolver: zodResolver(addEnvironmentSchema), - }); - - const buildArgsForm = useForm({ - defaultValues: { buildArgs: data?.buildArgs || "", }, - resolver: zodResolver(addBuildArgsSchema), + resolver: zodResolver(addEnvironmentSchema), }); const onEnvSubmit = async (data: EnvironmentSchema) => { saveEnvironmentMutation .mutateAsync({ env: data.env, + buildArgs: data.buildArgs, applicationId, }) .then(async () => { @@ -64,40 +55,22 @@ export const ShowEnvironment = ({ applicationId }: Props) => { }); }; - const onBuildArgsSubmit = async (data: BuildArgsSchema) => { - saveBuildArgsMutation - .mutateAsync({ - buildArgs: data.buildArgs, - applicationId, - }) - .then(async () => { - toast.success("Buildargs Added"); - await refetch(); - }) - .catch(() => { - toast.error("Error to add build-args"); - }); - }; - return ( -
-
- + + + - - - {data?.buildType === "dockerfile" && ( -
- + {data?.buildType === "dockerfile" && ( @@ -115,9 +88,20 @@ export const ShowEnvironment = ({ applicationId }: Props) => { } placeholder="NPM_TOKEN=xyz" /> - - - )} -
+ )} + +
+ +
+
+ + + ); }; diff --git a/components/ui/secrets.tsx b/components/ui/secrets.tsx index 2ad5e020..50fc22ac 100644 --- a/components/ui/secrets.tsx +++ b/components/ui/secrets.tsx @@ -23,7 +23,6 @@ interface Props { title: string; description: ReactNode; placeholder: string; - isLoading: boolean; } export const Secrets = (props: Props) => { @@ -31,7 +30,7 @@ export const Secrets = (props: Props) => { const form = useFormContext>(); return ( - + <>
{props.title} @@ -75,18 +74,7 @@ export const Secrets = (props: Props) => { )} /> - -
- -
- + ); }; diff --git a/server/api/routers/application.ts b/server/api/routers/application.ts index 6fd5e2eb..4ad7086a 100644 --- a/server/api/routers/application.ts +++ b/server/api/routers/application.ts @@ -9,7 +9,6 @@ import { apiFindMonitoringStats, apiFindOneApplication, apiReloadApplication, - apiSaveBuildArgs, apiSaveBuildType, apiSaveDockerProvider, apiSaveEnvironmentVariables, @@ -188,13 +187,6 @@ export const applicationRouter = createTRPCRouter({ .mutation(async ({ input }) => { await updateApplication(input.applicationId, { env: input.env, - }); - return true; - }), - saveBuildArgs: protectedProcedure - .input(apiSaveBuildArgs) - .mutation(async ({ input }) => { - await updateApplication(input.applicationId, { buildArgs: input.buildArgs, }); return true; diff --git a/server/db/schema/application.ts b/server/db/schema/application.ts index 607c5275..af8f6016 100644 --- a/server/db/schema/application.ts +++ b/server/db/schema/application.ts @@ -276,6 +276,7 @@ const createSchema = createInsertSchema(applications, { applicationId: z.string(), autoDeploy: z.boolean(), env: z.string().optional(), + buildArgs: z.string().optional(), name: z.string().min(1), description: z.string().optional(), memoryReservation: z.number().optional(), @@ -376,12 +377,6 @@ export const apiSaveEnvironmentVariables = createSchema .pick({ applicationId: true, env: true, - }) - .required(); - -export const apiSaveBuildArgs = createSchema - .pick({ - applicationId: true, buildArgs: true, }) .required(); From 8230c1ba913272f8c677d284faae84109c41fffd Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 22 Jul 2024 22:43:47 +0200 Subject: [PATCH 18/61] fix: docker compose monitoring --- components/dashboard/compose/monitoring/show.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/dashboard/compose/monitoring/show.tsx b/components/dashboard/compose/monitoring/show.tsx index 0bb959c1..92c31482 100644 --- a/components/dashboard/compose/monitoring/show.tsx +++ b/components/dashboard/compose/monitoring/show.tsx @@ -31,6 +31,7 @@ export const ShowMonitoringCompose = ({ const { data } = api.docker.getContainersByAppNameMatch.useQuery( { appName: appName, + appType, }, { enabled: !!appName, From cd051b72fcb8b6876b56a92153b95b34c2152b87 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:12:13 -0600 Subject: [PATCH 19/61] refactor: remove imports --- .../application/environment/show.tsx | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/components/dashboard/application/environment/show.tsx b/components/dashboard/application/environment/show.tsx index fb344d78..6a8ac57a 100644 --- a/components/dashboard/application/environment/show.tsx +++ b/components/dashboard/application/environment/show.tsx @@ -20,7 +20,8 @@ interface Props { } export const ShowEnvironment = ({ applicationId }: Props) => { - const saveEnvironmentMutation = api.application.saveEnvironment.useMutation(); + const { mutateAsync, isLoading } = + api.application.saveEnvironment.useMutation(); const { data, refetch } = api.application.one.useQuery( { @@ -40,12 +41,11 @@ export const ShowEnvironment = ({ applicationId }: Props) => { }); const onEnvSubmit = async (data: EnvironmentSchema) => { - saveEnvironmentMutation - .mutateAsync({ - env: data.env, - buildArgs: data.buildArgs, - applicationId, - }) + mutateAsync({ + env: data.env, + buildArgs: data.buildArgs, + applicationId, + }) .then(async () => { toast.success("Environments Added"); await refetch(); @@ -91,11 +91,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => { )}
-
From 82721251cc9da397330d773515c09094b411e4eb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:12:26 -0600 Subject: [PATCH 20/61] refactor: remove secrets --- components/ui/secrets.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/ui/secrets.tsx b/components/ui/secrets.tsx index 50fc22ac..a596ef1b 100644 --- a/components/ui/secrets.tsx +++ b/components/ui/secrets.tsx @@ -1,7 +1,5 @@ import { CodeEditor } from "@/components/shared/code-editor"; -import { Button } from "@/components/ui/button"; import { - Card, CardContent, CardDescription, CardHeader, From 5b1ca4eafcfc61a429515235b841d84e815031b7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:13:57 -0600 Subject: [PATCH 21/61] refactor: rename form --- components/dashboard/application/environment/show.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/dashboard/application/environment/show.tsx b/components/dashboard/application/environment/show.tsx index 6a8ac57a..9b38f1ce 100644 --- a/components/dashboard/application/environment/show.tsx +++ b/components/dashboard/application/environment/show.tsx @@ -32,7 +32,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => { }, ); - const envForm = useForm({ + const form = useForm({ defaultValues: { env: data?.env || "", buildArgs: data?.buildArgs || "", @@ -40,7 +40,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => { resolver: zodResolver(addEnvironmentSchema), }); - const onEnvSubmit = async (data: EnvironmentSchema) => { + const onSubmit = async (data: EnvironmentSchema) => { mutateAsync({ env: data.env, buildArgs: data.buildArgs, @@ -56,9 +56,9 @@ export const ShowEnvironment = ({ applicationId }: Props) => { }; return ( -
+ From a4bf48fa68c07621d4d8d28163f7e16202eff0e8 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Tue, 23 Jul 2024 14:34:46 +0200 Subject: [PATCH 22/61] fix: update redirect --- server/utils/traefik/redirect.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/utils/traefik/redirect.ts b/server/utils/traefik/redirect.ts index 9606730d..6e0922a6 100644 --- a/server/utils/traefik/redirect.ts +++ b/server/utils/traefik/redirect.ts @@ -10,10 +10,9 @@ import { export const updateRedirectMiddleware = (appName: string, data: Redirect) => { const config = loadMiddlewares(); + const middlewareName = `redirect-${appName}-${data.uniqueConfigKey}`; - if (config?.http?.middlewares?.[appName]) { - const middlewareName = `${appName}-${data.uniqueConfigKey}`; - + if (config?.http?.middlewares?.[middlewareName]) { config.http.middlewares[middlewareName] = { redirectRegex: { regex: data.regex, From 75bd10cbd5a799a977365462fcce4560b28788c3 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Tue, 23 Jul 2024 20:55:35 +0200 Subject: [PATCH 23/61] fix: http to https --- __test__/traefik/traefik.test.ts | 187 +++++++++++++++++++++++++++++++ server/utils/traefik/domain.ts | 68 +++++++---- 2 files changed, 234 insertions(+), 21 deletions(-) create mode 100644 __test__/traefik/traefik.test.ts diff --git a/__test__/traefik/traefik.test.ts b/__test__/traefik/traefik.test.ts new file mode 100644 index 00000000..797074ea --- /dev/null +++ b/__test__/traefik/traefik.test.ts @@ -0,0 +1,187 @@ +import type { Domain } from "@/server/api/services/domain"; +import type { Redirect } from "@/server/api/services/redirect"; +import type { ApplicationNested } from "@/server/utils/builders"; +import { createRouterConfig } from "@/server/utils/traefik/domain"; +import { expect, test } from "vitest"; + +const baseApp: ApplicationNested = { + applicationId: "", + applicationStatus: "done", + appName: "", + autoDeploy: true, + branch: null, + buildArgs: null, + buildPath: "/", + buildType: "nixpacks", + command: null, + cpuLimit: null, + cpuReservation: null, + createdAt: "", + customGitBranch: "", + customGitBuildPath: "/", + customGitSSHKey: "", + customGitUrl: "", + description: "", + dockerfile: null, + dockerImage: null, + dropBuildPath: null, + enabled: null, + env: null, + healthCheckSwarm: null, + labelsSwarm: null, + memoryLimit: null, + memoryReservation: null, + modeSwarm: null, + mounts: [], + name: "", + networkSwarm: null, + owner: null, + password: null, + placementSwarm: null, + ports: [], + projectId: "", + redirects: [], + refreshToken: "", + registry: null, + registryId: null, + replicas: 1, + repository: null, + restartPolicySwarm: null, + rollbackConfigSwarm: null, + security: [], + sourceType: "git", + subtitle: null, + title: null, + updateConfigSwarm: null, + username: null, +}; + +const baseDomain: Domain = { + applicationId: "", + certificateType: "none", + createdAt: "", + domainId: "", + host: "", + https: false, + path: null, + port: null, + uniqueConfigKey: 1, +}; + +const baseRedirect: Redirect = { + redirectId: "", + regex: "", + replacement: "", + permanent: false, + uniqueConfigKey: 1, + createdAt: "", + applicationId: "", +}; + +/** Middlewares */ + +test("Web entrypoint on http domain", async () => { + const router = await createRouterConfig( + baseApp, + { ...baseDomain, https: false }, + "web", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); +}); + +test("Web entrypoint on http domain with redirect", async () => { + const router = await createRouterConfig( + { + ...baseApp, + appName: "test", + redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], + }, + { ...baseDomain, https: false }, + "web", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); + expect(router.middlewares).toContain("redirect-test-1"); +}); + +test("Web entrypoint on http domain with multiple redirect", async () => { + const router = await createRouterConfig( + { + ...baseApp, + appName: "test", + redirects: [ + { ...baseRedirect, uniqueConfigKey: 1 }, + { ...baseRedirect, uniqueConfigKey: 2 }, + ], + }, + { ...baseDomain, https: false }, + "web", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); + expect(router.middlewares).toContain("redirect-test-1"); + expect(router.middlewares).toContain("redirect-test-2"); +}); + +test("Web entrypoint on https domain", async () => { + const router = await createRouterConfig( + baseApp, + { ...baseDomain, https: true }, + "web", + ); + + expect(router.middlewares).toContain("redirect-to-https"); +}); + +test("Web entrypoint on https domain with redirect", async () => { + const router = await createRouterConfig( + { + ...baseApp, + appName: "test", + redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], + }, + { ...baseDomain, https: true }, + "web", + ); + + expect(router.middlewares).toContain("redirect-to-https"); + expect(router.middlewares).not.toContain("redirect-test-1"); +}); + +test("Websecure entrypoint on https domain", async () => { + const router = await createRouterConfig( + baseApp, + { ...baseDomain, https: true }, + "websecure", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); +}); + +test("Websecure entrypoint on https domain with redirect", async () => { + const router = await createRouterConfig( + { + ...baseApp, + appName: "test", + redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], + }, + { ...baseDomain, https: true }, + "websecure", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); + expect(router.middlewares).toContain("redirect-test-1"); +}); + +/** Certificates */ + +test("CertificateType on websecure entrypoint", async () => { + const router = await createRouterConfig( + baseApp, + { ...baseDomain, certificateType: "letsencrypt" }, + "websecure", + ); + + expect(router.tls?.certResolver).toBe("letsencrypt"); +}); diff --git a/server/utils/traefik/domain.ts b/server/utils/traefik/domain.ts index be7a7fac..016964df 100644 --- a/server/utils/traefik/domain.ts +++ b/server/utils/traefik/domain.ts @@ -13,12 +13,28 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => { const config: FileConfig = loadOrCreateConfig(appName); const serviceName = `${appName}-service-${domain.uniqueConfigKey}`; const routerName = `${appName}-router-${domain.uniqueConfigKey}`; + const routerNameSecure = `${appName}-router-websecure-${domain.uniqueConfigKey}`; config.http = config.http || { routers: {}, services: {} }; config.http.routers = config.http.routers || {}; config.http.services = config.http.services || {}; - config.http.routers[routerName] = await createRouterConfig(app, domain); + config.http.routers[routerName] = await createRouterConfig( + app, + domain, + "web", + ); + + // if (domain.https && process.env.NODE_ENV === "production") { + if (domain.https) { + config.http.routers[routerNameSecure] = await createRouterConfig( + app, + domain, + "websecure", + ); + } else { + delete config.http.routers[routerNameSecure]; + } config.http.services[serviceName] = createServiceConfig(appName, domain); writeTraefikConfig(config, appName); @@ -28,10 +44,15 @@ export const removeDomain = async (appName: string, uniqueKey: number) => { const config: FileConfig = loadOrCreateConfig(appName); const routerKey = `${appName}-router-${uniqueKey}`; + const routerSecureKey = `${appName}-router-websecure-${uniqueKey}`; + const serviceKey = `${appName}-service-${uniqueKey}`; if (config.http?.routers?.[routerKey]) { delete config.http.routers[routerKey]; } + if (config.http?.routers?.[routerSecureKey]) { + delete config.http.routers[routerSecureKey]; + } if (config.http?.services?.[serviceKey]) { delete config.http.services[serviceKey]; } @@ -47,7 +68,11 @@ export const removeDomain = async (appName: string, uniqueKey: number) => { } }; -const createRouterConfig = async (app: ApplicationNested, domain: Domain) => { +export const createRouterConfig = async ( + app: ApplicationNested, + domain: Domain, + entryPoint: "web" | "websecure", +) => { const { appName, redirects, security } = app; const { certificateType } = domain; @@ -56,32 +81,33 @@ const createRouterConfig = async (app: ApplicationNested, domain: Domain) => { rule: `Host(\`${host}\`)${path ? ` && PathPrefix(\`${path}\`)` : ""}`, service: `${appName}-service-${uniqueConfigKey}`, middlewares: [], - entryPoints: https - ? ["web", ...(process.env.NODE_ENV === "production" ? ["websecure"] : [])] - : ["web"], - tls: {}, + entryPoints: [entryPoint], }; - if (https) { + if (entryPoint === "web" && https) { routerConfig.middlewares = ["redirect-to-https"]; } - // redirects - for (const redirect of redirects) { - const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`; - routerConfig.middlewares?.push(middlewareName); + if ((entryPoint === "websecure" && https) || !https) { + // redirects + for (const redirect of redirects) { + const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`; + routerConfig.middlewares?.push(middlewareName); + } + + // security + if (security.length > 0) { + const middlewareName = `auth-${appName}`; + routerConfig.middlewares?.push(middlewareName); + } } - // security - if (security.length > 0) { - const middlewareName = `auth-${appName}`; - routerConfig.middlewares?.push(middlewareName); - } - - if (certificateType === "letsencrypt") { - routerConfig.tls = { certResolver: "letsencrypt" }; - } else if (certificateType === "none") { - routerConfig.tls = undefined; + if (entryPoint === "websecure") { + if (certificateType === "letsencrypt") { + routerConfig.tls = { certResolver: "letsencrypt" }; + } else if (certificateType === "none") { + routerConfig.tls = undefined; + } } return routerConfig; From 88f67b1c7152a01492ad5f0f94700077eb108b07 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Tue, 23 Jul 2024 21:06:57 +0200 Subject: [PATCH 24/61] test: reset baseapp --- __test__/traefik/traefik.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__test__/traefik/traefik.test.ts b/__test__/traefik/traefik.test.ts index 797074ea..f10eefb3 100644 --- a/__test__/traefik/traefik.test.ts +++ b/__test__/traefik/traefik.test.ts @@ -18,7 +18,7 @@ const baseApp: ApplicationNested = { cpuReservation: null, createdAt: "", customGitBranch: "", - customGitBuildPath: "/", + customGitBuildPath: "", customGitSSHKey: "", customGitUrl: "", description: "", From 56b52b3f9c944af9631f78ee82f7e80642eb590f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 24 Jul 2024 00:04:59 -0600 Subject: [PATCH 25/61] refactor(docker-build): replace docker build with the command --- server/utils/builders/docker-file.ts | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/server/utils/builders/docker-file.ts b/server/utils/builders/docker-file.ts index d8ead600..46aa44e5 100644 --- a/server/utils/builders/docker-file.ts +++ b/server/utils/builders/docker-file.ts @@ -1,9 +1,8 @@ import type { WriteStream } from "node:fs"; -import { docker } from "@/server/constants"; -import { prepareBuildArgs } from "@/server/utils/docker/utils"; -import * as tar from "tar-fs"; +import { prepareEnvironmentVariables } from "@/server/utils/docker/utils"; import type { ApplicationNested } from "."; import { getBuildAppDirectory } from "../filesystem/directory"; +import { spawnAsync } from "../process/spawnAsync"; import { createEnvFile } from "./utils"; export const buildCustomDocker = async ( @@ -14,29 +13,30 @@ export const buildCustomDocker = async ( const dockerFilePath = getBuildAppDirectory(application); try { const image = `${appName}`; + const contextPath = dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; - const tarStream = tar.pack(contextPath); + const args = prepareEnvironmentVariables(buildArgs); + + const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."]; + + for (const arg of args) { + commandArgs.push("--build-arg", arg); + } createEnvFile(dockerFilePath, env); - - const stream = await docker.buildImage(tarStream, { - t: image, - buildargs: prepareBuildArgs(buildArgs), - dockerfile: dockerFilePath.substring(dockerFilePath.lastIndexOf("/") + 1), - }); - - await new Promise((resolve, reject) => { - docker.modem.followProgress( - stream, - (err, res) => (err ? reject(err) : resolve(res)), - (event) => { - if (event.stream) { - writeStream.write(event.stream); - } - }, - ); - }); + await spawnAsync( + "docker", + commandArgs, + (data) => { + if (writeStream.writable) { + writeStream.write(data); + } + }, + { + cwd: contextPath, + }, + ); } catch (error) { throw error; } From 1b7ecd5a410fa3bde204e7da82e52372abbaa0b3 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Wed, 24 Jul 2024 10:39:15 +0200 Subject: [PATCH 26/61] refactor: remove comment --- server/utils/traefik/domain.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/utils/traefik/domain.ts b/server/utils/traefik/domain.ts index 016964df..401c7a8e 100644 --- a/server/utils/traefik/domain.ts +++ b/server/utils/traefik/domain.ts @@ -25,7 +25,6 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => { "web", ); - // if (domain.https && process.env.NODE_ENV === "production") { if (domain.https) { config.http.routers[routerNameSecure] = await createRouterConfig( app, From 13c686c2284453cb754d94e1a892211651a5e928 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Wed, 24 Jul 2024 18:47:52 +0200 Subject: [PATCH 27/61] feat: condition certificate --- .../application/domains/add-domain.tsx | 131 +++++---- .../application/domains/show-domains.tsx | 21 +- .../application/domains/update-domain.tsx | 254 ------------------ server/db/schema/domain.ts | 4 +- 4 files changed, 98 insertions(+), 312 deletions(-) delete mode 100644 components/dashboard/application/domains/update-domain.tsx diff --git a/components/dashboard/application/domains/add-domain.tsx b/components/dashboard/application/domains/add-domain.tsx index 17adf275..59cc05c3 100644 --- a/components/dashboard/application/domains/add-domain.tsx +++ b/components/dashboard/application/domains/add-domain.tsx @@ -29,38 +29,52 @@ import { import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { PlusIcon } from "lucide-react"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/; -// const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/; -// .regex(hostnameRegex -const addDomain = z.object({ - host: z.string().min(1, "Hostname is required"), +const domain = z.object({ + host: z.string().regex(hostnameRegex, { message: "Invalid hostname" }), path: z.string().min(1), - port: z.number(), + port: z + .number() + .min(1, { message: "Port must be at least 1" }) + .max(65535, { message: "Port must be 65535 or below" }), https: z.boolean(), certificateType: z.enum(["letsencrypt", "none"]), }); -type AddDomain = z.infer; +type Domain = z.infer; interface Props { applicationId: string; - children?: React.ReactNode; + domainId?: string; + children: React.ReactNode; } export const AddDomain = ({ applicationId, - children = , + domainId = "", + children, }: Props) => { + const [isOpen, setIsOpen] = useState(false); const utils = api.useUtils(); + const { data } = api.domain.one.useQuery( + { + domainId, + }, + { + enabled: !!domainId, + }, + ); - const { mutateAsync, isError, error } = api.domain.create.useMutation(); + const { mutateAsync, isError, error } = domainId + ? api.domain.update.useMutation() + : api.domain.create.useMutation(); - const form = useForm({ + const form = useForm({ defaultValues: { host: "", https: false, @@ -68,15 +82,29 @@ export const AddDomain = ({ port: 3000, certificateType: "none", }, - resolver: zodResolver(addDomain), + resolver: zodResolver(domain), }); useEffect(() => { - form.reset(); - }, [form, form.reset, form.formState.isSubmitSuccessful]); + if (data) { + form.reset(data); + } + }, [form, form.reset, data]); - const onSubmit = async (data: AddDomain) => { + const dictionary = { + success: domainId ? "Domain Updated" : "Domain Created", + error: domainId + ? "Error to update the domain" + : "Error to create the domain", + submit: domainId ? "Update" : "Create", + dialogDescription: domainId + ? "In this section you can edit a domain" + : "In this section you can add domains", + }; + + const onSubmit = async (data: Domain) => { await mutateAsync({ + domainId, applicationId, host: data.host, https: data.https, @@ -85,27 +113,27 @@ export const AddDomain = ({ certificateType: data.certificateType, }) .then(async () => { - toast.success("Domain Created"); + toast.success(dictionary.success); await utils.domain.byApplicationId.invalidate({ applicationId, }); await utils.application.readTraefikConfig.invalidate({ applicationId }); + setIsOpen(false); }) .catch(() => { - toast.error("Error to create the domain"); + toast.error(dictionary.error); + setIsOpen(false); }); }; return ( - + - + {children} Domain - - In this section you can add custom domains - + {dictionary.dialogDescription} {isError && {error?.message}} @@ -169,33 +197,36 @@ export const AddDomain = ({ ); }} /> - ( - - Certificate - + + + + + + + + None + + Letsencrypt (Default) + + + + + + )} + /> + )} - - None - - Letsencrypt (Default) - - - - - - )} - /> - Create + {dictionary.submit} diff --git a/components/dashboard/application/domains/show-domains.tsx b/components/dashboard/application/domains/show-domains.tsx index 5aed3524..d7724ce7 100644 --- a/components/dashboard/application/domains/show-domains.tsx +++ b/components/dashboard/application/domains/show-domains.tsx @@ -8,13 +8,11 @@ import { } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { ExternalLink, GlobeIcon, RefreshCcw } from "lucide-react"; +import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react"; import Link from "next/link"; -import React from "react"; import { AddDomain } from "./add-domain"; import { DeleteDomain } from "./delete-domain"; import { GenerateDomain } from "./generate-domain"; -import { UpdateDomain } from "./update-domain"; interface Props { applicationId: string; @@ -43,7 +41,9 @@ export const ShowDomains = ({ applicationId }: Props) => {
{data && data?.length > 0 && ( - Add Domain + )} {data && data?.length > 0 && ( @@ -61,7 +61,9 @@ export const ShowDomains = ({ applicationId }: Props) => {
- Add Domain + @@ -90,7 +92,14 @@ export const ShowDomains = ({ applicationId }: Props) => { {item.https ? "HTTPS" : "HTTP"}
- + + +
diff --git a/components/dashboard/application/domains/update-domain.tsx b/components/dashboard/application/domains/update-domain.tsx deleted file mode 100644 index 6614a480..00000000 --- a/components/dashboard/application/domains/update-domain.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/; - -const updateDomain = z.object({ - host: z.string().regex(hostnameRegex, { message: "Invalid hostname" }), - path: z.string().min(1), - port: z - .number() - .min(1, { message: "Port must be at least 1" }) - .max(65535, { message: "Port must be 65535 or below" }), - https: z.boolean(), - certificateType: z.enum(["letsencrypt", "none"]), -}); - -type UpdateDomain = z.infer; - -interface Props { - domainId: string; -} - -export const UpdateDomain = ({ domainId }: Props) => { - const utils = api.useUtils(); - const { data, refetch } = api.domain.one.useQuery( - { - domainId, - }, - { - enabled: !!domainId, - }, - ); - const { mutateAsync, isError, error } = api.domain.update.useMutation(); - - const form = useForm({ - defaultValues: { - host: "", - https: true, - path: "/", - port: 3000, - certificateType: "none", - }, - resolver: zodResolver(updateDomain), - }); - - useEffect(() => { - if (data) { - form.reset({ - host: data.host || "", - port: data.port || 3000, - path: data.path || "/", - https: data.https, - certificateType: data.certificateType, - }); - } - }, [form, form.reset, data]); - - const onSubmit = async (data: UpdateDomain) => { - await mutateAsync({ - domainId, - host: data.host, - https: data.https, - path: data.path, - port: data.port, - certificateType: data.certificateType, - }) - .then(async (data) => { - toast.success("Domain Updated"); - await refetch(); - await utils.domain.byApplicationId.invalidate({ - applicationId: data?.applicationId, - }); - await utils.application.readTraefikConfig.invalidate({ - applicationId: data?.applicationId, - }); - }) - .catch(() => { - toast.error("Error to update the domain"); - }); - }; - return ( - - - - - - - Domain - - In this section you can add custom domains - - - {isError && {error?.message}} - -
- -
-
- ( - - Host - - - - - - - )} - /> - - { - return ( - - Path - - - - - - ); - }} - /> - - { - return ( - - Container Port - - { - field.onChange(Number.parseInt(e.target.value)); - }} - /> - - - - ); - }} - /> - ( - - Certificate - - - - )} - /> - ( - -
- HTTPS - - Automatically provision SSL Certificate. - -
- - - -
- )} - /> -
-
-
- - - - - -
-
- ); -}; diff --git a/server/db/schema/domain.ts b/server/db/schema/domain.ts index 3ceca6b5..dad92090 100644 --- a/server/db/schema/domain.ts +++ b/server/db/schema/domain.ts @@ -13,8 +13,8 @@ export const domains = pgTable("domain", { .$defaultFn(() => nanoid()), host: text("host").notNull(), https: boolean("https").notNull().default(false), - port: integer("port").default(80), - path: text("path").default("/"), + port: integer("port").default(80).notNull(), + path: text("path").default("/").notNull(), uniqueConfigKey: serial("uniqueConfigKey"), createdAt: text("createdAt") .notNull() From fa5b75e6fbd4df6147b36391c6136bdde4812a68 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Wed, 24 Jul 2024 19:01:18 +0200 Subject: [PATCH 28/61] fix: type --- __test__/traefik/traefik.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__test__/traefik/traefik.test.ts b/__test__/traefik/traefik.test.ts index f10eefb3..f6af400d 100644 --- a/__test__/traefik/traefik.test.ts +++ b/__test__/traefik/traefik.test.ts @@ -63,8 +63,8 @@ const baseDomain: Domain = { domainId: "", host: "", https: false, - path: null, - port: null, + path: "", + port: 3000, uniqueConfigKey: 1, }; From 7fd35999b1918d35f0220334e6d81e0e1ad9a236 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Wed, 24 Jul 2024 19:38:25 +0200 Subject: [PATCH 29/61] fix: terminal text selectable --- components/dashboard/docker/logs/docker-logs-id.tsx | 5 ++++- styles/globals.css | 6 ------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/components/dashboard/docker/logs/docker-logs-id.tsx b/components/dashboard/docker/logs/docker-logs-id.tsx index be27aeda..a269cc0c 100644 --- a/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/components/dashboard/docker/logs/docker-logs-id.tsx @@ -23,8 +23,11 @@ export const DockerLogsId: React.FC = ({ id, containerId }) => { cursorBlink: true, cols: 80, rows: 30, - lineHeight: 1.4, + lineHeight: 1.25, fontWeight: 400, + fontSize: 14, + fontFamily: + 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', convertEol: true, theme: { diff --git a/styles/globals.css b/styles/globals.css index 3b207a0c..9d9ffaff 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -102,12 +102,6 @@ } } -#terminal span { - font-family: "Inter", sans-serif; - font-weight: 500; - letter-spacing: 0px !important; -} - /* Codemirror */ .cm-editor { @apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none; From 32f35a6ca0d4f41daa77719c22c276bd3b3ee566 Mon Sep 17 00:00:00 2001 From: Yuki <60097976+binaryYuki@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:25:28 +0800 Subject: [PATCH 30/61] Update traefik-setup.ts --- server/setup/traefik-setup.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/setup/traefik-setup.ts b/server/setup/traefik-setup.ts index 4fc383ac..692be4ce 100644 --- a/server/setup/traefik-setup.ts +++ b/server/setup/traefik-setup.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs"; import path from "node:path"; import type { CreateServiceOptions } from "dockerode"; import { dump } from "js-yaml"; @@ -184,6 +184,9 @@ export const createDefaultTraefikConfig = () => { const yamlStr = dump(configObject); mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true }); writeFileSync(mainConfig, yamlStr, "utf8"); + + const acmeJsonPath = "/etc/dokploy/traefik/dynamic/acme.json"; + chmodSync(acmeJsonPath, '600'); }; export const createDefaultMiddlewares = () => { From 087e2c81ccde63c7f374ca39b243b9a2c61a2264 Mon Sep 17 00:00:00 2001 From: Yuki <60097976+binaryYuki@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:34:18 +0800 Subject: [PATCH 31/61] accept suggestion by coderabbit Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- server/setup/traefik-setup.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/setup/traefik-setup.ts b/server/setup/traefik-setup.ts index 692be4ce..3b07d518 100644 --- a/server/setup/traefik-setup.ts +++ b/server/setup/traefik-setup.ts @@ -186,7 +186,11 @@ export const createDefaultTraefikConfig = () => { writeFileSync(mainConfig, yamlStr, "utf8"); const acmeJsonPath = "/etc/dokploy/traefik/dynamic/acme.json"; - chmodSync(acmeJsonPath, '600'); + if (existsSync(acmeJsonPath)) { + chmodSync(acmeJsonPath, '600'); + } else { + console.error(`File not found: ${acmeJsonPath}`); + } }; export const createDefaultMiddlewares = () => { From 734a6607c8a837caca406271cf5159f690c2a68d Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:31:03 -0600 Subject: [PATCH 32/61] fix(databases): add ip to useeffect deps --- .../mariadb/general/show-external-mariadb-credentials.tsx | 1 + .../dashboard/mongo/general/show-external-mongo-credentials.tsx | 1 + .../dashboard/mysql/general/show-external-mysql-credentials.tsx | 1 + .../postgres/general/show-external-postgres-credentials.tsx | 1 + .../dashboard/redis/general/show-external-redis-credentials.tsx | 2 +- 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx b/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx index b4ed7dc4..a908de07 100644 --- a/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx +++ b/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx @@ -90,6 +90,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => { form, data?.databaseName, data?.databaseUser, + ip, ]); return ( <> diff --git a/components/dashboard/mongo/general/show-external-mongo-credentials.tsx b/components/dashboard/mongo/general/show-external-mongo-credentials.tsx index 52b584fb..36d04c9c 100644 --- a/components/dashboard/mongo/general/show-external-mongo-credentials.tsx +++ b/components/dashboard/mongo/general/show-external-mongo-credentials.tsx @@ -90,6 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => { data?.databasePassword, form, data?.databaseUser, + ip, ]); return ( diff --git a/components/dashboard/mysql/general/show-external-mysql-credentials.tsx b/components/dashboard/mysql/general/show-external-mysql-credentials.tsx index caaf8556..18c1adaf 100644 --- a/components/dashboard/mysql/general/show-external-mysql-credentials.tsx +++ b/components/dashboard/mysql/general/show-external-mysql-credentials.tsx @@ -91,6 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => { data?.databaseName, data?.databaseUser, form, + ip, ]); return ( <> diff --git a/components/dashboard/postgres/general/show-external-postgres-credentials.tsx b/components/dashboard/postgres/general/show-external-postgres-credentials.tsx index d27022b7..28a96eb2 100644 --- a/components/dashboard/postgres/general/show-external-postgres-credentials.tsx +++ b/components/dashboard/postgres/general/show-external-postgres-credentials.tsx @@ -92,6 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => { data?.databasePassword, form, data?.databaseName, + ip, ]); return ( diff --git a/components/dashboard/redis/general/show-external-redis-credentials.tsx b/components/dashboard/redis/general/show-external-redis-credentials.tsx index 136f0ef4..b8832841 100644 --- a/components/dashboard/redis/general/show-external-redis-credentials.tsx +++ b/components/dashboard/redis/general/show-external-redis-credentials.tsx @@ -85,7 +85,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => { }; setConnectionUrl(buildConnectionUrl()); - }, [data?.appName, data?.externalPort, data?.databasePassword, form]); + }, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]); return ( <>
From 0af532f87ee26f6db649caecb410fe38bcb691bc Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:51:15 -0600 Subject: [PATCH 33/61] refactor(directories): add 600 permissions to SSH_PATH #262 --- server/setup/config-paths.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/setup/config-paths.ts b/server/setup/config-paths.ts index 2b2c262b..f609bc6c 100644 --- a/server/setup/config-paths.ts +++ b/server/setup/config-paths.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync } from "node:fs"; import { APPLICATIONS_PATH, BASE_PATH, @@ -32,6 +32,9 @@ export const setupDirectories = () => { for (const dir of directories) { try { createDirectoryIfNotExist(dir); + if (dir === SSH_PATH) { + chmodSync(SSH_PATH, "600"); + } } catch (error) { console.log(error, " On path: ", dir); } From 115c8641e74e106b2cdbc460d04660c97e95f3a9 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Thu, 25 Jul 2024 08:59:10 +0200 Subject: [PATCH 34/61] fix: nullable type --- __test__/traefik/traefik.test.ts | 4 ++-- components/dashboard/application/domains/add-domain.tsx | 5 +++-- server/db/schema/domain.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/__test__/traefik/traefik.test.ts b/__test__/traefik/traefik.test.ts index f6af400d..f10eefb3 100644 --- a/__test__/traefik/traefik.test.ts +++ b/__test__/traefik/traefik.test.ts @@ -63,8 +63,8 @@ const baseDomain: Domain = { domainId: "", host: "", https: false, - path: "", - port: 3000, + path: null, + port: null, uniqueConfigKey: 1, }; diff --git a/components/dashboard/application/domains/add-domain.tsx b/components/dashboard/application/domains/add-domain.tsx index 59cc05c3..b8ccae1c 100644 --- a/components/dashboard/application/domains/add-domain.tsx +++ b/components/dashboard/application/domains/add-domain.tsx @@ -37,11 +37,12 @@ const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/; const domain = z.object({ host: z.string().regex(hostnameRegex, { message: "Invalid hostname" }), - path: z.string().min(1), + path: z.string().min(1).nullable(), port: z .number() .min(1, { message: "Port must be at least 1" }) - .max(65535, { message: "Port must be 65535 or below" }), + .max(65535, { message: "Port must be 65535 or below" }) + .nullable(), https: z.boolean(), certificateType: z.enum(["letsencrypt", "none"]), }); diff --git a/server/db/schema/domain.ts b/server/db/schema/domain.ts index dad92090..3ceca6b5 100644 --- a/server/db/schema/domain.ts +++ b/server/db/schema/domain.ts @@ -13,8 +13,8 @@ export const domains = pgTable("domain", { .$defaultFn(() => nanoid()), host: text("host").notNull(), https: boolean("https").notNull().default(false), - port: integer("port").default(80).notNull(), - path: text("path").default("/").notNull(), + port: integer("port").default(80), + path: text("path").default("/"), uniqueConfigKey: serial("uniqueConfigKey"), createdAt: text("createdAt") .notNull() From e72add74c380f851ac0a7565cec3e8db3be064d3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:02:10 -0600 Subject: [PATCH 35/61] fix(templates): change path of volumes to be in files folder to prevent delete the volumes --- templates/appsmith/docker-compose.yml | 26 +++++++++++++------------- templates/directus/docker-compose.yml | 6 +++--- templates/listmonk/docker-compose.yml | 13 +++++++++---- templates/odoo/docker-compose.yml | 7 +++---- templates/plausible/docker-compose.yml | 8 ++++---- 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/templates/appsmith/docker-compose.yml b/templates/appsmith/docker-compose.yml index 4fbdc341..ad07a709 100644 --- a/templates/appsmith/docker-compose.yml +++ b/templates/appsmith/docker-compose.yml @@ -1,18 +1,18 @@ version: "3.8" services: - appsmith: - image: index.docker.io/appsmith/appsmith-ee:v1.29 - networks: - - dokploy-network - ports: - - ${APP_SMITH_PORT} - labels: - - "traefik.enable=true" - - "traefik.http.routers.${HASH}.rule=Host(`${APP_SMITH_HOST}`)" - - "traefik.http.services.${HASH}.loadbalancer.server.port=${APP_SMITH_PORT}" - volumes: - - ./stacks:/appsmith-stacks + appsmith: + image: index.docker.io/appsmith/appsmith-ee:v1.29 + networks: + - dokploy-network + ports: + - ${APP_SMITH_PORT} + labels: + - "traefik.enable=true" + - "traefik.http.routers.${HASH}.rule=Host(`${APP_SMITH_HOST}`)" + - "traefik.http.services.${HASH}.loadbalancer.server.port=${APP_SMITH_PORT}" + volumes: + - ../files/stacks:/appsmith-stacks networks: dokploy-network: - external: true \ No newline at end of file + external: true diff --git a/templates/directus/docker-compose.yml b/templates/directus/docker-compose.yml index c022e0b3..08a5db45 100644 --- a/templates/directus/docker-compose.yml +++ b/templates/directus/docker-compose.yml @@ -23,8 +23,8 @@ services: ports: - 8055 volumes: - - ./uploads:/directus/uploads - - ./extensions:/directus/extensions + - ../files/uploads:/directus/uploads + - ../files/extensions:/directus/extensions depends_on: - cache - database @@ -53,4 +53,4 @@ networks: dokploy-network: external: true volumes: - directus: \ No newline at end of file + directus: diff --git a/templates/listmonk/docker-compose.yml b/templates/listmonk/docker-compose.yml index e17b7657..beabf447 100644 --- a/templates/listmonk/docker-compose.yml +++ b/templates/listmonk/docker-compose.yml @@ -23,10 +23,15 @@ services: networks: - dokploy-network volumes: - - ./config.toml:/listmonk/config.toml + - ../files/config.toml:/listmonk/config.toml depends_on: - db - command: [sh, -c, "sleep 3 && ./listmonk --install --idempotent --yes --config config.toml"] + command: + [ + sh, + -c, + "sleep 3 && ./listmonk --install --idempotent --yes --config config.toml", + ] app: restart: unless-stopped @@ -41,7 +46,7 @@ services: - db - setup volumes: - - ./config.toml:/listmonk/config.toml + - ../files/config.toml:/listmonk/config.toml labels: - "traefik.enable=true" - "traefik.http.routers.${HASH}.rule=Host(`${LISTMONK_HOST}`)" @@ -50,7 +55,7 @@ services: volumes: listmonk-data: driver: local - + networks: dokploy-network: external: true diff --git a/templates/odoo/docker-compose.yml b/templates/odoo/docker-compose.yml index 8538bc72..e6e2a724 100644 --- a/templates/odoo/docker-compose.yml +++ b/templates/odoo/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +version: "3.8" services: web: image: odoo:16.0 @@ -18,8 +18,8 @@ services: - "traefik.http.services.${HASH}.loadbalancer.server.port=${ODOO_PORT}" volumes: - odoo-web-data:/var/lib/odoo - - ./config:/etc/odoo - - ./addons:/mnt/extra-addons + - ../files/config:/etc/odoo + - ../files/addons:/mnt/extra-addons db: image: postgres:13 @@ -36,7 +36,6 @@ volumes: odoo-web-data: odoo-db-data: - networks: dokploy-network: external: true diff --git a/templates/plausible/docker-compose.yml b/templates/plausible/docker-compose.yml index cc4c41e2..350cd87c 100644 --- a/templates/plausible/docker-compose.yml +++ b/templates/plausible/docker-compose.yml @@ -18,8 +18,8 @@ services: volumes: - event-data:/var/lib/clickhouse - event-logs:/var/log/clickhouse-server - - ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro - - ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro + - ../files/clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro + - ../files/clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro ulimits: nofile: soft: 262144 @@ -50,7 +50,7 @@ volumes: driver: local event-logs: driver: local - + networks: dokploy-network: - external: true \ No newline at end of file + external: true From 4cacc6b3d12e47fdc8a0f28d07a6394cdc2b41e4 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Thu, 25 Jul 2024 09:15:13 +0200 Subject: [PATCH 36/61] fix: type --- .../application/domains/add-domain.tsx | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/components/dashboard/application/domains/add-domain.tsx b/components/dashboard/application/domains/add-domain.tsx index b8ccae1c..623779dd 100644 --- a/components/dashboard/application/domains/add-domain.tsx +++ b/components/dashboard/application/domains/add-domain.tsx @@ -37,12 +37,11 @@ const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/; const domain = z.object({ host: z.string().regex(hostnameRegex, { message: "Invalid hostname" }), - path: z.string().min(1).nullable(), + path: z.string().min(1), port: z .number() .min(1, { message: "Port must be at least 1" }) - .max(65535, { message: "Port must be 65535 or below" }) - .nullable(), + .max(65535, { message: "Port must be 65535 or below" }), https: z.boolean(), certificateType: z.enum(["letsencrypt", "none"]), }); @@ -75,20 +74,26 @@ export const AddDomain = ({ ? api.domain.update.useMutation() : api.domain.create.useMutation(); + const defaultValues: Domain = { + host: "", + https: false, + path: "/", + port: 3000, + certificateType: "none", + }; + const form = useForm({ - defaultValues: { - host: "", - https: false, - path: "/", - port: 3000, - certificateType: "none", - }, + defaultValues, resolver: zodResolver(domain), }); useEffect(() => { if (data) { - form.reset(data); + form.reset({ + ...data, + path: data.path || defaultValues.path, + port: data.port || defaultValues.port, + }); } }, [form, form.reset, data]); @@ -107,11 +112,7 @@ export const AddDomain = ({ await mutateAsync({ domainId, applicationId, - host: data.host, - https: data.https, - path: data.path, - port: data.port, - certificateType: data.certificateType, + ...data, }) .then(async () => { toast.success(dictionary.success); From 54c7572447a46e720c30034567f5958106953c0b Mon Sep 17 00:00:00 2001 From: Yuki <60097976+binaryYuki@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:18:59 +0800 Subject: [PATCH 37/61] check if exist Check if the file already exist, if yes modify its mode --- server/setup/traefik-setup.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/setup/traefik-setup.ts b/server/setup/traefik-setup.ts index 3b07d518..b6f5867a 100644 --- a/server/setup/traefik-setup.ts +++ b/server/setup/traefik-setup.ts @@ -90,6 +90,13 @@ export const createDefaultServerTraefikConfig = () => { console.log("Default traefik config already exists"); return; } + + const acmeJsonPath = "/etc/dokploy/traefik/dynamic/acme.json"; + if (existsSync(acmeJsonPath)) { + chmodSync(acmeJsonPath, '600'); + } else { + console.error(`File not found: ${acmeJsonPath}`); + } const appName = "dokploy"; const serviceURLDefault = `http://${appName}:${process.env.PORT || 3000}`; @@ -184,13 +191,6 @@ export const createDefaultTraefikConfig = () => { const yamlStr = dump(configObject); mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true }); writeFileSync(mainConfig, yamlStr, "utf8"); - - const acmeJsonPath = "/etc/dokploy/traefik/dynamic/acme.json"; - if (existsSync(acmeJsonPath)) { - chmodSync(acmeJsonPath, '600'); - } else { - console.error(`File not found: ${acmeJsonPath}`); - } }; export const createDefaultMiddlewares = () => { From ef689f06d6309e30f4afbbfd80b51d8ef0d6c709 Mon Sep 17 00:00:00 2001 From: Yuki <60097976+binaryYuki@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:25:04 +0800 Subject: [PATCH 38/61] Update traefik-setup.ts --- server/setup/traefik-setup.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/server/setup/traefik-setup.ts b/server/setup/traefik-setup.ts index b6f5867a..7a5fee4f 100644 --- a/server/setup/traefik-setup.ts +++ b/server/setup/traefik-setup.ts @@ -86,17 +86,18 @@ export const initializeTraefik = async () => { export const createDefaultServerTraefikConfig = () => { const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml"); - if (existsSync(configFilePath)) { - console.log("Default traefik config already exists"); - return; - } + const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json"); - const acmeJsonPath = "/etc/dokploy/traefik/dynamic/acme.json"; if (existsSync(acmeJsonPath)) { chmodSync(acmeJsonPath, '600'); } else { console.error(`File not found: ${acmeJsonPath}`); } + + if (existsSync(configFilePath)) { + console.log("Default traefik config already exists"); + return; + } const appName = "dokploy"; const serviceURLDefault = `http://${appName}:${process.env.PORT || 3000}`; @@ -191,6 +192,12 @@ export const createDefaultTraefikConfig = () => { const yamlStr = dump(configObject); mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true }); writeFileSync(mainConfig, yamlStr, "utf8"); + const acmeJsonPath = "/etc/dokploy/traefik/dynamic/acme.json"; + if (existsSync(acmeJsonPath)) { + chmodSync(acmeJsonPath, '600'); + } else { + console.error(`File not found: ${acmeJsonPath}, func createDefaultTraefikConfig`); + } }; export const createDefaultMiddlewares = () => { From 9e47103131929e553528e817a2196d0ed69384ba Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:48:52 -0600 Subject: [PATCH 39/61] refactor(script): make ipv4 first and format the url when is ipv6 #258 --- docker/prod.sh | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/docker/prod.sh b/docker/prod.sh index 4dc23da2..2b833340 100644 --- a/docker/prod.sh +++ b/docker/prod.sh @@ -30,11 +30,6 @@ if ss -tulnp | grep ':443 ' >/dev/null; then exit 1 fi - - - - - command_exists() { command -v "$@" > /dev/null 2>&1 } @@ -46,7 +41,25 @@ else fi docker swarm leave --force 2>/dev/null -docker swarm init; + +get_ip() { + # Try to get IPv4 + local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null) + + if [ -n "$ipv4" ]; then + echo "$ipv4" + else + # Try to get IPv6 + local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null) + if [ -n "$ipv6" ]; then + echo "$ipv6" + fi + fi +} + +advertise_addr=$(get_ip) + +docker swarm init --advertise-addr $advertise_addr echo "Swarm initialized" @@ -71,19 +84,28 @@ docker service create \ --publish published=3000,target=3000,mode=host \ --update-parallelism 1 \ --update-order stop-first \ + --constraint 'node.role == manager' \ dokploy/dokploy:latest - -public_ip=$(hostname -I | awk '{print $1}') - GREEN="\033[0;32m" YELLOW="\033[1;33m" BLUE="\033[0;34m" NC="\033[0m" # No Color +format_ip_for_url() { + local ip="$1" + if echo "$ip" | grep -q ':'; then + # IPv6 + echo "[${ip}]" + else + # IPv4 + echo "${ip}" + fi +} +formatted_addr=$(format_ip_for_url "$advertise_addr") echo "" printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n" printf "${BLUE}Wait 15 seconds for the server to start${NC}\n" -printf "${YELLOW}Please go to http://${public_ip}:3000${NC}\n\n" +printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n" echo "" From 1519a715350062201500b319f05e912acea76b9b Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:49:41 -0600 Subject: [PATCH 40/61] refactor(script): make ipv4 first and format the url when is ipv6 #258 --- docker/prod.sh | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/docker/prod.sh b/docker/prod.sh index 4dc23da2..2b833340 100644 --- a/docker/prod.sh +++ b/docker/prod.sh @@ -30,11 +30,6 @@ if ss -tulnp | grep ':443 ' >/dev/null; then exit 1 fi - - - - - command_exists() { command -v "$@" > /dev/null 2>&1 } @@ -46,7 +41,25 @@ else fi docker swarm leave --force 2>/dev/null -docker swarm init; + +get_ip() { + # Try to get IPv4 + local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null) + + if [ -n "$ipv4" ]; then + echo "$ipv4" + else + # Try to get IPv6 + local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null) + if [ -n "$ipv6" ]; then + echo "$ipv6" + fi + fi +} + +advertise_addr=$(get_ip) + +docker swarm init --advertise-addr $advertise_addr echo "Swarm initialized" @@ -71,19 +84,28 @@ docker service create \ --publish published=3000,target=3000,mode=host \ --update-parallelism 1 \ --update-order stop-first \ + --constraint 'node.role == manager' \ dokploy/dokploy:latest - -public_ip=$(hostname -I | awk '{print $1}') - GREEN="\033[0;32m" YELLOW="\033[1;33m" BLUE="\033[0;34m" NC="\033[0m" # No Color +format_ip_for_url() { + local ip="$1" + if echo "$ip" | grep -q ':'; then + # IPv6 + echo "[${ip}]" + else + # IPv4 + echo "${ip}" + fi +} +formatted_addr=$(format_ip_for_url "$advertise_addr") echo "" printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n" printf "${BLUE}Wait 15 seconds for the server to start${NC}\n" -printf "${YELLOW}Please go to http://${public_ip}:3000${NC}\n\n" +printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n" echo "" From 9a4b474cdc3aba50c1fd71fc6d18f4e60802a674 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:04:54 -0600 Subject: [PATCH 41/61] refactor(traefik-setup): change order of chmodsync --- server/setup/traefik-setup.ts | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/server/setup/traefik-setup.ts b/server/setup/traefik-setup.ts index 7a5fee4f..e9f27860 100644 --- a/server/setup/traefik-setup.ts +++ b/server/setup/traefik-setup.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import type { CreateServiceOptions } from "dockerode"; import { dump } from "js-yaml"; @@ -86,17 +86,10 @@ export const initializeTraefik = async () => { export const createDefaultServerTraefikConfig = () => { const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml"); - const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json"); - - if (existsSync(acmeJsonPath)) { - chmodSync(acmeJsonPath, '600'); - } else { - console.error(`File not found: ${acmeJsonPath}`); - } - + if (existsSync(configFilePath)) { - console.log("Default traefik config already exists"); - return; + console.log("Default traefik config already exists"); + return; } const appName = "dokploy"; @@ -133,6 +126,11 @@ export const createDefaultServerTraefikConfig = () => { export const createDefaultTraefikConfig = () => { const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml"); + const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json"); + + if (existsSync(acmeJsonPath)) { + chmodSync(acmeJsonPath, "600"); + } if (existsSync(mainConfig)) { console.log("Main config already exists"); return; @@ -192,12 +190,6 @@ export const createDefaultTraefikConfig = () => { const yamlStr = dump(configObject); mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true }); writeFileSync(mainConfig, yamlStr, "utf8"); - const acmeJsonPath = "/etc/dokploy/traefik/dynamic/acme.json"; - if (existsSync(acmeJsonPath)) { - chmodSync(acmeJsonPath, '600'); - } else { - console.error(`File not found: ${acmeJsonPath}, func createDefaultTraefikConfig`); - } }; export const createDefaultMiddlewares = () => { From ee58672d587829db4a7d3973a67e27316aed9d61 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Thu, 25 Jul 2024 11:03:14 +0200 Subject: [PATCH 42/61] refactor: dry validation rules --- .../application/domains/add-domain.tsx | 42 ++++++++---------- server/db/schema/domain.ts | 43 +++++++------------ server/db/validations/index.ts | 25 +++++++++++ 3 files changed, 57 insertions(+), 53 deletions(-) create mode 100644 server/db/validations/index.ts diff --git a/components/dashboard/application/domains/add-domain.tsx b/components/dashboard/application/domains/add-domain.tsx index 623779dd..76899a7c 100644 --- a/components/dashboard/application/domains/add-domain.tsx +++ b/components/dashboard/application/domains/add-domain.tsx @@ -28,23 +28,14 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; -import { z } from "zod"; -const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/; -const domain = z.object({ - host: z.string().regex(hostnameRegex, { message: "Invalid hostname" }), - path: z.string().min(1), - port: z - .number() - .min(1, { message: "Port must be at least 1" }) - .max(65535, { message: "Port must be 65535 or below" }), - https: z.boolean(), - certificateType: z.enum(["letsencrypt", "none"]), -}); +import { domain } from "@/server/db/validations"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { flushSync } from "react-dom"; +import type z from "zod"; type Domain = z.infer; @@ -74,16 +65,7 @@ export const AddDomain = ({ ? api.domain.update.useMutation() : api.domain.create.useMutation(); - const defaultValues: Domain = { - host: "", - https: false, - path: "/", - port: 3000, - certificateType: "none", - }; - const form = useForm({ - defaultValues, resolver: zodResolver(domain), }); @@ -91,8 +73,9 @@ export const AddDomain = ({ if (data) { form.reset({ ...data, - path: data.path || defaultValues.path, - port: data.port || defaultValues.port, + /* Convert null to undefined */ + path: data.path || undefined, + port: data.port || undefined, }); } }, [form, form.reset, data]); @@ -120,11 +103,19 @@ export const AddDomain = ({ applicationId, }); await utils.application.readTraefikConfig.invalidate({ applicationId }); + + /* + Reset form if it was a new domain + Flushsync is needed for a bug witht he react-hook-form reset method + https://github.com/orgs/react-hook-form/discussions/7589#discussioncomment-10060621 + */ + if (!domainId) { + flushSync(() => form.reset()); + } setIsOpen(false); }) .catch(() => { toast.error(dictionary.error); - setIsOpen(false); }); }; return ( @@ -239,6 +230,7 @@ export const AddDomain = ({ Automatically provision SSL Certificate. +
({ references: [applications.applicationId], }), })); -const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/; -const createSchema = createInsertSchema(domains, { - domainId: z.string().min(1), - host: z.string().min(1), - path: z.string().min(1), - port: z.number(), - https: z.boolean(), - applicationId: z.string(), - certificateType: z.enum(["letsencrypt", "none"]), -}); -export const apiCreateDomain = createSchema - .pick({ - host: true, - path: true, - port: true, - https: true, - applicationId: true, - certificateType: true, - }) - .required(); +const createSchema = createInsertSchema(domains, domain._def.schema.shape); + +export const apiCreateDomain = createSchema.pick({ + host: true, + path: true, + port: true, + https: true, + applicationId: true, + certificateType: true, +}); export const apiFindDomain = createSchema .pick({ @@ -59,19 +49,16 @@ export const apiFindDomain = createSchema }) .required(); -export const apiFindDomainByApplication = createSchema - .pick({ - applicationId: true, - }) - .required(); +export const apiFindDomainByApplication = createSchema.pick({ + applicationId: true, +}); export const apiUpdateDomain = createSchema .pick({ - domainId: true, host: true, path: true, port: true, https: true, certificateType: true, }) - .required(); + .merge(createSchema.pick({ domainId: true }).required()); diff --git a/server/db/validations/index.ts b/server/db/validations/index.ts new file mode 100644 index 00000000..fcb6117a --- /dev/null +++ b/server/db/validations/index.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const domain = z + .object({ + host: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/, { + message: "Invalid hostname", + }), + path: z.string().min(1).optional(), + port: z + .number() + .min(1, { message: "Port must be at least 1" }) + .max(65535, { message: "Port must be 65535 or below" }) + .optional(), + https: z.boolean().optional(), + certificateType: z.enum(["letsencrypt", "none"]).optional(), + }) + .superRefine((input, ctx) => { + if (input.https && !input.certificateType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["certificateType"], + message: "Required", + }); + } + }); From f866250c25dcfef7424f3868a3e614a373fc5a47 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Thu, 25 Jul 2024 15:19:03 +0200 Subject: [PATCH 43/61] feat: new table and crud operations --- .../settings/ssh-keys/add-ssh-key.tsx | 165 ++++++++++++++++++ .../settings/ssh-keys/delete-ssh-key.tsx | 61 +++++++ .../settings/ssh-keys/show-ssh-keys.tsx | 96 ++++++++++ .../settings/ssh-keys/update-ssh-key.tsx | 148 ++++++++++++++++ components/layouts/settings-layout.tsx | 7 + pages/dashboard/settings/ssh-keys.tsx | 41 +++++ server/api/root.ts | 2 + server/api/routers/ssh-key.ts | 63 +++++++ server/api/services/ssh-key.ts | 70 ++++++++ server/db/schema/index.ts | 1 + server/db/schema/ssh-key.ts | 57 ++++++ server/db/validations/index.ts | 22 +++ 12 files changed, 733 insertions(+) create mode 100644 components/dashboard/settings/ssh-keys/add-ssh-key.tsx create mode 100644 components/dashboard/settings/ssh-keys/delete-ssh-key.tsx create mode 100644 components/dashboard/settings/ssh-keys/show-ssh-keys.tsx create mode 100644 components/dashboard/settings/ssh-keys/update-ssh-key.tsx create mode 100644 pages/dashboard/settings/ssh-keys.tsx create mode 100644 server/api/routers/ssh-key.ts create mode 100644 server/api/services/ssh-key.ts create mode 100644 server/db/schema/ssh-key.ts create mode 100644 server/db/validations/index.ts diff --git a/components/dashboard/settings/ssh-keys/add-ssh-key.tsx b/components/dashboard/settings/ssh-keys/add-ssh-key.tsx new file mode 100644 index 00000000..4e761c4a --- /dev/null +++ b/components/dashboard/settings/ssh-keys/add-ssh-key.tsx @@ -0,0 +1,165 @@ +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { sshKeyCreate } from "@/server/db/validations"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { type ReactNode, useState } from "react"; +import { flushSync } from "react-dom"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import type { z } from "zod"; + +type SSHKey = z.infer; + +interface Props { + children: ReactNode; +} + +export const AddSSHKey = ({ children }: Props) => { + const utils = api.useUtils(); + + const [isOpen, setIsOpen] = useState(false); + + const { mutateAsync, isError, error, isLoading } = + api.sshKey.create.useMutation(); + + const form = useForm({ + resolver: zodResolver(sshKeyCreate), + }); + + const onSubmit = async (data: SSHKey) => { + await mutateAsync(data) + .then(async () => { + toast.success("SSH key created successfully"); + await utils.sshKey.all.invalidate(); + /* + Flushsync is needed for a bug witht he react-hook-form reset method + https://github.com/orgs/react-hook-form/discussions/7589#discussioncomment-10060621 + */ + flushSync(() => form.reset()); + setIsOpen(false); + }) + .catch(() => { + toast.error("Error to create the SSH key"); + }); + }; + + return ( + + + {children} + + + + SSH Key + + In this section you can add an SSH key + + + {isError && {error?.message}} + +
+ + { + return ( + + Name + + + + + + ); + }} + /> + + { + return ( + + Description + + + + + + ); + }} + /> + ( + +
+ Private Key +
+ +