From 0bb10cabb95fa7e793d32ac5e2ea1c76ec724e72 Mon Sep 17 00:00:00 2001 From: jarek Date: Fri, 3 Apr 2026 11:51:42 +0200 Subject: [PATCH] v1.0.23 --- Dockerfile | 8 +- VERSION | 2 +- .../0004_add_git_stack_deploy_options.sql | 3 + drizzle-pg/meta/0004_snapshot.json | 2916 ++++++++++++++++ drizzle-pg/meta/_journal.json | 7 + drizzle/0004_add_git_stack_deploy_options.sql | 3 + drizzle/meta/0004_snapshot.json | 3046 +++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 27 +- server.js | 2 + src/lib/components/theme-toggle.svelte | 56 +- src/lib/data/changelog.json | 22 +- src/lib/server/db.ts | 53 +- src/lib/server/db/schema/index.ts | 3 + src/lib/server/db/schema/pg-schema.ts | 3 + src/lib/server/git.ts | 22 +- src/lib/server/scanner.ts | 44 +- src/lib/server/scheduler/index.ts | 63 +- .../server/scheduler/tasks/system-cleanup.ts | 64 + src/lib/server/sse-parser.ts | 89 + src/lib/server/sse.ts | 87 +- src/lib/server/stacks.ts | 99 +- src/lib/utils/shell-detection.ts | 77 + .../containers/batch-update-stream/+server.ts | 23 +- .../api/containers/check-updates/+server.ts | 2 +- src/routes/api/git/stacks/+server.ts | 5 +- src/routes/api/git/stacks/[id]/+server.ts | 5 +- src/routes/api/registry/image/+server.ts | 54 +- src/routes/api/settings/scanner/+server.ts | 6 +- .../api/settings/scanner/cache/+server.ts | 40 + .../api/stacks/[name]/deploy/+server.ts | 80 + .../api/stacks/[name]/restart/+server.ts | 4 +- src/routes/containers/+page.svelte | 45 +- src/routes/login/+page.svelte | 2 +- .../environments/EnvironmentModal.svelte | 2 +- src/routes/settings/general/GeneralTab.svelte | 44 + .../notifications/NotificationsTab.svelte | 2 +- src/routes/stacks/+page.svelte | 96 +- src/routes/stacks/GitStackModal.svelte | 52 +- src/routes/stacks/ImportStackModal.svelte | 37 +- src/routes/stacks/RedeployPopover.svelte | 99 + src/routes/terminal/+page.svelte | 85 +- 42 files changed, 7147 insertions(+), 239 deletions(-) create mode 100644 drizzle-pg/0004_add_git_stack_deploy_options.sql create mode 100644 drizzle-pg/meta/0004_snapshot.json create mode 100644 drizzle/0004_add_git_stack_deploy_options.sql create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 src/lib/server/sse-parser.ts create mode 100644 src/routes/api/settings/scanner/cache/+server.ts create mode 100644 src/routes/api/stacks/[name]/deploy/+server.ts create mode 100644 src/routes/stacks/RedeployPopover.svelte diff --git a/Dockerfile b/Dockerfile index 87a0bcb..62bdecb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,9 +75,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* \ && cp "$(dpkg -L libnss-wrapper | grep 'libnss_wrapper\.so$')" /usr/local/lib/libnss_wrapper.so -# Copy package files and install dependencies +# Copy package files and install dependencies (--ignore-scripts blocks malicious postinstall hooks) COPY package.json package-lock.json ./ -RUN npm ci +RUN npm ci --ignore-scripts \ + && npm rebuild better-sqlite3 argon2 # Copy source code and build COPY . . @@ -85,7 +86,8 @@ RUN npm run build # Production dependencies only (rebuilds native addons like better-sqlite3) RUN rm -rf node_modules \ - && npm ci --omit=dev \ + && npm ci --omit=dev --ignore-scripts \ + && npm rebuild better-sqlite3 argon2 \ && rm -rf node_modules/@types # Build Go collector diff --git a/VERSION b/VERSION index 90c4f8c..3a3aab6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.0.22 +v1.0.23 diff --git a/drizzle-pg/0004_add_git_stack_deploy_options.sql b/drizzle-pg/0004_add_git_stack_deploy_options.sql new file mode 100644 index 0000000..2df1b59 --- /dev/null +++ b/drizzle-pg/0004_add_git_stack_deploy_options.sql @@ -0,0 +1,3 @@ +ALTER TABLE "git_stacks" ADD COLUMN "build_on_deploy" boolean DEFAULT false;--> statement-breakpoint +ALTER TABLE "git_stacks" ADD COLUMN "repull_images" boolean DEFAULT false;--> statement-breakpoint +ALTER TABLE "git_stacks" ADD COLUMN "force_redeploy" boolean DEFAULT false; \ No newline at end of file diff --git a/drizzle-pg/meta/0004_snapshot.json b/drizzle-pg/meta/0004_snapshot.json new file mode 100644 index 0000000..02a5c0b --- /dev/null +++ b/drizzle-pg/meta/0004_snapshot.json @@ -0,0 +1,2916 @@ +{ + "id": "b10cba96-4947-484f-84a2-efb65205381f", + "prevId": "eef8322a-0ccc-418c-b0f6-f51972a1850e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_settings": { + "name": "auth_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_update_settings": { + "name": "auto_update_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.config_sets": { + "name": "config_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.container_events": { + "name": "container_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_notifications": { + "name": "environment_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "notification_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_name_unique": { + "name": "environments_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_credentials": { + "name": "git_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_repositories": { + "name": "git_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_stacks": { + "name": "git_stacks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_on_deploy": { + "name": "build_on_deploy", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repull_images": { + "name": "repull_images", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "force_redeploy": { + "name": "force_redeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hawser_tokens": { + "name": "hawser_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics": { + "name": "host_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_percent": { + "name": "memory_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_used": { + "name": "memory_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ldap_config": { + "name": "ldap_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_config": { + "name": "oidc_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_container_updates": { + "name": "pending_container_updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registries": { + "name": "registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registries_name_unique": { + "name": "registries_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_executions": { + "name": "schedule_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + { + "expression": "schedule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_environment_variables": { + "name": "stack_environment_variables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_events": { + "name": "stack_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_sources": { + "name": "stack_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_compose_path": { + "name": "external_compose_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_env_path": { + "name": "external_env_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "role_id", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vulnerability_scans": { + "name": "vulnerability_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanned_at": { + "name": "scanned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/_journal.json b/drizzle-pg/meta/_journal.json index 590bb1f..7e297bb 100644 --- a/drizzle-pg/meta/_journal.json +++ b/drizzle-pg/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1767687362730, "tag": "0003_add_stack_paths", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1774155653752, + "tag": "0004_add_git_stack_deploy_options", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/0004_add_git_stack_deploy_options.sql b/drizzle/0004_add_git_stack_deploy_options.sql new file mode 100644 index 0000000..16d430f --- /dev/null +++ b/drizzle/0004_add_git_stack_deploy_options.sql @@ -0,0 +1,3 @@ +ALTER TABLE `git_stacks` ADD `build_on_deploy` integer DEFAULT false;--> statement-breakpoint +ALTER TABLE `git_stacks` ADD `repull_images` integer DEFAULT false;--> statement-breakpoint +ALTER TABLE `git_stacks` ADD `force_redeploy` integer DEFAULT false; \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..68437c7 --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,3046 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "89db2259-da8c-4c3e-be28-084a352fe533", + "prevId": "6414712d-d1a8-437b-9d1c-e339b4829a85", + "tables": { + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_settings": { + "name": "auth_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auto_update_settings": { + "name": "auto_update_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_updated": { + "name": "last_updated", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "columns": [ + "environment_id", + "container_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config_sets": { + "name": "config_sets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "container_events": { + "name": "container_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_notifications": { + "name": "environment_notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "columns": [ + "environment_id", + "notification_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environments_name_unique": { + "name": "environments_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_credentials": { + "name": "git_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_repositories": { + "name": "git_repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'compose.yaml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_stacks": { + "name": "git_stacks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'compose.yaml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build_on_deploy": { + "name": "build_on_deploy", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "repull_images": { + "name": "repull_images", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "force_redeploy": { + "name": "force_redeploy", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hawser_tokens": { + "name": "hawser_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics": { + "name": "host_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_percent": { + "name": "memory_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_config": { + "name": "oidc_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pending_container_updates": { + "name": "pending_container_updates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "columns": [ + "environment_id", + "container_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "registries": { + "name": "registries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "registries_name_unique": { + "name": "registries_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_executions": { + "name": "schedule_executions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_at": { + "name": "triggered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + "schedule_type", + "schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_environment_variables": { + "name": "stack_environment_variables", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_secret": { + "name": "is_secret", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "columns": [ + "stack_name", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_events": { + "name": "stack_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_sources": { + "name": "stack_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_path": { + "name": "env_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "columns": [ + "user_id", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "columns": [ + "user_id", + "role_id", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vulnerability_scans": { + "name": "vulnerability_scans", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanned_at": { + "name": "scanned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + "environment_id", + "image_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index f4192f3..b6dcf18 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1767689000000, "tag": "0003_add_stack_paths", "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1774155653752, + "tag": "0004_add_git_stack_deploy_options", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 41aea4a..3b7f4f3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dockhand", "private": true, - "version": "1.0.22", + "version": "1.0.23", "type": "module", "scripts": { "dev": "npx vite dev", @@ -70,25 +70,25 @@ "@codemirror/theme-one-dark": "6.1.3", "@codemirror/view": "6.39.11", "@lezer/highlight": "1.2.3", - "@lucide/lab": "^0.1.2", + "@lucide/lab": "0.1.2", "ansi_up": "6.0.6", - "argon2": "^0.41.1", - "better-sqlite3": "^11.7.0", - "codemirror": "6.0.2", + "argon2": "0.41.1", + "better-sqlite3": "11.7.0", "croner": "9.1.0", "cronstrue": "3.9.0", "devalue": "5.6.4", "drizzle-orm": "0.45.1", - "js-yaml": "^4.1.1", - "ldapts": "^8.1.3", - "nodemailer": "^7.0.12", - "otpauth": "^9.4.1", + "fast-xml-parser": "5.5.8", + "js-yaml": "4.1.1", + "ldapts": "8.1.3", + "nodemailer": "8.0.4", + "otpauth": "9.4.1", "postgres": "3.4.8", - "qrcode": "^1.5.4", - "svelte-dnd-action": "0.9.69", + "qrcode": "1.5.4", + "rollup": "4.60.0", "svelte-sonner": "1.0.7", "undici": "7.24.5", - "ws": "^8.18.0" + "ws": "8.18.0" }, "devDependencies": { "@internationalized/date": "^3.10.1", @@ -102,7 +102,7 @@ "@types/better-sqlite3": "^7.6.12", "@types/js-yaml": "^4.0.9", "@types/node": "^22.10.0", - "@types/nodemailer": "7.0.5", + "@types/nodemailer": "7.0.11", "@types/qrcode": "^1.5.6", "@types/ws": "^8.5.13", "@xterm/addon-fit": "^0.11.0", @@ -122,7 +122,6 @@ "svelte": "5.53.5", "svelte-check": "^4.3.5", "svelte-easy-crop": "^5.0.0", - "svelte-virtual-scroll-list": "^1.3.0", "tailwind-merge": "^3.4.0", "tailwind-variants": "^3.2.2", "tailwindcss": "^4.1.18", diff --git a/server.js b/server.js index c76cd67..fb810b9 100644 --- a/server.js +++ b/server.js @@ -455,3 +455,5 @@ function handleHawserConnection(ws, connId, remoteIp) { server.listen(PORT, HOST, () => { console.log(`Listening on http://${HOST}:${PORT}/ with WebSocket`); }); + + diff --git a/src/lib/components/theme-toggle.svelte b/src/lib/components/theme-toggle.svelte index bebec2e..f97563a 100644 --- a/src/lib/components/theme-toggle.svelte +++ b/src/lib/components/theme-toggle.svelte @@ -1,44 +1,60 @@ - diff --git a/src/lib/data/changelog.json b/src/lib/data/changelog.json index 0907c7a..ed4d3b2 100644 --- a/src/lib/data/changelog.json +++ b/src/lib/data/changelog.json @@ -1,7 +1,27 @@ [ + { + "version": "1.0.23", + "date": "2026-04-03", + "changes": [ + { "type": "feature", "text": "theme toggle with system option — auto-follows OS light/dark preference (#803)" }, + { "type": "feature", "text": "custom user option for terminal shell sessions, persisted per container (#830)" }, + { "type": "feature", "text": "redeploy button for internal stacks with pull/build/force-recreate options (#152)" }, + { "type": "feature", "text": "build, re-pull images and force redeployment options for git stacks (#792, #472)" }, + { "type": "fix", "text": "allow underscores in hostname validation (#790)" }, + { "type": "fix", "text": "HTTPS git repos with self-signed CA certificates fail to clone/pull (#842)" }, + { "type": "fix", "text": "stack restart fails for containers using network_mode: service: — added recreate option (#844)" }, + { "type": "fix", "text": "git stack sync deletes data in relative volume paths (#831)" }, + { "type": "fix", "text": "batch update skips Hawser containers (#485)" }, + { "type": "fix", "text": "registry delete fails for multi-arch/OCI manifest images" }, + { "type": "fix", "text": "scanner cache cleanup to prevent volume bloat (#808)" }, + { "type": "fix", "text": "negotiate Docker API version for scanner/updater sidecar containers (#759)" }, + { "type": "fix", "text": "scan vulnerability counts mismatch with displayed list (#705)" } + ], + "imageTag": "fnsys/dockhand:v1.0.23" + }, { "version": "1.0.22", - "comingSoon": true, + "date": "2026-03-21", "changes": [ { "type": "feature", "text": "dashboard list view with inline search and connection filters (#740)" }, { "type": "feature", "text": "custom environment icon (#754)" }, diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 81b4ce6..2628d6f 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -2055,6 +2055,9 @@ export interface GitStackData { autoUpdateCron: string; webhookEnabled: boolean; webhookSecret: string | null; + buildOnDeploy: boolean; + repullImages: boolean; + forceRedeploy: boolean; lastSync: string | null; lastCommit: string | null; syncStatus: GitSyncStatus; @@ -2144,6 +2147,9 @@ export async function getGitStacks(environmentId?: number): Promise autoUpdateCron: gitStacks.autoUpdateCron, webhookEnabled: gitStacks.webhookEnabled, webhookSecret: gitStacks.webhookSecret, + buildOnDeploy: gitStacks.buildOnDeploy, + repullImages: gitStacks.repullImages, + forceRedeploy: gitStacks.forceRedeploy, lastSync: gitStacks.lastSync, lastCommit: gitStacks.lastCommit, syncStatus: gitStacks.syncStatus, @@ -2260,6 +2275,9 @@ export async function getGitStack(id: number): Promise autoUpdateCron: row.autoUpdateCron, webhookEnabled: row.webhookEnabled, webhookSecret: row.webhookSecret, + buildOnDeploy: row.buildOnDeploy ?? false, + repullImages: row.repullImages ?? false, + forceRedeploy: row.forceRedeploy ?? false, lastSync: row.lastSync, lastCommit: row.lastCommit, syncStatus: row.syncStatus, @@ -2289,6 +2307,9 @@ export async function getGitStackByName(stackName: string, environmentId?: numbe autoUpdateCron: gitStacks.autoUpdateCron, webhookEnabled: gitStacks.webhookEnabled, webhookSecret: gitStacks.webhookSecret, + buildOnDeploy: gitStacks.buildOnDeploy, + repullImages: gitStacks.repullImages, + forceRedeploy: gitStacks.forceRedeploy, lastSync: gitStacks.lastSync, lastCommit: gitStacks.lastCommit, syncStatus: gitStacks.syncStatus, @@ -2323,6 +2344,9 @@ export async function getGitStackByName(stackName: string, environmentId?: numbe autoUpdateCron: row.autoUpdateCron, webhookEnabled: row.webhookEnabled, webhookSecret: row.webhookSecret, + buildOnDeploy: row.buildOnDeploy ?? false, + repullImages: row.repullImages ?? false, + forceRedeploy: row.forceRedeploy ?? false, lastSync: row.lastSync, lastCommit: row.lastCommit, syncStatus: row.syncStatus, @@ -2352,6 +2376,9 @@ export async function getGitStackByWebhookSecret(secret: string): Promise { const result = await db.insert(gitStacks).values({ stackName: data.stackName, @@ -2419,7 +2452,10 @@ export async function createGitStack(data: { autoUpdateSchedule: data.autoUpdateSchedule || 'daily', autoUpdateCron: data.autoUpdateCron || '0 3 * * *', webhookEnabled: data.webhookEnabled || false, - webhookSecret: data.webhookSecret || null + webhookSecret: data.webhookSecret || null, + buildOnDeploy: data.buildOnDeploy ?? false, + repullImages: data.repullImages ?? false, + forceRedeploy: data.forceRedeploy ?? false }).returning(); return getGitStack(result[0].id) as Promise; } @@ -2436,6 +2472,9 @@ export async function updateGitStack(id: number, data: Partial): P if (data.autoUpdateCron !== undefined) updateData.autoUpdateCron = data.autoUpdateCron; if (data.webhookEnabled !== undefined) updateData.webhookEnabled = data.webhookEnabled; if (data.webhookSecret !== undefined) updateData.webhookSecret = data.webhookSecret; + if (data.buildOnDeploy !== undefined) updateData.buildOnDeploy = data.buildOnDeploy; + if (data.repullImages !== undefined) updateData.repullImages = data.repullImages; + if (data.forceRedeploy !== undefined) updateData.forceRedeploy = data.forceRedeploy; if (data.lastSync !== undefined) updateData.lastSync = data.lastSync; if (data.lastCommit !== undefined) updateData.lastCommit = data.lastCommit; if (data.syncStatus !== undefined) updateData.syncStatus = data.syncStatus; @@ -2470,6 +2509,9 @@ export async function getEnabledAutoUpdateGitStacks(): Promise { autoUpdateCron: gitStacks.autoUpdateCron, webhookEnabled: gitStacks.webhookEnabled, webhookSecret: gitStacks.webhookSecret, + buildOnDeploy: gitStacks.buildOnDeploy, + repullImages: gitStacks.repullImages, + forceRedeploy: gitStacks.forceRedeploy, lastSync: gitStacks.lastSync, lastCommit: gitStacks.lastCommit, syncStatus: gitStacks.syncStatus, @@ -2551,6 +2599,9 @@ export async function getAllAutoUpdateGitStacks(): Promise { autoUpdateCron: row.autoUpdateCron, webhookEnabled: row.webhookEnabled, webhookSecret: row.webhookSecret, + buildOnDeploy: row.buildOnDeploy ?? false, + repullImages: row.repullImages ?? false, + forceRedeploy: row.forceRedeploy ?? false, lastSync: row.lastSync, lastCommit: row.lastCommit, syncStatus: row.syncStatus, diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 396e84e..cb6ebaa 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -315,6 +315,9 @@ export const gitStacks = sqliteTable('git_stacks', { autoUpdateCron: text('auto_update_cron').default('0 3 * * *'), webhookEnabled: integer('webhook_enabled', { mode: 'boolean' }).default(false), webhookSecret: text('webhook_secret'), + buildOnDeploy: integer('build_on_deploy', { mode: 'boolean' }).default(false), + repullImages: integer('repull_images', { mode: 'boolean' }).default(false), + forceRedeploy: integer('force_redeploy', { mode: 'boolean' }).default(false), lastSync: text('last_sync'), lastCommit: text('last_commit'), syncStatus: text('sync_status').default('pending'), diff --git a/src/lib/server/db/schema/pg-schema.ts b/src/lib/server/db/schema/pg-schema.ts index 6c08051..68754f7 100644 --- a/src/lib/server/db/schema/pg-schema.ts +++ b/src/lib/server/db/schema/pg-schema.ts @@ -318,6 +318,9 @@ export const gitStacks = pgTable('git_stacks', { autoUpdateCron: text('auto_update_cron').default('0 3 * * *'), webhookEnabled: boolean('webhook_enabled').default(false), webhookSecret: text('webhook_secret'), + buildOnDeploy: boolean('build_on_deploy').default(false), + repullImages: boolean('repull_images').default(false), + forceRedeploy: boolean('force_redeploy').default(false), lastSync: timestamp('last_sync', { mode: 'string' }), lastCommit: text('last_commit'), syncStatus: text('sync_status').default('pending'), diff --git a/src/lib/server/git.ts b/src/lib/server/git.ts index 3517b7f..142e159 100644 --- a/src/lib/server/git.ts +++ b/src/lib/server/git.ts @@ -153,6 +153,11 @@ async function buildGitEnv(credential: GitCredential | null): Promise { SSH_AUTH_SOCK: '' }; + // Pass custom CA certificate to git CLI (NODE_EXTRA_CA_CERTS only affects Node.js) + if (process.env.NODE_EXTRA_CA_CERTS) { + env.GIT_SSL_CAINFO = process.env.NODE_EXTRA_CA_CERTS; + } + // Ensure current UID is resolvable for SSH/git operations await ensurePasswdEntry(env); @@ -932,8 +937,10 @@ export async function deployGitStack(stackId: number, options?: { force?: boolea // Check if there are changes - skip redeploy if no changes and not forced // Note: For new stacks (first deploy), syncResult.updated will be true - if (!force && !syncResult.updated) { - console.log(`${logPrefix} No changes detected and force=false, skipping redeploy`); + // forceRedeploy setting overrides the skip logic for webhooks/scheduled syncs + const shouldDeploy = force || gitStack.forceRedeploy || syncResult.updated; + if (!shouldDeploy) { + console.log(`${logPrefix} No changes detected and force=false, forceRedeploy=false, skipping redeploy`); return { success: true, output: 'No changes detected, skipping redeploy', @@ -943,6 +950,9 @@ export async function deployGitStack(stackId: number, options?: { force?: boolea const forceRecreate = syncResult.updated; console.log(`${logPrefix} Will force recreate:`, forceRecreate, `(updated=${syncResult.updated})`); + console.log(`${logPrefix} Build on deploy:`, gitStack.buildOnDeploy); + console.log(`${logPrefix} Re-pull images:`, gitStack.repullImages); + console.log(`${logPrefix} Force redeploy setting:`, gitStack.forceRedeploy); // Deploy using unified function - handles both new and existing stacks // Uses `docker compose up -d --remove-orphans` which only recreates changed services @@ -960,7 +970,9 @@ export async function deployGitStack(stackId: number, options?: { force?: boolea sourceDir: syncResult.composeDir, // Copy entire directory from git repo composeFileName: syncResult.composeFileName, // Use original compose filename from repo envFileName: syncResult.envFileName, // Env file relative to compose dir (for --env-file flag, optional) - forceRecreate + forceRecreate, + build: gitStack.buildOnDeploy, + pullPolicy: gitStack.repullImages ? 'always' : undefined }); console.log(`${logPrefix} ----------------------------------------`); @@ -1214,7 +1226,9 @@ export async function deployGitStackWithProgress( envId: gitStack.environmentId, sourceDir: composeDir, // Copy entire directory from git repo composeFileName: basename(gitStack.composePath), // Use original compose filename from repo - envFileName // Env file relative to compose dir (for --env-file flag, optional) + envFileName, // Env file relative to compose dir (for --env-file flag, optional) + build: gitStack.buildOnDeploy, + pullPolicy: gitStack.repullImages ? 'always' : undefined }); if (result.success) { diff --git a/src/lib/server/scanner.ts b/src/lib/server/scanner.ts index a5d71e8..0978243 100644 --- a/src/lib/server/scanner.ts +++ b/src/lib/server/scanner.ts @@ -18,7 +18,7 @@ import { getEnvironment, getEnvSetting, getSetting } from './db'; import { sendEventNotification } from './notifications'; import { getHostDockerSocket, getHostDataDir, extractUidFromSocketPath, getOwnDockerHost, getOwnNetworkMode } from './host-path'; import { resolve } from 'node:path'; -import { mkdir, chown } from 'node:fs/promises'; +import { mkdir, chown, rm } from 'node:fs/promises'; export type ScannerType = 'none' | 'grype' | 'trivy' | 'both'; @@ -1147,3 +1147,45 @@ export async function cleanupScannerVolumes(envId?: number): Promise { console.error('[Scanner] Failed to cleanup scanner volumes:', errorMsg); } } + +/** + * Clean up all scanner cache storage (volumes + bind mount directories). + * Handles both standard Docker (named volumes) and rootless Docker (bind mounts). + * Next scan after cleanup will re-download a fresh vulnerability database (~200MB). + */ +export async function cleanupScannerCache(envId?: number): Promise<{ volumes: string[]; dirs: string[] }> { + const removedVolumes: string[] = []; + const removedDirs: string[] = []; + + // 1. Remove named volumes (standard Docker mode) + for (const volumeName of [GRYPE_VOLUME_NAME, TRIVY_VOLUME_NAME]) { + try { + await removeVolume(volumeName, true, envId); + removedVolumes.push(volumeName); + const envSuffix = envId ? ` (env ${envId})` : ''; + console.log(`[Scanner] Removed volume: ${volumeName}${envSuffix}`); + } catch { + // Volume might not exist, ignore + } + } + + // 2. Remove bind mount cache directories (rootless Docker mode, local only) + if (!envId) { + for (const scannerType of ['grype', 'trivy'] as const) { + const cachePath = resolve(DATA_DIR, SCANNER_CACHE_DIR, scannerType); + try { + await rm(cachePath, { recursive: true, force: true }); + removedDirs.push(cachePath); + console.log(`[Scanner] Removed cache directory: ${cachePath}`); + } catch { + // Directory might not exist, ignore + } + } + } + + if (removedVolumes.length > 0 || removedDirs.length > 0) { + console.log(`[Scanner] Cache cleanup complete: ${removedVolumes.length} volumes, ${removedDirs.length} directories removed`); + } + + return { volumes: removedVolumes, dirs: removedDirs }; +} diff --git a/src/lib/server/scheduler/index.ts b/src/lib/server/scheduler/index.ts index 8a2f210..1243c7b 100644 --- a/src/lib/server/scheduler/index.ts +++ b/src/lib/server/scheduler/index.ts @@ -45,9 +45,11 @@ import { runScheduleCleanupJob, runEventCleanupJob, runVolumeHelperCleanupJob, + runScannerCacheCleanupJob, SYSTEM_SCHEDULE_CLEANUP_ID, SYSTEM_EVENT_CLEANUP_ID, - SYSTEM_VOLUME_HELPER_CLEANUP_ID + SYSTEM_VOLUME_HELPER_CLEANUP_ID, + SYSTEM_SCANNER_CLEANUP_ID } from './tasks/system-cleanup'; // Store all active cron jobs @@ -57,6 +59,7 @@ const activeJobs: Map = new Map(); let cleanupJob: Cron | null = null; let eventCleanupJob: Cron | null = null; let volumeHelperCleanupJob: Cron | null = null; +let scannerCacheCleanupJob: Cron | null = null; // Scheduler state let isRunning = false; @@ -131,10 +134,35 @@ export async function startScheduler(): Promise { await runVolumeHelperCleanupJob('cron', volumeCleanupFns); }); + // Scanner cache cleanup runs weekly (Sunday 3am) to prevent DB volume bloat + const scannerCleanupFn = async () => { + const { cleanupScannerCache } = await import('../scanner'); + const envs = await getEnvironments(); + + // Clean local cache (volumes + bind mount dirs) + const localResult = await cleanupScannerCache(); + + // Clean remote environment volumes + for (const env of envs) { + try { + const envResult = await cleanupScannerCache(env.id); + localResult.volumes.push(...envResult.volumes); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + console.log(`[Scanner] Skipping cache cleanup for env "${env.name}" (id=${env.id}): ${msg}`); + } + } + + return localResult; + }; + scannerCacheCleanupJob = new Cron('0 3 * * 0', { timezone: defaultTimezone, legacyMode: false }, async () => { + await runScannerCacheCleanupJob('cron', scannerCleanupFn); + }); console.log(`[Scheduler] System schedule cleanup: ${scheduleCleanupCron} [${defaultTimezone}]`); console.log(`[Scheduler] System event cleanup: ${eventCleanupCron} [${defaultTimezone}]`); console.log(`[Scheduler] Volume helper cleanup: every 30 minutes [${defaultTimezone}]`); + console.log(`[Scheduler] Scanner cache cleanup: weekly (Sunday 3am) [${defaultTimezone}]`); // Register all dynamic schedules from database await refreshAllSchedules(); @@ -164,6 +192,10 @@ export function stopScheduler(): void { volumeHelperCleanupJob.stop(); volumeHelperCleanupJob = null; } + if (scannerCacheCleanupJob) { + scannerCacheCleanupJob.stop(); + scannerCacheCleanupJob = null; + } // Stop all dynamic jobs for (const [key, job] of activeJobs.entries()) { @@ -487,6 +519,9 @@ export async function refreshSystemJobs(): Promise { if (volumeHelperCleanupJob) { volumeHelperCleanupJob.stop(); } + if (scannerCacheCleanupJob) { + scannerCacheCleanupJob.stop(); + } // Re-create with new timezone cleanupJob = new Cron(scheduleCleanupCron, { timezone: defaultTimezone, legacyMode: false }, async () => { @@ -501,9 +536,18 @@ export async function refreshSystemJobs(): Promise { await runVolumeHelperCleanupJob('cron', volumeCleanupFns); }); + const scannerCleanupFn = async () => { + const { cleanupScannerCache } = await import('../scanner'); + return cleanupScannerCache(); + }; + scannerCacheCleanupJob = new Cron('0 3 * * 0', { timezone: defaultTimezone, legacyMode: false }, async () => { + await runScannerCacheCleanupJob('cron', scannerCleanupFn); + }); + console.log(`[Scheduler] System schedule cleanup: ${scheduleCleanupCron} [${defaultTimezone}]`); console.log(`[Scheduler] System event cleanup: ${eventCleanupCron} [${defaultTimezone}]`); console.log(`[Scheduler] Volume helper cleanup: every 30 minutes [${defaultTimezone}]`); + console.log(`[Scheduler] Scanner cache cleanup: weekly (Sunday 3am) [${defaultTimezone}]`); } // ============================================================================= @@ -637,6 +681,13 @@ export async function triggerSystemJob(jobId: string): Promise<{ success: boolea cleanupExpiredVolumeHelpers }); return { success: true }; + } else if (jobId === String(SYSTEM_SCANNER_CLEANUP_ID) || jobId === 'scanner-cache-cleanup') { + const scannerCleanupFn = async () => { + const { cleanupScannerCache } = await import('../scanner'); + return cleanupScannerCache(); + }; + runScannerCacheCleanupJob('manual', scannerCleanupFn); + return { success: true }; } else { return { success: false, error: 'Unknown system job ID' }; } @@ -694,6 +745,16 @@ export async function getSystemSchedules(): Promise { nextRun: getNextRun('*/30 * * * *')?.toISOString() ?? null, isSystem: true, enabled: true + }, + { + id: SYSTEM_SCANNER_CLEANUP_ID, + type: 'system_cleanup' as const, + name: 'Scanner cache cleanup', + description: 'Removes scanner vulnerability database cache to reclaim disk space', + cronExpression: '0 3 * * 0', + nextRun: getNextRun('0 3 * * 0')?.toISOString() ?? null, + isSystem: true, + enabled: true } ]; } diff --git a/src/lib/server/scheduler/tasks/system-cleanup.ts b/src/lib/server/scheduler/tasks/system-cleanup.ts index 5f6d6e0..715c4ca 100644 --- a/src/lib/server/scheduler/tasks/system-cleanup.ts +++ b/src/lib/server/scheduler/tasks/system-cleanup.ts @@ -20,6 +20,7 @@ import { export const SYSTEM_SCHEDULE_CLEANUP_ID = 1; export const SYSTEM_EVENT_CLEANUP_ID = 2; export const SYSTEM_VOLUME_HELPER_CLEANUP_ID = 3; +export const SYSTEM_SCANNER_CLEANUP_ID = 4; /** * Execute schedule execution cleanup job. @@ -200,3 +201,66 @@ export async function runVolumeHelperCleanupJob( }); } } + +/** + * Execute scanner cache cleanup job. + * Removes scanner database volumes and bind mount directories to reclaim disk space. + */ +export async function runScannerCacheCleanupJob( + triggeredBy: ScheduleTrigger = 'cron', + cleanupFn?: () => Promise<{ volumes: string[]; dirs: string[] }> +): Promise { + const startTime = Date.now(); + + const execution = await createScheduleExecution({ + scheduleType: 'system_cleanup', + scheduleId: SYSTEM_SCANNER_CLEANUP_ID, + environmentId: null, + entityName: 'Scanner cache cleanup', + triggeredBy, + status: 'running' + }); + + await updateScheduleExecution(execution.id, { + startedAt: new Date().toISOString() + }); + + const log = async (message: string) => { + console.log(`[Scanner Cache Cleanup] ${message}`); + await appendScheduleExecutionLog(execution.id, `[${new Date().toISOString()}] ${message}`); + }; + + try { + await log('Starting scanner cache cleanup'); + + let result: { volumes: string[]; dirs: string[] }; + if (cleanupFn) { + result = await cleanupFn(); + } else { + const { cleanupScannerCache } = await import('../../scanner'); + result = await cleanupScannerCache(); + } + + if (result.volumes.length > 0) { + await log(`Removed volumes: ${result.volumes.join(', ')}`); + } + if (result.dirs.length > 0) { + await log(`Removed directories: ${result.dirs.join(', ')}`); + } + await log(`Cleanup complete: ${result.volumes.length} volumes, ${result.dirs.length} directories removed`); + await updateScheduleExecution(execution.id, { + status: 'success', + completedAt: new Date().toISOString(), + duration: Date.now() - startTime, + details: { removedVolumes: result.volumes, removedDirs: result.dirs } + }); + } catch (error: any) { + await log(`Error: ${error.message}`); + await updateScheduleExecution(execution.id, { + status: 'failed', + completedAt: new Date().toISOString(), + duration: Date.now() - startTime, + errorMessage: error.message + }); + } +} diff --git a/src/lib/server/sse-parser.ts b/src/lib/server/sse-parser.ts new file mode 100644 index 0000000..3f2912a --- /dev/null +++ b/src/lib/server/sse-parser.ts @@ -0,0 +1,89 @@ +/** + * Pure SSE parsing utilities — no server dependencies. + * Can be safely imported in unit tests and client code. + */ + +/** + * Check if the client prefers JSON over SSE. + * Returns true when Accept header includes application/json but NOT text/event-stream. + */ +export function prefersJSON(request?: Request): boolean { + const accept = request?.headers.get('accept') || ''; + return accept.includes('application/json') && !accept.includes('text/event-stream'); +} + +/** + * Wrap an SSE Response for JSON-preferring clients. + * + * Consumes the SSE stream using proper event framing (blank-line delimited, + * multi-line data joined with \n, CRLF stripped). Returns the `result` event + * data as a JSON response, or a fallback if no result event was emitted. + * + * Usage: + * if (prefersJSON(request)) return sseToJSON(buildSSEResponse()); + * return buildSSEResponse(); + */ +export async function sseToJSON(sseResponse: Response): Promise { + const reader = sseResponse.body!.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let eventType = ''; + let dataLines: string[] = []; + let resultData: unknown = null; + + const dispatch = () => { + const data = dataLines.join('\n'); + const type = eventType || 'message'; + eventType = ''; + dataLines = []; + if (type === 'result' && data) { + try { + resultData = JSON.parse(data); + } catch { + // keep previous resultData + } + } + }; + + const parseLine = (rawLine: string) => { + const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine; + if (line.startsWith(':')) return; + if (line === '') { dispatch(); return; } + const colon = line.indexOf(':'); + const field = colon === -1 ? line : line.slice(0, colon); + let val = colon === -1 ? '' : line.slice(colon + 1); + if (val.startsWith(' ')) val = val.slice(1); + if (field === 'event') eventType = val || 'message'; + else if (field === 'data') dataLines.push(val); + }; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) parseLine(line); + } + + // Flush remaining bytes and process trailing content + buffer += decoder.decode(); + if (buffer) { + for (const line of buffer.split('\n')) parseLine(line); + } + // Final dispatch for servers missing trailing blank line + if (dataLines.length > 0) dispatch(); + } catch { + // stream error, return what we have + } finally { + reader.releaseLock(); + } + + const body = resultData ?? { success: false, error: 'No result' }; + return new Response(JSON.stringify(body), { + headers: { 'content-type': 'application/json' } + }); +} diff --git a/src/lib/server/sse.ts b/src/lib/server/sse.ts index 6397b53..24f5e6f 100644 --- a/src/lib/server/sse.ts +++ b/src/lib/server/sse.ts @@ -1,90 +1,9 @@ import { json } from '@sveltejs/kit'; import { createJob, appendLine, completeJob, failJob } from '$lib/server/jobs'; +import { prefersJSON } from '$lib/server/sse-parser'; -/** - * Check if the client prefers JSON over SSE. - * Returns true when Accept header includes application/json but NOT text/event-stream. - */ -export function prefersJSON(request?: Request): boolean { - const accept = request?.headers.get('accept') || ''; - return accept.includes('application/json') && !accept.includes('text/event-stream'); -} - -/** - * Wrap an SSE Response for JSON-preferring clients. - * - * Consumes the SSE stream using proper event framing (blank-line delimited, - * multi-line data joined with \n, CRLF stripped). Returns the `result` event - * data as a JSON response, or a fallback if no result event was emitted. - * - * Usage: - * if (prefersJSON(request)) return sseToJSON(buildSSEResponse()); - * return buildSSEResponse(); - */ -export async function sseToJSON(sseResponse: Response): Promise { - const reader = sseResponse.body!.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - let eventType = ''; - let dataLines: string[] = []; - let resultData: unknown = null; - - const dispatch = () => { - const data = dataLines.join('\n'); - const type = eventType || 'message'; - eventType = ''; - dataLines = []; - if (type === 'result' && data) { - try { - resultData = JSON.parse(data); - } catch { - // keep previous resultData - } - } - }; - - const parseLine = (rawLine: string) => { - const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine; - if (line.startsWith(':')) return; - if (line === '') { dispatch(); return; } - const colon = line.indexOf(':'); - const field = colon === -1 ? line : line.slice(0, colon); - let val = colon === -1 ? '' : line.slice(colon + 1); - if (val.startsWith(' ')) val = val.slice(1); - if (field === 'event') eventType = val || 'message'; - else if (field === 'data') dataLines.push(val); - }; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) parseLine(line); - } - - // Flush remaining bytes and process trailing content - buffer += decoder.decode(); - if (buffer) { - for (const line of buffer.split('\n')) parseLine(line); - } - // Final dispatch for servers missing trailing blank line - if (dataLines.length > 0) dispatch(); - } catch { - // stream error, return what we have - } finally { - reader.releaseLock(); - } - - const body = resultData ?? { success: false, error: 'No result' }; - return new Response(JSON.stringify(body), { - headers: { 'Content-Type': 'application/json' } - }); -} +// Re-export pure parsing utilities (no server deps) for backward compat +export { prefersJSON, sseToJSON } from '$lib/server/sse-parser'; /** * Job-based response for long-running operations. diff --git a/src/lib/server/stacks.ts b/src/lib/server/stacks.ts index ee032be..e6f3cac 100644 --- a/src/lib/server/stacks.ts +++ b/src/lib/server/stacks.ts @@ -58,6 +58,8 @@ export interface StackOperationResult { success: boolean; output?: string; error?: string; + /** The docker compose command that was executed (for debugging/testing) */ + command?: string; } /** @@ -99,6 +101,8 @@ export interface DeployStackOptions { envId?: number | null; sourceDir?: string; // Directory to copy all files from (for git stacks) forceRecreate?: boolean; + build?: boolean; // Build images before starting (--build) + pullPolicy?: string; // Pull policy: 'always' | 'missing' | 'never' composePath?: string; // Custom compose file path (for adopted/imported stacks) envPath?: string; // Custom env file path (for adopted/imported stacks) composeFileName?: string; // Compose filename to use (e.g., "docker-compose.yaml") for git stacks @@ -788,6 +792,8 @@ interface ComposeCommandOptions { stackName: string; envId?: number | null; forceRecreate?: boolean; + build?: boolean; // Build images before starting (--build) + pullPolicy?: string; // Pull policy: 'always' | 'missing' | 'never' removeVolumes?: boolean; stackFiles?: Record; // All files to send to Hawser /** Working directory for compose execution (for imported stacks) */ @@ -848,7 +854,9 @@ async function executeLocalCompose( customComposePath?: string, customEnvPath?: string, useOverrideFile?: boolean, - serviceName?: string + serviceName?: string, + build?: boolean, + pullPolicy?: string ): Promise { const logPrefix = `[Stack:${stackName}]`; @@ -1040,6 +1048,8 @@ async function executeLocalCompose( case 'up': args.push('up', '-d', '--remove-orphans'); if (forceRecreate) args.push('--force-recreate'); + if (build) args.push('--build'); + if (pullPolicy) args.push('--pull', pullPolicy); // If targeting a specific service, only update that service if (serviceName) { args.push(serviceName); @@ -1067,11 +1077,13 @@ async function executeLocalCompose( break; } + const commandStr = args.join(' '); + console.log(`${logPrefix} ----------------------------------------`); console.log(`${logPrefix} EXECUTE LOCAL COMPOSE`); console.log(`${logPrefix} ----------------------------------------`); console.log(`${logPrefix} Operation:`, operation); - console.log(`${logPrefix} Command:`, args.join(' ')); + console.log(`${logPrefix} Command:`, commandStr); console.log(`${logPrefix} Working directory:`, stackDir); console.log(`${logPrefix} Compose file:`, composeFile); console.log(`${logPrefix} DOCKER_HOST:`, dockerHost || '(local socket)'); @@ -1141,20 +1153,23 @@ async function executeLocalCompose( return { success: false, output: stdout, - error: `docker compose ${operation} timed out after ${COMPOSE_TIMEOUT_MS / 1000} seconds` + error: `docker compose ${operation} timed out after ${COMPOSE_TIMEOUT_MS / 1000} seconds`, + command: commandStr }; } if (code === 0) { return { success: true, - output: stdout || stderr || `Stack "${stackName}" ${operation} completed successfully` + output: stdout || stderr || `Stack "${stackName}" ${operation} completed successfully`, + command: commandStr }; } else { return { success: false, output: stdout, - error: stderr || `docker compose ${operation} exited with code ${code}` + error: stderr || `docker compose ${operation} exited with code ${code}`, + command: commandStr }; } } finally { @@ -1165,7 +1180,8 @@ async function executeLocalCompose( return { success: false, output: '', - error: `Failed to run docker compose ${operation}: ${err.message}` + error: `Failed to run docker compose ${operation}: ${err.message}`, + command: commandStr }; } finally { // Cleanup temp override file from host path translation @@ -1207,7 +1223,9 @@ async function executeComposeViaHawser( removeVolumes?: boolean, stackFiles?: Record, serviceName?: string, - composeFileName?: string + composeFileName?: string, + build?: boolean, + pullPolicy?: string ): Promise { const logPrefix = `[Stack:${stackName}]`; // Import dockerFetch dynamically to avoid circular dependency @@ -1280,6 +1298,8 @@ async function executeComposeViaHawser( files, // Files including .env (secrets NOT in .env file) forceRecreate: forceRecreate || false, removeVolumes: removeVolumes || false, + build: build || false, + pullPolicy: pullPolicy || '', registries, // Registry credentials for docker login serviceName // Target specific service only (with --no-deps) }); @@ -1347,7 +1367,7 @@ async function executeComposeCommand( envVars?: Record, secretVars?: Record ): Promise { - const { stackName, envId, forceRecreate, removeVolumes, stackFiles, workingDir, composePath, envPath, useOverrideFile, serviceName, composeFileName } = options; + const { stackName, envId, forceRecreate, build, pullPolicy, removeVolumes, stackFiles, workingDir, composePath, envPath, useOverrideFile, serviceName, composeFileName } = options; // Get environment configuration const env = envId ? await getEnvironment(envId) : null; @@ -1369,7 +1389,9 @@ async function executeComposeCommand( composePath, envPath, useOverrideFile, - serviceName + serviceName, + build, + pullPolicy ); } @@ -1431,7 +1453,9 @@ async function executeComposeCommand( removeVolumes, hawserStackFiles, serviceName, - composeFileName + composeFileName, + build, + pullPolicy ); } @@ -1462,7 +1486,9 @@ async function executeComposeCommand( composePath, envPath, useOverrideFile, - serviceName + serviceName, + build, + pullPolicy ); } @@ -1483,7 +1509,9 @@ async function executeComposeCommand( composePath, envPath, useOverrideFile, - serviceName + serviceName, + build, + pullPolicy ); } } @@ -1747,7 +1775,7 @@ export interface RequireComposeResult { * - envPath: Path to the .env file (Docker Compose reads non-secrets from it) * - needsFileLocation: true if stack needs user to specify file paths */ -async function requireComposeFile( +export async function requireComposeFile( stackName: string, envId?: number | null, composeConfigPath?: string @@ -1877,12 +1905,19 @@ export async function stopStack( } /** - * Restart a stack using docker compose restart - * Falls back to individual container restart for stacks without compose files + * Restart a stack using docker compose restart or stop+up (recreate mode). + * + * mode='restart' (default): Uses 'docker compose restart' — fast, in-place restart + * that preserves container IDs but won't fix stale network_mode references. + * mode='recreate': Uses 'docker compose stop' then 'docker compose up -d' — + * recreates containers, fixing network_mode: service: dependencies. + * + * Falls back to individual container restart for stacks without compose files. */ export async function restartStack( stackName: string, - envId?: number | null + envId?: number | null, + mode: 'restart' | 'recreate' = 'restart' ): Promise { const result = await requireComposeFile(stackName, envId); @@ -1891,13 +1926,17 @@ export async function restartStack( return withContainerFallback(stackName, envId, 'restart'); } - const composeResult = await executeComposeCommand( - 'restart', - { stackName, envId, workingDir: result.stackDir, composePath: result.composePath, envPath: result.envPath }, - result.content!, - result.nonSecretVars, - result.secretVars - ); + const opts: ComposeCommandOptions = { stackName, envId, workingDir: result.stackDir, composePath: result.composePath, envPath: result.envPath }; + + let composeResult: StackOperationResult; + + if (mode === 'recreate') { + // Stop first, then bring up with --force-recreate to ensure new container IDs + await executeComposeCommand('stop', opts, result.content!, result.nonSecretVars, result.secretVars); + composeResult = await executeComposeCommand('up', { ...opts, forceRecreate: true }, result.content!, result.nonSecretVars, result.secretVars); + } else { + composeResult = await executeComposeCommand('restart', opts, result.content!, result.nonSecretVars, result.secretVars); + } // Restart any dynamically-spawned child containers not in the compose file await cleanupOrphanStackContainers(stackName, envId, 'restart'); @@ -2133,7 +2172,7 @@ export async function removeStack( * Uses stack locking to prevent concurrent deployments. */ export async function deployStack(options: DeployStackOptions): Promise { - const { name, compose, envId, sourceDir, forceRecreate, composePath, envPath, composeFileName, envFileName } = options; + const { name, compose, envId, sourceDir, forceRecreate, build, pullPolicy, composePath, envPath, composeFileName, envFileName } = options; const logPrefix = `[Stack:${name}]`; console.log(`${logPrefix} ========================================`); @@ -2206,12 +2245,12 @@ export async function deployStack(options: DeployStackOptions): Promise ${workingDir}`); } else { // Internal stack: check if a custom path exists in DB (adopted/imported stacks) @@ -2275,6 +2314,8 @@ export async function deployStack(options: DeployStackOptions): Promise; + return map[containerId] ?? null; + } + } catch { /* ignore */ } + return null; +} + +/** Save user choice for a container to localStorage */ +export function saveUserForContainer(containerId: string, user: string) { + if (typeof window === 'undefined') return; + try { + const stored = localStorage.getItem(TERMINAL_USER_STORAGE_KEY); + const map = stored ? JSON.parse(stored) as Record : {}; + if (user === 'root') { + delete map[containerId]; + } else { + map[containerId] = user; + } + localStorage.setItem(TERMINAL_USER_STORAGE_KEY, JSON.stringify(map)); + } catch { /* ignore */ } + + // Also track custom users globally + const isPreset = USER_OPTIONS.some(o => o.value === user); + if (!isPreset && user) { + addCustomUser(user); + } +} + +/** Get all custom users ever used */ +export function getCustomUsers(): string[] { + if (typeof window === 'undefined') return []; + try { + const stored = localStorage.getItem(CUSTOM_USERS_STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch { return []; } +} + +/** Add a custom user to the global list */ +function addCustomUser(user: string) { + if (typeof window === 'undefined') return; + try { + const users = getCustomUsers(); + if (!users.includes(user)) { + users.push(user); + localStorage.setItem(CUSTOM_USERS_STORAGE_KEY, JSON.stringify(users)); + } + } catch { /* ignore */ } +} + +/** Remove a custom user from the global list and clear per-container references */ +export function removeCustomUser(user: string) { + if (typeof window === 'undefined') return; + try { + const users = getCustomUsers().filter(u => u !== user); + localStorage.setItem(CUSTOM_USERS_STORAGE_KEY, JSON.stringify(users)); + + // Clear per-container entries that reference this user + const stored = localStorage.getItem(TERMINAL_USER_STORAGE_KEY); + if (stored) { + const map = JSON.parse(stored) as Record; + for (const [id, u] of Object.entries(map)) { + if (u === user) delete map[id]; + } + localStorage.setItem(TERMINAL_USER_STORAGE_KEY, JSON.stringify(map)); + } + } catch { /* ignore */ } +} + export interface ShellDetectionResult { shells: string[]; defaultShell: string | null; diff --git a/src/routes/api/containers/batch-update-stream/+server.ts b/src/routes/api/containers/batch-update-stream/+server.ts index c4bf516..705d5ea 100644 --- a/src/routes/api/containers/batch-update-stream/+server.ts +++ b/src/routes/api/containers/batch-update-stream/+server.ts @@ -14,7 +14,7 @@ import { import { auditContainer } from '$lib/server/audit'; import { getScannerSettings, scanImage } from '$lib/server/scanner'; import { saveVulnerabilityScan, removePendingContainerUpdate, type VulnerabilityCriteria } from '$lib/server/db'; -import { parseImageNameAndTag, shouldBlockUpdate, combineScanSummaries, isDockhandContainer } from '$lib/server/scheduler/tasks/update-utils'; +import { parseImageNameAndTag, shouldBlockUpdate, combineScanSummaries, isSystemContainer } from '$lib/server/scheduler/tasks/update-utils'; import { recreateContainer } from '$lib/server/scheduler/tasks/container-update'; import { createJob, appendLine, completeJob, failJob } from '$lib/server/jobs'; @@ -156,8 +156,9 @@ export const POST: RequestHandler = async (event) => { const imageName = config.Image; const currentImageId = inspectData.Image; - // Skip Dockhand container - cannot update itself - if (isDockhandContainer(imageName)) { + // Skip system containers (Dockhand, Hawser) + const systemType = isSystemContainer(imageName); + if (systemType) { sendData({ type: 'progress', containerId, @@ -166,7 +167,7 @@ export const POST: RequestHandler = async (event) => { current: i + 1, total: containerIds.length, success: true, - message: `Skipping ${containerName} - cannot update Dockhand itself` + message: `Skipping ${containerName} - cannot update ${systemType} container` }); skippedCount++; continue; @@ -331,9 +332,11 @@ export const POST: RequestHandler = async (event) => { } } - // Collect vulnerabilities from all scanners (cap at 100) + // Collect vulnerabilities from all scanners (sort by severity, cap at 100) + const severityOrder: Record = { critical: 0, high: 1, medium: 2, low: 3, negligible: 4, unknown: 5 }; const vulnerabilities = scanResults .flatMap(r => r.vulnerabilities || []) + .sort((a, b) => (severityOrder[a.severity] ?? 9) - (severityOrder[b.severity] ?? 9)) .slice(0, 100) .map(v => ({ id: v.id, @@ -345,11 +348,11 @@ export const POST: RequestHandler = async (event) => { scanner: v.scanner })); - // Build scan message from individual results - const totalCritical = individualScannerResults.reduce((s, r) => s + r.critical, 0); - const totalHigh = individualScannerResults.reduce((s, r) => s + r.high, 0); - const totalMedium = individualScannerResults.reduce((s, r) => s + r.medium, 0); - const totalLow = individualScannerResults.reduce((s, r) => s + r.low, 0); + // Derive combined totals from the displayed (sliced) array so summary matches the table + const totalCritical = vulnerabilities.filter(v => v.severity === 'critical').length; + const totalHigh = vulnerabilities.filter(v => v.severity === 'high').length; + const totalMedium = vulnerabilities.filter(v => v.severity === 'medium').length; + const totalLow = vulnerabilities.filter(v => v.severity === 'low').length; const hasVulns = totalCritical + totalHigh + totalMedium + totalLow > 0; sendData({ diff --git a/src/routes/api/containers/check-updates/+server.ts b/src/routes/api/containers/check-updates/+server.ts index 94ba655..e627aa3 100644 --- a/src/routes/api/containers/check-updates/+server.ts +++ b/src/routes/api/containers/check-updates/+server.ts @@ -102,7 +102,7 @@ export const POST: RequestHandler = async ({ url, cookies, request }) => { } await Promise.all(Array.from({ length: Math.min(CONCURRENCY, containers.length) }, () => runNext())); - const updatesFound = results.filter(r => r.hasUpdate).length; + const updatesFound = results.filter(r => r.hasUpdate && !r.systemContainer).length; // Save containers with updates to the database for persistence if (envIdNum) { diff --git a/src/routes/api/git/stacks/+server.ts b/src/routes/api/git/stacks/+server.ts index 9429d68..35574fe 100644 --- a/src/routes/api/git/stacks/+server.ts +++ b/src/routes/api/git/stacks/+server.ts @@ -119,7 +119,10 @@ export const POST: RequestHandler = async (event) => { autoUpdateSchedule: data.autoUpdateSchedule || 'daily', autoUpdateCron: data.autoUpdateCron || '0 3 * * *', webhookEnabled: data.webhookEnabled || false, - webhookSecret: webhookSecret + webhookSecret: webhookSecret, + buildOnDeploy: data.buildOnDeploy ?? false, + repullImages: data.repullImages ?? false, + forceRedeploy: data.forceRedeploy ?? false }); // Create stack_sources entry so the stack appears in the list immediately diff --git a/src/routes/api/git/stacks/[id]/+server.ts b/src/routes/api/git/stacks/[id]/+server.ts index 8833d53..45223a5 100644 --- a/src/routes/api/git/stacks/[id]/+server.ts +++ b/src/routes/api/git/stacks/[id]/+server.ts @@ -72,7 +72,10 @@ export const PUT: RequestHandler = async (event) => { autoUpdateSchedule: data.autoUpdateSchedule, autoUpdateCron: data.autoUpdateCron, webhookEnabled: data.webhookEnabled, - webhookSecret: data.webhookSecret + webhookSecret: data.webhookSecret, + buildOnDeploy: data.buildOnDeploy, + repullImages: data.repullImages, + forceRedeploy: data.forceRedeploy }); // If stack name changed, update related records diff --git a/src/routes/api/registry/image/+server.ts b/src/routes/api/registry/image/+server.ts index 304f555..98ae38f 100644 --- a/src/routes/api/registry/image/+server.ts +++ b/src/routes/api/registry/image/+server.ts @@ -10,6 +10,14 @@ function isDockerHub(url: string): boolean { lower.includes('registry.hub.docker.com'); } +// Manifest types in priority order: single-platform first, then multi-arch +const MANIFEST_TYPES = [ + 'application/vnd.docker.distribution.manifest.v2+json', + 'application/vnd.oci.image.manifest.v1+json', + 'application/vnd.docker.distribution.manifest.list.v2+json', + 'application/vnd.oci.image.index.v1+json' +]; + export const DELETE: RequestHandler = async ({ url }) => { try { const registryId = url.searchParams.get('registry'); @@ -39,43 +47,41 @@ export const DELETE: RequestHandler = async ({ url }) => { } const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull,push,delete`); - // Note: orgPath is not used here because imageName already contains the full repo path - const headers: HeadersInit = { - 'Accept': 'application/vnd.docker.distribution.manifest.v2+json' - }; - - if (authHeader) { - headers['Authorization'] = authHeader; - } - - // Step 1: Get the manifest digest + // Step 1: Resolve manifest digest. Try each type individually because + // the registry may only serve certain types, and DELETE requires the + // Accept header to match the stored manifest type. const manifestUrl = `${baseUrl}/v2/${imageName}/manifests/${tag}`; - const headResponse = await fetch(manifestUrl, { - method: 'HEAD', - headers - }); + let digest: string | null = null; + let matchedType: string | null = null; - if (!headResponse.ok) { + for (const mediaType of MANIFEST_TYPES) { + const headers: HeadersInit = { 'Accept': mediaType }; + if (authHeader) headers['Authorization'] = authHeader; + + const headResponse = await fetch(manifestUrl, { method: 'HEAD', headers }); + if (headResponse.ok) { + digest = headResponse.headers.get('Docker-Content-Digest'); + matchedType = mediaType; + break; + } if (headResponse.status === 401) { return json({ error: 'Authentication failed' }, { status: 401 }); } - if (headResponse.status === 404) { - return json({ error: 'Image or tag not found' }, { status: 404 }); - } - return json({ error: `Failed to get manifest: ${headResponse.status}` }, { status: headResponse.status }); } - const digest = headResponse.headers.get('Docker-Content-Digest'); - if (!digest) { - return json({ error: 'Could not get image digest. Registry may not support deletion.' }, { status: 400 }); + if (!digest || !matchedType) { + return json({ error: 'Image or tag not found' }, { status: 404 }); } - // Step 2: Delete the manifest by digest + // Step 2: Delete the manifest by digest using the matched type + const deleteHeaders: HeadersInit = { 'Accept': matchedType }; + if (authHeader) deleteHeaders['Authorization'] = authHeader; + const deleteUrl = `${baseUrl}/v2/${imageName}/manifests/${digest}`; const deleteResponse = await fetch(deleteUrl, { method: 'DELETE', - headers + headers: deleteHeaders }); if (!deleteResponse.ok) { diff --git a/src/routes/api/settings/scanner/+server.ts b/src/routes/api/settings/scanner/+server.ts index 7f29ad9..8e7ece4 100644 --- a/src/routes/api/settings/scanner/+server.ts +++ b/src/routes/api/settings/scanner/+server.ts @@ -4,7 +4,7 @@ import { checkScannerAvailability, getScannerVersions, checkScannerUpdates, - cleanupScannerVolumes, + cleanupScannerCache, getGlobalScannerDefaults, type ScannerType } from '$lib/server/scanner'; @@ -195,8 +195,8 @@ export const DELETE: RequestHandler = async ({ url, cookies }) => { } } - // Also cleanup scanner database volumes - await cleanupScannerVolumes(parsedEnvId); + // Also cleanup scanner database cache (volumes + bind mount dirs) + await cleanupScannerCache(parsedEnvId); return json({ success: true, diff --git a/src/routes/api/settings/scanner/cache/+server.ts b/src/routes/api/settings/scanner/cache/+server.ts new file mode 100644 index 0000000..d609222 --- /dev/null +++ b/src/routes/api/settings/scanner/cache/+server.ts @@ -0,0 +1,40 @@ +import { json, type RequestHandler } from '@sveltejs/kit'; +import { cleanupScannerCache } from '$lib/server/scanner'; +import { authorize } from '$lib/server/authorize'; +import { getEnvironments } from '$lib/server/db'; + +export const DELETE: RequestHandler = async ({ cookies }) => { + const auth = await authorize(cookies); + + if (auth.authEnabled && !await auth.can('settings', 'edit')) { + return json({ error: 'Permission denied' }, { status: 403 }); + } + + try { + const envs = await getEnvironments(); + + // Clean local cache (volumes + bind mount dirs) + const result = await cleanupScannerCache(); + + // Clean remote environment volumes + const skippedEnvs: string[] = []; + for (const env of envs) { + try { + const envResult = await cleanupScannerCache(env.id); + result.volumes.push(...envResult.volumes); + } catch { + skippedEnvs.push(env.name); + } + } + + return json({ + success: true, + removedVolumes: result.volumes, + removedDirs: result.dirs, + skippedEnvironments: skippedEnvs + }); + } catch (error) { + console.error('Failed to clear scanner cache:', error); + return json({ error: 'Failed to clear scanner cache' }, { status: 500 }); + } +}; diff --git a/src/routes/api/stacks/[name]/deploy/+server.ts b/src/routes/api/stacks/[name]/deploy/+server.ts new file mode 100644 index 0000000..1fcdd7b --- /dev/null +++ b/src/routes/api/stacks/[name]/deploy/+server.ts @@ -0,0 +1,80 @@ +import { json } from '@sveltejs/kit'; +import { deployStack, requireComposeFile, ComposeFileNotFoundError } from '$lib/server/stacks'; +import { authorize } from '$lib/server/authorize'; +import { auditStack } from '$lib/server/audit'; +import { createJobResponse } from '$lib/server/sse'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async (event) => { + const { params, url, cookies, request } = event; + const auth = await authorize(cookies); + + const envId = url.searchParams.get('env'); + const envIdNum = envId ? parseInt(envId) : undefined; + + // Permission check with environment context + if (auth.authEnabled && !(await auth.can('stacks', 'start', envIdNum))) { + return json({ error: 'Permission denied' }, { status: 403 }); + } + + // Environment access check (enterprise only) + if (envIdNum && auth.isEnterprise && !(await auth.canAccessEnvironment(envIdNum))) { + return json({ error: 'Access denied to this environment' }, { status: 403 }); + } + + const body = await request.json(); + const { pull, build, forceRecreate } = body as { + pull?: boolean; + build?: boolean; + forceRecreate?: boolean; + }; + + return createJobResponse(async (send) => { + try { + const stackName = decodeURIComponent(params.name); + + send('progress', { status: 'Reading compose file...' }); + const composeResult = await requireComposeFile(stackName, envIdNum); + + if (!composeResult.success) { + send('result', { + success: false, + error: composeResult.needsFileLocation + ? 'Stack compose file location not configured' + : composeResult.error || 'Compose file not found' + }); + return; + } + + send('progress', { status: 'Deploying stack...' }); + const result = await deployStack({ + name: stackName, + compose: composeResult.content!, + envId: envIdNum, + pullPolicy: pull ? 'always' : undefined, + build, + forceRecreate, + composePath: composeResult.composePath, + envPath: composeResult.envPath + }); + + // Audit log + await auditStack(event, 'deploy', stackName, envIdNum, { + pull, build, forceRecreate + }); + + if (!result.success) { + send('result', { success: false, error: result.error }); + return; + } + send('result', { success: true, output: result.output }); + } catch (error) { + if (error instanceof ComposeFileNotFoundError) { + send('result', { success: false, error: error.message }); + return; + } + console.error('Error deploying compose stack:', error); + send('result', { success: false, error: 'Failed to deploy compose stack' }); + } + }, event.request); +}; diff --git a/src/routes/api/stacks/[name]/restart/+server.ts b/src/routes/api/stacks/[name]/restart/+server.ts index 90ccc6f..893c198 100644 --- a/src/routes/api/stacks/[name]/restart/+server.ts +++ b/src/routes/api/stacks/[name]/restart/+server.ts @@ -22,10 +22,12 @@ export const POST: RequestHandler = async (event) => { return json({ error: 'Access denied to this environment' }, { status: 403 }); } + const mode = url.searchParams.get('mode') === 'recreate' ? 'recreate' : 'restart'; + return createJobResponse(async (send) => { try { const stackName = decodeURIComponent(params.name); - const result = await restartStack(stackName, envIdNum); + const result = await restartStack(stackName, envIdNum, mode); // Audit log await auditStack(event, 'restart', stackName, envIdNum); diff --git a/src/routes/containers/+page.svelte b/src/routes/containers/+page.svelte index c8fd2a4..be2d08a 100644 --- a/src/routes/containers/+page.svelte +++ b/src/routes/containers/+page.svelte @@ -84,7 +84,7 @@ import { watchJob } from '$lib/utils/sse-fetch'; import { ipToNumber } from '$lib/utils/ip'; import { formatHostPortUrl } from '$lib/utils/url'; - import { detectShells, getBestShell, hasAvailableShell, USER_OPTIONS, type ShellDetectionResult } from '$lib/utils/shell-detection'; + import { detectShells, getBestShell, hasAvailableShell, USER_OPTIONS, getSavedUser, saveUserForContainer, getCustomUsers, removeCustomUser, type ShellDetectionResult } from '$lib/utils/shell-detection'; import { DataGrid } from '$lib/components/data-grid'; import type { ColumnConfig } from '$lib/types'; import type { DataGridRowState } from '$lib/components/data-grid/types'; @@ -243,6 +243,8 @@ let terminalPopoverStates = $state>({}); let terminalShell = $state('/bin/bash'); let terminalUser = $state('root'); + let terminalCustomUser = $state(''); + let terminalCustomUsers = $state([]); // Confirmation popover state let confirmStopId = $state(null); @@ -470,7 +472,7 @@ // Unlock button width if (updateCheckBtnEl) updateCheckBtnEl.style.minWidth = ''; - const containersWithUpdates = data.results.filter((r: any) => r.hasUpdate); + const containersWithUpdates = data.results.filter((r: any) => r.hasUpdate && !r.systemContainer); const failed = data.results.filter((r: any) => r.error && !r.hasUpdate); failedUpdateChecks = failed.map((r: any) => ({ containerName: r.containerName, @@ -1041,13 +1043,18 @@ currentTerminalContainerId = container.id; terminalPopoverStates[container.id] = false; } else { + // Restore saved user for this container + const savedUser = getSavedUser(container.id); + terminalUser = savedUser ?? 'root'; + terminalCustomUsers = getCustomUsers(); // Show popover to configure new terminal terminalPopoverStates[container.id] = true; } } function startTerminal(container: ContainerInfo) { - // Create new terminal session + saveUserForContainer(container.id, terminalUser); + terminalCustomUsers = getCustomUsers(); const terminal: ActiveTerminal = { containerId: container.id, containerName: container.name, @@ -1336,6 +1343,7 @@ onMount(async () => { loadLayoutMode(); loadStatusFilter(); + terminalCustomUsers = getCustomUsers(); // Load persisted pending updates from database loadPendingUpdates(); @@ -2138,7 +2146,7 @@ - {userOptions.find(o => o.value === terminalUser)?.label || 'Select'} + {userOptions.find(o => o.value === terminalUser)?.label || terminalUser || 'Select'} {#each userOptions as option} @@ -2147,6 +2155,35 @@ {option.label} {/each} + {#if terminalCustomUsers.length > 0} +
+ {#each terminalCustomUsers as cu} +
+ + + {cu} + + +
+ {/each} + {/if} +
+
+ { e.stopPropagation(); if (e.key === 'Enter' && terminalCustomUser.trim()) { const u = terminalCustomUser.trim(); terminalUser = u; saveUserForContainer(container.id, u); terminalCustomUsers = getCustomUsers(); terminalCustomUser = ''; } }} + onclick={(e) => e.stopPropagation()} + /> +
diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 3d3d91a..d325524 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -189,7 +189,7 @@ {#each oidcProviders as provider} + + diff --git a/src/routes/settings/notifications/NotificationsTab.svelte b/src/routes/settings/notifications/NotificationsTab.svelte index 6c02679..fafc222 100644 --- a/src/routes/settings/notifications/NotificationsTab.svelte +++ b/src/routes/settings/notifications/NotificationsTab.svelte @@ -220,7 +220,7 @@ {/if} -
+
{/if} + {#if source.sourceType !== 'git' && source.sourceType !== 'external' && $canAccess('stacks', 'start')} + redeployStack(stack.name, options)} + > + {#snippet children()} + + {/snippet} + + {/if} {#if stackActionLoading === stack.name}
{:else if stack.status === 'running' || stack.status === 'partial' || stack.status === 'restarting'} {#if $canAccess('stacks', 'restart')} - restartStack(stack.name)} - onOpenChange={() => {}} - > - {#snippet children({ open })} - - {/snippet} - + restartPopoverOpen[stack.name] = v}> + + {#snippet child({ props })} + + {/snippet} + + +
+ Restart stack {stack.name.length > 20 ? stack.name.slice(0, 20) + '...' : stack.name} +
+ + +
+
+
+
{/if} {#if $canAccess('stacks', 'stop')} ({ key: v.key.trim(), @@ -880,6 +895,41 @@ {/if}
+ +
+

Deploy options

+
+
+ + +
+ +
+

+ Run --build to build images from Dockerfiles before starting containers. +

+
+
+ + +
+ +
+

+ Always pull latest images before deploying, even if the compose file hasn't changed. Useful for CI/CD workflows with static tags like :latest. +

+
+
+ + +
+ +
+

+ Always redeploy the stack on webhook or scheduled sync, even if no git changes are detected. +

+
+ {#if !gitStack}
diff --git a/src/routes/stacks/ImportStackModal.svelte b/src/routes/stacks/ImportStackModal.svelte index 94fca4f..85a779d 100644 --- a/src/routes/stacks/ImportStackModal.svelte +++ b/src/routes/stacks/ImportStackModal.svelte @@ -3,7 +3,7 @@ import { Button } from '$lib/components/ui/button'; import { Badge } from '$lib/components/ui/badge'; import { Checkbox } from '$lib/components/ui/checkbox'; - import { Import, Loader2, Play, Info } from 'lucide-svelte'; + import { Import, Loader2, Play, Info, ServerCog } from 'lucide-svelte'; import FilesystemBrowser, { type FileEntry } from './FilesystemBrowser.svelte'; import CodeEditor from '$lib/components/CodeEditor.svelte'; import yaml from 'js-yaml'; @@ -56,6 +56,11 @@ // Look up the icon from the environments list since currentEnvironment doesn't store it const currentEnvData = $derived($environments.find(e => e.id === envId)); const envIcon = $derived(currentEnvData?.icon || 'globe'); + const isRemoteEnv = $derived( + currentEnvData?.connectionType === 'hawser-standard' || + currentEnvData?.connectionType === 'hawser-edge' || + (currentEnvData?.connectionType === 'direct' && !!currentEnvData?.host) + ); // Reset when modal closes $effect(() => { @@ -299,9 +304,16 @@ // Browser title with environment info const browserTitle = $derived.by(() => { - const envPart = envName ? ` · ${envName}` : ''; + const envPart = envName ? ` to ${envName}` : ''; return `Adopt stacks${envPart}`; }); + + const browserDescription = $derived.by(() => { + if (isRemoteEnv) { + return `Browse the Dockhand host filesystem to find compose files. Files are managed locally — Hawser only proxies Docker API calls, not filesystem access.`; + } + return 'Browse to a compose file or scan a directory for stacks.'; + }); {#if view === 'browse'} @@ -311,7 +323,7 @@ bind:open title={browserTitle} icon={Import} - description="Browse to a compose file or scan a directory for stacks." + description={browserDescription} selectMode="adopt" highlightFilter={/\.ya?ml$/i} onFilePreview={handleFilePreview} @@ -327,8 +339,7 @@ - Select stacks to adopt - · + Select stacks to adopt to {envName} @@ -381,7 +392,13 @@
-
+
+ {#if isRemoteEnv} +
+ + These compose files are on the Dockhand host, not on {envName}. Docker commands will be sent to {envName} via Hawser, but the files are managed locally. +
+ {/if}
What happens when you adopt: Dockhand will track these compose files, letting you edit, start, and stop the stacks from the UI. Your files stay in their current location. @@ -475,7 +492,13 @@
-
+
+ {#if isRemoteEnv} +
+ + This compose file is on the Dockhand host, not on {envName}. Docker commands will be sent to {envName} via Hawser, but the file is managed locally. +
+ {/if}
What happens when you adopt: Dockhand will track this compose file, letting you edit, start, and stop the stack from the UI. Your files stay in their current location. diff --git a/src/routes/stacks/RedeployPopover.svelte b/src/routes/stacks/RedeployPopover.svelte new file mode 100644 index 0000000..dc652fe --- /dev/null +++ b/src/routes/stacks/RedeployPopover.svelte @@ -0,0 +1,99 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + +
+

Redeploy stack

+
+ + + +
+ +
+
+
diff --git a/src/routes/terminal/+page.svelte b/src/routes/terminal/+page.svelte index 2ca1cf9..046b02b 100644 --- a/src/routes/terminal/+page.svelte +++ b/src/routes/terminal/+page.svelte @@ -15,7 +15,7 @@ import { currentEnvironment, environments, appendEnvParam } from '$lib/stores/environment'; import Terminal from './Terminal.svelte'; import { NoEnvironment } from '$lib/components/ui/empty-state'; - import { detectShells, getBestShell, hasAvailableShell, USER_OPTIONS, type ShellInfo, type ShellDetectionResult } from '$lib/utils/shell-detection'; + import { detectShells, getBestShell, hasAvailableShell, USER_OPTIONS, getSavedUser, saveUserForContainer, getCustomUsers, removeCustomUser, type ShellInfo, type ShellDetectionResult } from '$lib/utils/shell-detection'; // Track if we've handled the initial container from URL let initialContainerHandled = $state(false); @@ -35,6 +35,8 @@ // Shell/user options let selectedShell = $state('/bin/bash'); let selectedUser = $state('root'); + let customUserInput = $state(''); + let customUsers = $state([]); let terminalFontSize = $state(14); // Track previous shell/user for reconnection @@ -115,6 +117,16 @@ if (bestShell && bestShell !== selectedShell) { selectedShell = bestShell; } + + // Restore saved user for this container + const savedUser = getSavedUser(container.id); + if (savedUser !== null) { + selectedUser = savedUser; + committedUser = savedUser; + } else { + selectedUser = 'root'; + committedUser = 'root'; + } } catch (error) { console.error('Failed to detect shells:', error); } finally { @@ -151,16 +163,42 @@ } } + // Committed user: only updates when a preset is selected or custom input is confirmed + let committedUser = $state('root'); + + function commitUser(user: string) { + committedUser = user; + if (selectedContainer) { + saveUserForContainer(selectedContainer.id, user); + customUsers = getCustomUsers(); + } + } + + // When a user is selected from dropdown, commit immediately + function onUserSelectChange(value: string) { + commitUser(value); + } + + // Confirm custom user on Enter + function onCustomUserKeydown(e: KeyboardEvent) { + e.stopPropagation(); + if (e.key === 'Enter' && customUserInput.trim()) { + const newUser = customUserInput.trim(); + selectedUser = newUser; + commitUser(newUser); + customUserInput = ''; + } + } + // Watch for shell/user changes while connected and trigger reconnect $effect(() => { if (selectedContainer && connected && terminalComponent) { - if (selectedShell !== prevShell || selectedUser !== prevUser) { - // Reconnect with new shell/user + if (selectedShell !== prevShell || committedUser !== prevUser) { terminalComponent.reconnect(); } } prevShell = selectedShell; - prevUser = selectedUser; + prevUser = committedUser; }); // Change font size @@ -175,6 +213,7 @@ } onMount(async () => { + customUsers = getCustomUsers(); await fetchContainers(); // Check for container ID in URL query parameter @@ -336,10 +375,10 @@
- + - {USER_OPTIONS.find(o => o.value === selectedUser)?.label || 'Select'} + {USER_OPTIONS.find(o => o.value === selectedUser)?.label || selectedUser || 'Select'} {#each USER_OPTIONS as option} @@ -348,6 +387,36 @@ {option.label} {/each} + {#if customUsers.length > 0} +
+ {#each customUsers as cu} +
+ + + {cu} + + +
+ {/each} + {/if} +
+ +
+ e.stopPropagation()} + /> +
@@ -428,13 +497,13 @@
- {#key `${selectedContainer.id}-${selectedShell}-${selectedUser}`} + {#key `${selectedContainer.id}-${selectedShell}-${committedUser}`}