From ba8670af66765b33ad73cf69f21ab55e975368af Mon Sep 17 00:00:00 2001 From: jarek Date: Mon, 17 Aug 2026 09:50:34 +0200 Subject: [PATCH] detect newer version tags for pinned images and show them as a badge with release notes --- drizzle-pg/0011_pending_updates_semver.sql | 2 + drizzle-pg/meta/0011_snapshot.json | 3483 ++++++++++++++++ drizzle-pg/meta/_journal.json | 7 + drizzle/0011_pending_updates_semver.sql | 2 + drizzle/meta/0011_snapshot.json | 3642 +++++++++++++++++ drizzle/meta/_journal.json | 7 + src/lib/components/CheckUpdatesButton.svelte | 37 +- src/lib/components/SemverCheckConfig.svelte | 135 + src/lib/components/VersionUpdateBadge.svelte | 61 + src/lib/components/VersionUpdateModal.svelte | 272 ++ src/lib/server/db.ts | 24 +- src/lib/server/db/schema/index.ts | 6 + src/lib/server/db/schema/pg-schema.ts | 6 + src/lib/server/docker.ts | 59 +- src/lib/server/registry/image-ref.ts | 75 + .../scheduler/tasks/env-update-check.ts | 86 +- src/lib/server/semver/check.ts | 27 + src/lib/server/semver/find-newer.ts | 117 + src/lib/server/semver/release-notes.ts | 107 + src/lib/server/semver/release-source.ts | 109 + .../server/semver/strategies/docker-hub.ts | 42 + .../server/semver/strategies/generic-v2.ts | 47 + src/lib/server/semver/tag-parser.ts | 95 + src/lib/server/semver/tag-source.ts | 76 + src/lib/server/stacks.ts | 25 +- src/lib/stores/containers.ts | 31 +- src/lib/types.ts | 11 + src/lib/utils/markdown.ts | 15 + .../containers/[id]/version-notes/+server.ts | 58 + .../api/containers/check-updates/+server.ts | 63 +- .../api/containers/pending-updates/+server.ts | 30 +- .../environments/[id]/update-check/+server.ts | 25 +- src/routes/containers/+page.svelte | 56 +- .../environments/EnvironmentModal.svelte | 24 +- .../environments/tabs/UpdatesTab.svelte | 17 + src/routes/stacks/+page.svelte | 39 +- 36 files changed, 8820 insertions(+), 98 deletions(-) create mode 100644 drizzle-pg/0011_pending_updates_semver.sql create mode 100644 drizzle-pg/meta/0011_snapshot.json create mode 100644 drizzle/0011_pending_updates_semver.sql create mode 100644 drizzle/meta/0011_snapshot.json create mode 100644 src/lib/components/SemverCheckConfig.svelte create mode 100644 src/lib/components/VersionUpdateBadge.svelte create mode 100644 src/lib/components/VersionUpdateModal.svelte create mode 100644 src/lib/server/registry/image-ref.ts create mode 100644 src/lib/server/semver/check.ts create mode 100644 src/lib/server/semver/find-newer.ts create mode 100644 src/lib/server/semver/release-notes.ts create mode 100644 src/lib/server/semver/release-source.ts create mode 100644 src/lib/server/semver/strategies/docker-hub.ts create mode 100644 src/lib/server/semver/strategies/generic-v2.ts create mode 100644 src/lib/server/semver/tag-parser.ts create mode 100644 src/lib/server/semver/tag-source.ts create mode 100644 src/lib/utils/markdown.ts create mode 100644 src/routes/api/containers/[id]/version-notes/+server.ts diff --git a/drizzle-pg/0011_pending_updates_semver.sql b/drizzle-pg/0011_pending_updates_semver.sql new file mode 100644 index 0000000..d972fe9 --- /dev/null +++ b/drizzle-pg/0011_pending_updates_semver.sql @@ -0,0 +1,2 @@ +ALTER TABLE "pending_container_updates" ADD COLUMN "has_image_update" boolean DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE "pending_container_updates" ADD COLUMN "newer_version" text; \ No newline at end of file diff --git a/drizzle-pg/meta/0011_snapshot.json b/drizzle-pg/meta/0011_snapshot.json new file mode 100644 index 0000000..ac6993b --- /dev/null +++ b/drizzle-pg/meta/0011_snapshot.json @@ -0,0 +1,3483 @@ +{ + "id": "5f9fe4e6-7799-406c-a232-c72726c6db27", + "prevId": "670ff7d7-5e55-4d96-a6d8-c9d2e3c765b4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_tokens": { + "name": "api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "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": { + "api_tokens_user_id_idx": { + "name": "api_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_tokens_token_prefix_idx": { + "name": "api_tokens_token_prefix_idx", + "columns": [ + { + "expression": "token_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_tokens_user_id_users_id_fk": { + "name": "api_tokens_user_id_users_id_fk", + "tableFrom": "api_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_tokens_token_hash_unique": { + "name": "api_tokens_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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.backup_configs": { + "name": "backup_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'container'" + }, + "target_name": { + "name": "target_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "all_volumes": { + "name": "all_volumes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "selected_volumes": { + "name": "selected_volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stop_before_backup": { + "name": "stop_before_backup", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retention": { + "name": "retention", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_backup_status": { + "name": "last_backup_status", + "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": { + "backup_configs_environment_id_environments_id_fk": { + "name": "backup_configs_environment_id_environments_id_fk", + "tableFrom": "backup_configs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_configs_destination_id_backup_destinations_id_fk": { + "name": "backup_configs_destination_id_backup_destinations_id_fk", + "tableFrom": "backup_configs", + "tableTo": "backup_destinations", + "columnsFrom": [ + "destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup_destinations": { + "name": "backup_destinations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "flags": { + "name": "flags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_path": { + "name": "host_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policies": { + "name": "policies", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_test_at": { + "name": "last_test_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_test_status": { + "name": "last_test_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_test_error": { + "name": "last_test_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_destinations_name_unique": { + "name": "backup_destinations_name_unique", + "nullsNotDistinct": false, + "columns": [ + "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 + }, + "context_dir": { + "name": "context_dir", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_on_deploy": { + "name": "build_on_deploy", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "no_build_cache": { + "name": "no_build_cache", + "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 + }, + "synced_files": { + "name": "synced_files", + "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 + }, + "has_image_update": { + "name": "has_image_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "newer_version": { + "name": "newer_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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.secret_providers": { + "name": "secret_providers", + "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 + }, + "config": { + "name": "config", + "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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "secret_providers_name_unique": { + "name": "secret_providers_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "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 + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_path": { + "name": "env_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provider_id": { + "name": "secret_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "injected_secret_keys": { + "name": "injected_secret_keys", + "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" + }, + "stack_sources_secret_provider_id_secret_providers_id_fk": { + "name": "stack_sources_secret_provider_id_secret_providers_id_fk", + "tableFrom": "stack_sources", + "tableTo": "secret_providers", + "columnsFrom": [ + "secret_provider_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.template_sources": { + "name": "template_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "builtin": { + "name": "builtin", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "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": { + "template_sources_source_id_unique": { + "name": "template_sources_source_id_unique", + "nullsNotDistinct": false, + "columns": [ + "source_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 15d638d..f66840b 100644 --- a/drizzle-pg/meta/_journal.json +++ b/drizzle-pg/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1783800723008, "tag": "0010_add_secret_providers", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1786892847678, + "tag": "0011_pending_updates_semver", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/0011_pending_updates_semver.sql b/drizzle/0011_pending_updates_semver.sql new file mode 100644 index 0000000..490d7b0 --- /dev/null +++ b/drizzle/0011_pending_updates_semver.sql @@ -0,0 +1,2 @@ +ALTER TABLE `pending_container_updates` ADD `has_image_update` integer DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE `pending_container_updates` ADD `newer_version` text; \ No newline at end of file diff --git a/drizzle/meta/0011_snapshot.json b/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..05497d0 --- /dev/null +++ b/drizzle/meta/0011_snapshot.json @@ -0,0 +1,3642 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "c0095b38-49f2-46e5-bca7-68643b496a28", + "prevId": "1589f271-4584-4d22-a768-05189bb2d564", + "tables": { + "api_tokens": { + "name": "api_tokens", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "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": { + "api_tokens_token_hash_unique": { + "name": "api_tokens_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "api_tokens_user_id_idx": { + "name": "api_tokens_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "api_tokens_token_prefix_idx": { + "name": "api_tokens_token_prefix_idx", + "columns": [ + "token_prefix" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_tokens_user_id_users_id_fk": { + "name": "api_tokens_user_id_users_id_fk", + "tableFrom": "api_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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": {} + }, + "backup_configs": { + "name": "backup_configs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'container'" + }, + "target_name": { + "name": "target_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "all_volumes": { + "name": "all_volumes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "selected_volumes": { + "name": "selected_volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stop_before_backup": { + "name": "stop_before_backup", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retention": { + "name": "retention", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "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": { + "backup_configs_environment_id_environments_id_fk": { + "name": "backup_configs_environment_id_environments_id_fk", + "tableFrom": "backup_configs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_configs_destination_id_backup_destinations_id_fk": { + "name": "backup_configs_destination_id_backup_destinations_id_fk", + "tableFrom": "backup_configs", + "tableTo": "backup_destinations", + "columnsFrom": [ + "destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_destinations": { + "name": "backup_destinations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "flags": { + "name": "flags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_path": { + "name": "host_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "policies": { + "name": "policies", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_test_at": { + "name": "last_test_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_test_status": { + "name": "last_test_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_test_error": { + "name": "last_test_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": { + "backup_destinations_name_unique": { + "name": "backup_destinations_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "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": "'docker-compose.yml'" + }, + "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": "'docker-compose.yml'" + }, + "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 + }, + "context_dir": { + "name": "context_dir", + "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 + }, + "no_build_cache": { + "name": "no_build_cache", + "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 + }, + "synced_files": { + "name": "synced_files", + "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 + }, + "has_image_update": { + "name": "has_image_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "newer_version": { + "name": "newer_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "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": {} + }, + "secret_providers": { + "name": "secret_providers", + "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 + }, + "config": { + "name": "config", + "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": { + "secret_providers_name_unique": { + "name": "secret_providers_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "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 + }, + "secret_provider_id": { + "name": "secret_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "injected_secret_keys": { + "name": "injected_secret_keys", + "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" + }, + "stack_sources_secret_provider_id_secret_providers_id_fk": { + "name": "stack_sources_secret_provider_id_secret_providers_id_fk", + "tableFrom": "stack_sources", + "tableTo": "secret_providers", + "columnsFrom": [ + "secret_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "template_sources": { + "name": "template_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "builtin": { + "name": "builtin", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "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": { + "template_sources_source_id_unique": { + "name": "template_sources_source_id_unique", + "columns": [ + "source_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "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 5793388..f84c52f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1783800723008, "tag": "0010_add_secret_providers", "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1786892843482, + "tag": "0011_pending_updates_semver", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/components/CheckUpdatesButton.svelte b/src/lib/components/CheckUpdatesButton.svelte index 51a2cee..09f849e 100644 --- a/src/lib/components/CheckUpdatesButton.svelte +++ b/src/lib/components/CheckUpdatesButton.svelte @@ -5,6 +5,7 @@ import { toast } from 'svelte-sonner'; import { appendEnvParam } from '$lib/stores/environment'; import { watchJob } from '$lib/utils/sse-fetch'; + import type { NewerVersion } from '$lib/types'; export interface UpdateCheckResultItem { containerId: string; @@ -12,6 +13,12 @@ imageName: string; } + /** A container that has a newer VERSION tag (semver) — independent of digest updates. */ + export interface NewerVersionItem { + containerId: string; + newerVersion: NewerVersion; + } + export interface FailedCheckItem { containerId: string; containerName: string; @@ -24,7 +31,11 @@ /** When true and no check is running, the button reflects the "updates found" (green) state — e.g. from persisted pending updates loaded on mount. */ hasPendingUpdates?: boolean; /** Called when a check finishes with the containers that have updates and any failures. */ - onComplete?: (result: { withUpdates: UpdateCheckResultItem[]; failed: FailedCheckItem[] }) => void; + onComplete?: (result: { + withUpdates: UpdateCheckResultItem[]; + failed: FailedCheckItem[]; + newerVersions: NewerVersionItem[]; + }) => void; } let { envId, hasPendingUpdates = false, onComplete }: Props = $props(); @@ -119,8 +130,19 @@ const failed: FailedCheckItem[] = data.results .filter((r: any) => r.error && !r.hasUpdate) .map((r: any) => ({ containerId: r.containerId, containerName: r.containerName, imageName: r.imageName, error: r.error })); + // Semver suggestions are independent of digest updates: a container can be + // digest-current yet run an outdated version tag. + const newerVersions: NewerVersionItem[] = data.results + .filter((r: any) => r.newerVersion) + .map((r: any) => ({ containerId: r.containerId, newerVersion: r.newerVersion })); - if (withUpdates.length === 0) { + // A newer version tag is a real result even when no digest update exists, + // so it counts toward the "found" state and the summary message. + const semverNote = newerVersions.length > 0 + ? `${newerVersions.length} newer version tag${newerVersions.length !== 1 ? 's' : ''}` + : ''; + + if (withUpdates.length === 0 && newerVersions.length === 0) { // Keep the "Latest" status until re-check / env-switch — don't auto-revert (#1019) status = 'none'; if (failed.length > 0) { @@ -130,14 +152,19 @@ } } else { status = 'found'; + const parts = [ + withUpdates.length > 0 ? `${withUpdates.length} update${withUpdates.length !== 1 ? 's' : ''} available` : '', + semverNote + ].filter(Boolean); + const summary = parts.join(', '); if (failed.length > 0) { - showFailedChecksToast(failed, `${withUpdates.length} update(s) available`); + showFailedChecksToast(failed, summary); } else { - toast.info(`${withUpdates.length} update(s) available`); + toast.info(summary); } } - onComplete?.({ withUpdates, failed }); + onComplete?.({ withUpdates, failed, newerVersions }); } catch { failError(); } diff --git a/src/lib/components/SemverCheckConfig.svelte b/src/lib/components/SemverCheckConfig.svelte new file mode 100644 index 0000000..f953d7c --- /dev/null +++ b/src/lib/components/SemverCheckConfig.svelte @@ -0,0 +1,135 @@ + + +
+
+ +
+ +

+ For a container pinned to a version tag (e.g. 16.2, v3.0), also detect + when a newer version is published. Advisory only. +

+
+ +
+ + {#if isFloating} +
+ + {floatingTag} is a floating tag — pin a version tag (e.g. 1.26) to enable newer-version detection. +
+ {:else if enabled} + +
+ +
+
+ +
+ {#each bumps as bump} + + {/each} +
+
+
+ Running 1.4.2, you'll be told about:
+ Patch bug-fix only → 1.4.3
+ Minor new features too → 1.4.3, 1.5.0
+ Major everything, incl. breaking → 1.4.3, 1.5.0, 2.0.0 +
+
+ + +
+
+
+ +

+ Only suggest tags with the same suffix as the one you run — the flavor, like + -alpine or -ls123. +

+
+
+
+
+ On 1.2-alpine → 1.5-alpine, never bare 1.5
+ Off any newer version, even a different flavor — noisier +
+
+ + +
+
+ +

+ Consider -rc / -beta tags. Off keeps a stable deployment on + stable releases. +

+
+
+
+
+ {/if} + + {#if enabled && !isFloating} +
+ + A newer version shows as a badge on the container — it is never auto-applied. Bump the tag in your compose yourself. +
+ {/if} +
+ + diff --git a/src/lib/components/VersionUpdateBadge.svelte b/src/lib/components/VersionUpdateBadge.svelte new file mode 100644 index 0000000..32a05ae --- /dev/null +++ b/src/lib/components/VersionUpdateBadge.svelte @@ -0,0 +1,61 @@ + + + + { + e.stopPropagation(); + onclick?.(); + }} + class="shrink-0 inline-flex items-center gap-1 self-center text-amber-500 hover:text-amber-400 transition-colors cursor-pointer + {variant === 'pill' + ? 'rounded-full bg-amber-500/10 hover:bg-amber-500/20 px-1.5 py-0.5 text-2xs font-medium' + : ''}" + > + + {#if variant === 'pill'} + {newerVersion.tag} + {/if} + + +
+

+ + Newer version available +

+

+ Move to + {newerVersion.tag} + {newerVersion.bump} +

+ {#if behind > 1} +

{behind} versions ahead of the tag you run.

+ {/if} +

Click for release notes. Never auto-applied.

+
+
+
diff --git a/src/lib/components/VersionUpdateModal.svelte b/src/lib/components/VersionUpdateModal.svelte new file mode 100644 index 0000000..27744ee --- /dev/null +++ b/src/lib/components/VersionUpdateModal.svelte @@ -0,0 +1,272 @@ + + + { if (!v) close(); }}> + + {#if container && newerVersion} + + + + {container.name} + + {currentTag} + + {newerVersion.tag} + + + + {newerVersion.bump} + + {#if versionPath.length > 1} + {versionPath.length} versions ahead + {/if} + + + + Advisory only. Dockhand never changes the tag in your compose - bump it yourself. + + + + + {#if versionPath.length > 1} +
+ Path: + {currentTag} + {#each versionPath as v, i} + + {v} + {/each} +
+ {/if} + + +
+ {#if loading} +
+ + Loading release notes... +
+ {:else if notes.length > 0} + {#each versionPath.slice().reverse() as version} + {@const note = noteFor(version)} + {#if note} +
+ + + {note.name || note.githubTag} + {#if note.publishedAt} + {formatDate(note.publishedAt)} + {/if} + e.stopPropagation()} + class="text-muted-foreground hover:text-foreground shrink-0 {note.publishedAt ? '' : 'ml-auto'}" + title="Open on GitHub" + > + + + +
+ {@html renderMarkdown(note.body)} +
+
+ {/if} + {/each} + {:else} + +
+ +

+ {#if changelogUrl} + Release notes for these versions aren't available inline. + {:else} + This image doesn't publish release notes Dockhand can read + (no org.opencontainers.image.source label pointing at a GitHub or Gitea/Forgejo repo). + {/if} +

+ {#if changelogUrl} + + {/if} +
+ {/if} +
+ + +
+ {#if source} + Notes from {source} + {/if} +
+
+ + +
+
+ {/if} +
+
+ + diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 5520a95..e08842c 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -973,6 +973,7 @@ export const NOTIFICATION_EVENT_TYPES = [ { id: 'auto_update_failed', label: 'Auto-update failed', description: 'Container auto-update failed (pull error, start error)', group: 'auto_update', scope: 'environment' }, { id: 'auto_update_blocked', label: 'Auto-update blocked', description: 'Update blocked due to vulnerability criteria', group: 'auto_update', scope: 'environment' }, { id: 'updates_detected', label: 'Updates detected', description: 'Container image updates are available (scheduled check)', group: 'auto_update', scope: 'environment' }, + { id: 'newer_version_available', label: 'Newer version tag', description: 'A newer version tag is published for a pinned image (semver, advisory)', group: 'auto_update', scope: 'environment' }, { id: 'batch_update_success', label: 'Batch update completed', description: 'Scheduled container updates completed successfully', group: 'auto_update', scope: 'environment' }, // Git stack events (environment-scoped) @@ -4867,6 +4868,12 @@ export interface EnvUpdateCheckSettings { cron: string; autoUpdate: boolean; vulnerabilityCriteria: VulnerabilityCriteria; + // Semver (newer-version-tag) detection — optional so pre-existing rows (which + // never had these) parse as "off" without any migration. + checkPinnedVersions?: boolean; + semverMaxBump?: 'patch' | 'minor' | 'major'; + semverMatchFlavor?: boolean; + semverIncludePrerelease?: boolean; } export async function getEnvUpdateCheckSettings(envId: number): Promise { @@ -5443,8 +5450,15 @@ export async function addPendingContainerUpdate( environmentId: number, containerId: string, containerName: string, - currentImage: string + currentImage: string, + // A row can exist for a digest update, a newer-version-tag (semver) suggestion, + // or both. Both flags default to the classic "digest update only" shape so + // existing callers keep working unchanged. + options: { hasImageUpdate?: boolean; newerVersion?: unknown | null } = {} ): Promise { + const hasImageUpdate = options.hasImageUpdate ?? true; + const newerVersion = options.newerVersion != null ? JSON.stringify(options.newerVersion) : null; + const now = new Date().toISOString(); // Use insert with onConflictDoUpdate for upsert behavior await db.insert(pendingContainerUpdates) .values({ @@ -5452,14 +5466,18 @@ export async function addPendingContainerUpdate( containerId, containerName, currentImage, - checkedAt: new Date().toISOString() + hasImageUpdate, + newerVersion, + checkedAt: now }) .onConflictDoUpdate({ target: [pendingContainerUpdates.environmentId, pendingContainerUpdates.containerId], set: { containerName, currentImage, - checkedAt: new Date().toISOString() + hasImageUpdate, + newerVersion, + checkedAt: now } }); } diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 8194cad..1f88beb 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -481,6 +481,12 @@ export const pendingContainerUpdates = sqliteTable('pending_container_updates', containerId: text('container_id').notNull(), containerName: text('container_name').notNull(), currentImage: text('current_image').notNull(), + // True when a digest image update is pending (the classic amber icon). Defaults + // true so pre-existing rows keep behaving exactly as before. + hasImageUpdate: integer('has_image_update', { mode: 'boolean' }).notNull().default(true), + // A newer VERSION tag (semver) for a pinned image, as JSON {tag,bump,skipped}. + // Null when there's no semver suggestion. Advisory - never auto-applied. + newerVersion: text('newer_version'), checkedAt: text('checked_at').default(sql`CURRENT_TIMESTAMP`), createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`) }, (table) => ({ diff --git a/src/lib/server/db/schema/pg-schema.ts b/src/lib/server/db/schema/pg-schema.ts index f60d15a..ca41645 100644 --- a/src/lib/server/db/schema/pg-schema.ts +++ b/src/lib/server/db/schema/pg-schema.ts @@ -484,6 +484,12 @@ export const pendingContainerUpdates = pgTable('pending_container_updates', { containerId: text('container_id').notNull(), containerName: text('container_name').notNull(), currentImage: text('current_image').notNull(), + // True when a digest image update is pending (the classic amber icon). Defaults + // true so pre-existing rows keep behaving exactly as before. + hasImageUpdate: boolean('has_image_update').notNull().default(true), + // A newer VERSION tag (semver) for a pinned image, as JSON {tag,bump,skipped}. + // Null when there's no semver suggestion. Advisory - never auto-applied. + newerVersion: text('newer_version'), checkedAt: timestamp('checked_at', { mode: 'string' }).defaultNow(), createdAt: timestamp('created_at', { mode: 'string' }).defaultNow() }, (table) => ({ diff --git a/src/lib/server/docker.ts b/src/lib/server/docker.ts index f952700..993e264 100644 --- a/src/lib/server/docker.ts +++ b/src/lib/server/docker.ts @@ -19,6 +19,9 @@ import type { Environment } from './db'; import { getSetting } from './db'; import { getAdditionalVolumeBinds, dedupeVolumesForRecreate } from './mount-dedupe'; import { resolveNanoCpusConflict, resolvePodmanUsernsMode } from './hostconfig-recreate'; +// Import-light image parsing shared with the semver layer; re-exported below for callers. +import { parseImageReference } from './registry/image-ref'; +export { parseImageReference } from './registry/image-ref'; import { rebaseEnvOntoImage, rebaseLabelsOntoImage, rebaseCommand, describeEnvRebase, describeLabelRebase, type ImageEnvLabels } from './container-env-merge'; import { encodeRegistryAuth } from './registry-auth'; import { isSystemContainer, classifyEmptyDigestImage, localDigestIsIndexChild } from './scheduler/tasks/update-utils'; @@ -3023,60 +3026,6 @@ export async function inspectImage(id: string, envId?: number | null) { return dockerJsonRequest(`/images/${encodeURIComponent(id)}/json`, {}, envId); } -/** - * Parse an image reference into registry, repository, and tag components. - * Follows Docker's reference parsing rules. - * Examples: - * nginx:latest -> { registry: 'index.docker.io', repo: 'library/nginx', tag: 'latest' } - * ghcr.io/user/image:v1 -> { registry: 'ghcr.io', repo: 'user/image', tag: 'v1' } - * registry.example.com:5000/repo:tag -> { registry: 'registry.example.com:5000', repo: 'repo', tag: 'tag' } - */ -function parseImageReference(imageName: string): { registry: string; repo: string; tag: string } { - let registry = 'index.docker.io'; // Docker Hub's actual host - let repo = imageName; - let tag = 'latest'; - - // Handle digest references (remove digest part for manifest lookup) - if (repo.includes('@')) { - const [repoWithoutDigest] = repo.split('@'); - repo = repoWithoutDigest; - } - - // Extract tag - const lastColon = repo.lastIndexOf(':'); - if (lastColon > -1) { - const potentialTag = repo.substring(lastColon + 1); - // Make sure it's not a port number (no slashes in tags) - if (!potentialTag.includes('/')) { - tag = potentialTag; - repo = repo.substring(0, lastColon); - } - } - - // Extract registry if present - const firstSlash = repo.indexOf('/'); - if (firstSlash > -1) { - const firstPart = repo.substring(0, firstSlash); - // If the first part contains a dot, colon, or is "localhost", it's a registry - if (firstPart.includes('.') || firstPart.includes(':') || firstPart === 'localhost') { - registry = firstPart; - repo = repo.substring(firstSlash + 1); - } - } - - // Normalize docker.io to index.docker.io (Docker Hub's actual registry host) - // docker.io redirects to www.docker.com, while index.docker.io is the real API - if (registry === 'docker.io') { - registry = 'index.docker.io'; - } - - // Docker Hub requires library/ prefix for official images - if (registry === 'index.docker.io' && !repo.includes('/')) { - repo = `library/${repo}`; - } - - return { registry, repo, tag }; -} /** * Parse a registry URL into host and path components. @@ -3113,7 +3062,7 @@ export function parseRegistryUrl(url: string): { host: string; path: string; ful * - Host-only stored: stored 'registry.example.com' matches requested 'registry.example.com/org' * (allows a single credential entry to work for all org paths) */ -async function findRegistryCredentials(registryHost: string): Promise<{ username: string; password: string } | null> { +export async function findRegistryCredentials(registryHost: string): Promise<{ username: string; password: string } | null> { try { // Import here to avoid circular dependency const { getRegistries } = await import('./db.js'); diff --git a/src/lib/server/registry/image-ref.ts b/src/lib/server/registry/image-ref.ts new file mode 100644 index 0000000..46a310d --- /dev/null +++ b/src/lib/server/registry/image-ref.ts @@ -0,0 +1,75 @@ +/** + * Parse a Docker image reference into its registry host, repository, and tag, + * following Docker's own reference rules. Pure and dependency-free so it can be + * unit-tested (and imported by the light semver layer) without pulling in the + * native modules that live in docker.ts. + * + * nginx:latest -> { registry: 'index.docker.io', repo: 'library/nginx', tag: 'latest' } + * ghcr.io/user/image:v1 -> { registry: 'ghcr.io', repo: 'user/image', tag: 'v1' } + * codeberg.org/forgejo/forgejo:15.0.2 -> { registry: 'codeberg.org', repo: 'forgejo/forgejo', tag: '15.0.2' } + * registry.example.com:5000/repo:tag -> { registry: 'registry.example.com:5000', repo: 'repo', tag: 'tag' } + */ +export interface ImageReference { + registry: string; + repo: string; + tag: string; +} + +export function parseImageReference(imageName: string): ImageReference { + let registry = 'index.docker.io'; // Docker Hub's actual API host. + let repo = imageName; + let tag = 'latest'; + + // Drop a digest — irrelevant to tag/version lookups. + if (repo.includes('@')) { + repo = repo.split('@')[0]; + } + + // Split off the tag, but a colon before a `/` is a registry port, not a tag. + const lastColon = repo.lastIndexOf(':'); + if (lastColon > -1) { + const potentialTag = repo.slice(lastColon + 1); + if (!potentialTag.includes('/')) { + tag = potentialTag; + repo = repo.slice(0, lastColon); + } + } + + // The first path segment is the registry only if it looks like a host + // (has a dot or port, or is localhost) — otherwise it's part of the repo. + const firstSlash = repo.indexOf('/'); + if (firstSlash > -1) { + const firstPart = repo.slice(0, firstSlash); + if (firstPart.includes('.') || firstPart.includes(':') || firstPart === 'localhost') { + registry = firstPart; + repo = repo.slice(firstSlash + 1); + } + } + + if (registry === 'docker.io') registry = 'index.docker.io'; + + // Docker Hub official images are namespaced under library/. + if (registry === 'index.docker.io' && !repo.includes('/')) { + repo = `library/${repo}`; + } + + return { registry, repo, tag }; +} + +/** Every hostname that means "Docker Hub". */ +const DOCKER_HUB_HOSTS = new Set([ + 'index.docker.io', + 'docker.io', + 'registry-1.docker.io', + 'registry.docker.io', + 'hub.docker.com', + 'registry.hub.docker.com' +]); + +export function isDockerHub(registry: string): boolean { + return DOCKER_HUB_HOSTS.has(registry.toLowerCase()); +} + +export function isGhcr(registry: string): boolean { + return registry.toLowerCase() === 'ghcr.io'; +} diff --git a/src/lib/server/scheduler/tasks/env-update-check.ts b/src/lib/server/scheduler/tasks/env-update-check.ts index edc2f71..b1b64ea 100644 --- a/src/lib/server/scheduler/tasks/env-update-check.ts +++ b/src/lib/server/scheduler/tasks/env-update-check.ts @@ -15,8 +15,11 @@ import { saveVulnerabilityScan, clearPendingContainerUpdates, addPendingContainerUpdate, - removePendingContainerUpdate + removePendingContainerUpdate, + getPendingContainerUpdates } from '../../db'; +import { checkNewerVersion } from '../../semver/check'; +import type { NewerVersion } from '../../semver/find-newer'; import { listContainers, inspectContainer, @@ -107,6 +110,32 @@ export async function runEnvUpdateCheckJob( await log(`Starting update check for environment: ${env.name}`); await log(`Auto-update mode: ${config.autoUpdate ? 'ON' : 'OFF'}`); + // Semver "newer version tag" detection rides this same pass when enabled. + const semverEnabled = !!config.checkPinnedVersions; + const semverOptions = { + maxBump: config.semverMaxBump ?? 'major', + matchFlavor: config.semverMatchFlavor ?? true, + includePrerelease: config.semverIncludePrerelease ?? false + } as const; + // Remember the target we last surfaced per container, so we only notify when + // a NEW newer version appears - not on every scheduled run. Read before clear. + const previousSemverTargets = new Map(); + if (semverEnabled) { + try { + for (const row of await getPendingContainerUpdates(environmentId)) { + if (row.newerVersion) { + try { + const nv = JSON.parse(row.newerVersion) as NewerVersion; + previousSemverTargets.set(row.containerId, nv.tag); + } catch { /* ignore malformed */ } + } + } + } catch { /* non-fatal */ } + } + // Collected here so a single notification can summarise all newly-found versions. + const newSemverFindings: { containerName: string; imageName: string; newerVersion: NewerVersion }[] = []; + const semverByContainer = new Map(); + // Clear pending updates at the start - we'll re-add as we discover updates await clearPendingContainerUpdates(environmentId); @@ -182,20 +211,66 @@ export async function runEnvUpdateCheckJob( newDigest: result.registryDigest, oldImageConfig }); - // Add to pending table immediately - will be removed on successful update - await addPendingContainerUpdate(environmentId, container.id, container.name, imageName); await log(` UPDATE AVAILABLE`); await log(` Current: ${result.currentDigest?.substring(0, 24) || 'unknown'}...`); await log(` New: ${result.registryDigest?.substring(0, 24) || 'unknown'}...`); } else { await log(` Up to date`); } + + // Newer-version-tag (semver) detection - independent of the digest check. + // Skips floating tags without a registry call. Never throws. + if (semverEnabled) { + const newer = await checkNewerVersion(imageName, semverOptions).catch(() => null); + if (newer) { + semverByContainer.set(container.id, newer); + await log(` NEWER VERSION: ${newer.tag} (${newer.bump})`); + if (previousSemverTargets.get(container.id) !== newer.tag) { + newSemverFindings.push({ containerName: container.name, imageName, newerVersion: newer }); + } + } + } } catch (err: any) { await log(` [${container.name}] Error: ${err.message}`); errorCount++; } } + // Persist pending rows once per container, merging the digest update and the + // semver suggestion so a pure-semver container still gets a (badge) row. + const pendingContainerIds = new Set([ + ...updatesAvailable.map((u) => u.containerId), + ...semverByContainer.keys() + ]); + for (const cid of pendingContainerIds) { + const digest = updatesAvailable.find((u) => u.containerId === cid); + const semver = semverByContainer.get(cid) ?? null; + const container = containers.find((c) => c.id === cid); + await addPendingContainerUpdate( + environmentId, + cid, + digest?.containerName ?? container?.name ?? cid, + digest?.imageName ?? container?.image ?? '', + { hasImageUpdate: !!digest, newerVersion: semver } + ); + } + + // Notify about NEW newer-version tags (advisory, never auto-applied). Fires + // independently of digest updates, and only for versions not surfaced last + // run - so a daily cron won't re-notify the same suggestion. + if (newSemverFindings.length > 0) { + const lines = newSemverFindings + .map((f) => `- ${f.containerName} (${f.imageName}): newer version ${f.newerVersion.tag} (${f.newerVersion.bump})`) + .join('\n'); + await log(''); + await log(`Newer version tags: ${newSemverFindings.length} new`); + await sendEventNotification('newer_version_available', { + title: `Newer version tag${newSemverFindings.length !== 1 ? 's' : ''} available on ${env.name}`, + message: `${newSemverFindings.length} container${newSemverFindings.length !== 1 ? 's have' : ' has'} a newer version tag published (advisory - not auto-applied):\n${lines}`, + type: 'info' + }, environmentId); + } + // Summary await log(''); await log('=== SUMMARY ==='); @@ -205,8 +280,9 @@ export async function runEnvUpdateCheckJob( await log(`Errors: ${errorCount}`); if (updatesAvailable.length === 0) { - await log('All containers are up to date'); - // Pending updates already cleared at start, nothing to add + await log('No digest updates'); + // Any semver (newer-version) rows were already persisted + notified above; + // there's just no digest work to do, so skip the auto-update path. await updateScheduleExecution(execution.id, { status: 'success', completedAt: new Date().toISOString(), diff --git a/src/lib/server/semver/check.ts b/src/lib/server/semver/check.ts new file mode 100644 index 0000000..29eea14 --- /dev/null +++ b/src/lib/server/semver/check.ts @@ -0,0 +1,27 @@ +/** + * Decide whether a running image has a newer VERSION tag available. Glues the + * registry tag list (I/O) to the pure version comparison, and short-circuits for + * floating tags so `latest`/`stable`/sha never trigger a registry fetch. + */ +import { parseImageReference } from '../registry/image-ref'; +import { parseTag } from './tag-parser'; +import { listVersionTags } from './tag-source'; +import { findNewerVersionTag, type FindNewerOptions, type NewerVersion } from './find-newer'; + +/** + * Returns the newer-version suggestion for `imageRef`, or null when the current + * tag isn't a version (floating) or nothing newer is published. Never throws — + * a registry failure yields null, so the update check degrades gracefully. + */ +export async function checkNewerVersion( + imageRef: string, + options: FindNewerOptions = {} +): Promise { + const { tag } = parseImageReference(imageRef); + + // Floating tag -> nothing to compare, and we skip the registry call entirely. + if (!parseTag(tag)) return null; + + const tags = await listVersionTags(imageRef); + return findNewerVersionTag(tag, tags, options); +} diff --git a/src/lib/server/semver/find-newer.ts b/src/lib/server/semver/find-newer.ts new file mode 100644 index 0000000..37708c2 --- /dev/null +++ b/src/lib/server/semver/find-newer.ts @@ -0,0 +1,117 @@ +/** + * Given the tag a container currently runs and every tag the registry offers, + * decide whether a newer VERSION tag exists — and which versions were skipped + * along the way. Pure: no network, no registry knowledge, fully unit-testable. + * + * This is advisory only. We never auto-apply it: the tag lives in the user's + * compose, which we don't own. We just tell them "a newer version is out". + */ + +import { + parseTag, + prefixMatches, + flavorMatches, + compareParts, + isPrerelease, + type ParsedTag +} from './tag-parser'; + +export type VersionBump = 'major' | 'minor' | 'patch'; + +export interface FindNewerOptions { + /** Cap the offered bump: `patch` stays quiet, `major` offers everything. Default `major`. */ + maxBump?: VersionBump; + /** Consider `-rc`/`-beta`/... tags even when the current tag is stable. Default false. */ + includePrerelease?: boolean; + /** Require the same flavor/suffix (`-alpine`). Default true — the #1 noise-killer. */ + matchFlavor?: boolean; +} + +export interface NewerVersion { + /** The recommended tag to move to, e.g. `16.4-alpine`. */ + tag: string; + /** How big the jump is, relative to the current version. */ + bump: VersionBump; + /** Every newer version between current and target (exclusive of current, inclusive of target), ordered. */ + skipped: string[]; +} + +/** Which segment first differs decides the bump: [0]=major, [1]=minor, else patch. */ +export function classifyBump(current: ParsedTag, candidate: ParsedTag): VersionBump { + if ((candidate.parts[0] ?? 0) !== (current.parts[0] ?? 0)) return 'major'; + if ((candidate.parts[1] ?? 0) !== (current.parts[1] ?? 0)) return 'minor'; + return 'patch'; +} + +const BUMP_RANK: Record = { patch: 1, minor: 2, major: 3 }; + +/** Canonical key for a numeric version: trailing zero segments trimmed, so `1.2` + * and `1.2.0` (equal under compareParts) map to the same key. Keeps at least one + * segment (`0.0.0` -> `0`). */ +function versionKey(parts: number[]): string { + let end = parts.length; + while (end > 1 && parts[end - 1] === 0) end--; + return parts.slice(0, end).join('.'); +} + +/** + * Find the newest version tag strictly greater than `currentTag`, or null. + * + * Returns null when the current tag isn't a version at all (`latest`, `stable`, + * a sha) — there is nothing to compare, so no suggestion is ever made. + */ +export function findNewerVersionTag( + currentTag: string, + allTags: string[], + options: FindNewerOptions = {} +): NewerVersion | null { + const current = parseTag(currentTag); + if (!current) return null; // floating tag — not a version, never suggest. + + const { maxBump = 'major', includePrerelease = false, matchFlavor = true } = options; + const maxRank = BUMP_RANK[maxBump]; + const currentIsPrerelease = isPrerelease(current); + + const candidates = allTags + .map((tag) => ({ tag, parsed: parseTag(tag) })) + .filter((c): c is { tag: string; parsed: ParsedTag } => c.parsed !== null) + .filter((c) => prefixMatches(c.parsed.prefix, current.prefix)) + .filter((c) => !matchFlavor || flavorMatches(c.parsed, current)) + .filter((c) => includePrerelease || currentIsPrerelease || !isPrerelease(c.parsed)) + .filter((c) => compareParts(c.parsed, current) > 0) + .filter((c) => BUMP_RANK[classifyBump(current, c.parsed)] <= maxRank); + + if (candidates.length === 0) return null; + + // A registry commonly publishes the same version both with and without the + // `v` prefix (`v3.6` and `3.6`). They are the SAME version, so collapse them + // to one entry, keeping the variant whose prefix matches the running tag (so + // `v3.0` suggests `v3.6`, not `3.6`). Keyed by the numeric tuple. + const byVersion = new Map(); + for (const c of candidates) { + // Key on the numeric value with trailing zero segments trimmed, so `1.2` and + // `1.2.0` (equal under compareParts) collapse to one entry instead of both + // landing in `skipped` and racing for `target`. + const key = versionKey(c.parsed.parts); + const existing = byVersion.get(key); + if (!existing) { + byVersion.set(key, c); + continue; + } + // Prefer the variant that shares the current tag's exact prefix. + const cMatches = c.parsed.prefix === current.prefix; + const existingMatches = existing.parsed.prefix === current.prefix; + if (cMatches && !existingMatches) byVersion.set(key, c); + } + + const deduped = Array.from(byVersion.values()); + // Highest version wins; sort ascending so `skipped` reads oldest -> newest. + deduped.sort((a, b) => compareParts(a.parsed, b.parsed)); + const target = deduped[deduped.length - 1]; + + return { + tag: target.tag, + bump: classifyBump(current, target.parsed), + skipped: deduped.map((c) => c.tag) + }; +} diff --git a/src/lib/server/semver/release-notes.ts b/src/lib/server/semver/release-notes.ts new file mode 100644 index 0000000..cead6a5 --- /dev/null +++ b/src/lib/server/semver/release-notes.ts @@ -0,0 +1,107 @@ +/** + * Fetch per-version release notes from GitHub for the versions a semver check + * surfaced. The match core (release-tag <-> wanted-version) is pure and unit- + * tested; only the fetch touches the network. Degrades to an empty list on any + * failure, so the modal always renders (it falls back to a plain link). + */ + +import { parseTag, compareParts } from './tag-parser'; +import type { ReleaseSource } from './release-source'; + +export interface ReleaseNote { + /** The requested version tag this note is for, e.g. `16.4-alpine`. */ + version: string; + /** The GitHub release name/title (may be empty). */ + name: string; + /** The GitHub release tag, e.g. `v16.4`. */ + githubTag: string; + /** Markdown body of the release. */ + body: string; + /** ISO date the release was published. */ + publishedAt: string | null; + /** Direct link to the release page. */ + url: string; +} + +interface GithubRelease { + tag_name: string; + name: string | null; + body: string | null; + published_at: string | null; + html_url: string; + draft: boolean; + prerelease: boolean; +} + +/** + * Pick, for each wanted version tag, the GitHub release whose tag parses to the + * SAME numeric version. Pure: `releases` is whatever the API returned, `wanted` + * is the list of image tags (target + skipped). Flavor/prefix noise is ignored — + * only the numeric tuple has to match, so `v16.4` matches `16.4-alpine`. + */ +export function matchReleasesToVersions( + releases: Pick[], + wanted: string[] +): ReleaseNote[] { + const notes: ReleaseNote[] = []; + for (const version of wanted) { + const parsedWanted = parseTag(version); + if (!parsedWanted) continue; + const match = releases.find((r) => { + const parsedRelease = parseTag(r.tag_name); + return parsedRelease && compareParts(parsedRelease, parsedWanted) === 0; + }); + if (match) { + notes.push({ + version, + name: match.name ?? '', + githubTag: match.tag_name, + body: match.body ?? '', + publishedAt: match.published_at, + url: match.html_url + }); + } + } + return notes; +} + +const PER_PAGE = 100; +const MAX_PAGES = 3; // 300 recent releases is plenty to cover a handful of skipped versions. + +/** + * Fetch release notes for `wanted` versions from a GitHub or Gitea/Forgejo forge. + * Both APIs return the same fields and page newest-first; only the page-size + * query param differs (`per_page` vs `limit`). Walks pages until every wanted + * version is matched or the page budget is spent. Never throws. + */ +export async function fetchReleaseNotes( + src: ReleaseSource, + wanted: string[], + fetchImpl: typeof fetch = fetch +): Promise { + if (wanted.length === 0) return []; + + const pageSizeParam = src.kind === 'github' ? 'per_page' : 'limit'; + const collected: GithubRelease[] = []; + try { + for (let page = 1; page <= MAX_PAGES; page++) { + const url = `${src.apiBase}?${pageSizeParam}=${PER_PAGE}&page=${page}`; + const res = await fetchImpl(url, { + headers: { Accept: 'application/json', 'User-Agent': 'dockhand' } + }); + if (!res.ok) break; + const batch = (await res.json()) as GithubRelease[]; + if (!Array.isArray(batch) || batch.length === 0) break; + collected.push(...batch.filter((r) => !r.draft)); + + // Stop early once we can match everything we're after. + const matched = matchReleasesToVersions(collected, wanted); + if (matched.length >= wanted.length) break; + if (batch.length < PER_PAGE) break; + } + } catch { + return matchReleasesToVersions(collected, wanted); + } + + return matchReleasesToVersions(collected, wanted); +} diff --git a/src/lib/server/semver/release-source.ts b/src/lib/server/semver/release-source.ts new file mode 100644 index 0000000..c62f888 --- /dev/null +++ b/src/lib/server/semver/release-source.ts @@ -0,0 +1,109 @@ +/** + * Resolve WHERE to fetch a container image's release notes from. Two forge + * families share one release-notes shape and are handled here: + * + * - GitHub -> https://api.github.com/repos//releases + * - Gitea / Forgejo -> https:///api/v1/repos//releases + * (Codeberg is a Forgejo instance; self-hosted Gitea/Forgejo work too.) + * + * Both return the same fields (`tag_name`, `name`, `body`, `published_at`, + * `html_url`), so the fetch + version-match logic is identical - only the base + * URL differs. Priority mirrors resolveChangelogUrl: the + * `org.opencontainers.image.source` label wins, then a `ghcr.io//` + * image name (always GitHub). + * + * Pure, no I/O, unit-tested. Images with no recognizable source resolve to null + * (we never guess a repo - a wrong repo is worse than no notes). + */ + +const GHCR_PREFIX = 'ghcr.io/'; + +export type ForgeKind = 'github' | 'gitea'; + +export interface ReleaseSource { + kind: ForgeKind; + /** `owner/repo`. */ + slug: string; + /** Fully-formed releases API base, ready for `?per_page`/`?limit` + `&page`. */ + apiBase: string; + /** Human-facing releases page (for the "View releases" fallback link). */ + releasesUrl: string; +} + +/** Pull `owner/repo` out of a forge URL, tolerating trailing slash / `.git`. */ +function slugFromUrl(url: string, host: string): string | null { + const idx = url.indexOf(host); + if (idx === -1) return null; + const after = url + .slice(idx + host.length) + .replace(/^\/+/, '') + .replace(/\/+$/, '') + .replace(/\.git$/, ''); + const parts = after.split('/'); + if (parts.length < 2 || !parts[0] || !parts[1]) return null; + return `${parts[0]}/${parts[1]}`; +} + +function hostOf(url: string): string | null { + try { + return new URL(url).host; + } catch { + return null; + } +} + +function stripImageTag(image: string): string { + const atIdx = image.indexOf('@'); + const withoutDigest = atIdx >= 0 ? image.slice(0, atIdx) : image; + const colonIdx = withoutDigest.lastIndexOf(':'); + if (colonIdx > withoutDigest.lastIndexOf('/')) { + return withoutDigest.slice(0, colonIdx); + } + return withoutDigest; +} + +export function resolveReleaseSource( + imageName: string | null | undefined, + labels?: Record | null +): ReleaseSource | null { + const source = labels?.['org.opencontainers.image.source']; + if (source) { + const host = hostOf(source); + if (host === 'github.com') { + const slug = slugFromUrl(source, 'github.com'); + if (slug) return githubSource(slug); + } else if (host) { + // Any other host is treated as a Gitea/Forgejo instance. Its releases + // API is host-relative, so the notes come from the SAME host the image + // declares (codeberg.org, gitea.com, a self-hosted forge, ...). + const slug = slugFromUrl(source, host); + if (slug) { + return { + kind: 'gitea', + slug, + apiBase: `https://${host}/api/v1/repos/${slug}/releases`, + releasesUrl: `https://${host}/${slug}/releases` + }; + } + } + } + + if (imageName && imageName.startsWith(GHCR_PREFIX)) { + const repo = stripImageTag(imageName.slice(GHCR_PREFIX.length)); + const parts = repo.split('/'); + if (parts.length >= 2 && parts[0] && parts[1]) { + return githubSource(`${parts[0]}/${parts[1]}`); + } + } + + return null; +} + +function githubSource(slug: string): ReleaseSource { + return { + kind: 'github', + slug, + apiBase: `https://api.github.com/repos/${slug}/releases`, + releasesUrl: `https://github.com/${slug}/releases` + }; +} diff --git a/src/lib/server/semver/strategies/docker-hub.ts b/src/lib/server/semver/strategies/docker-hub.ts new file mode 100644 index 0000000..04ce1e4 --- /dev/null +++ b/src/lib/server/semver/strategies/docker-hub.ts @@ -0,0 +1,42 @@ +/** + * List tags from Docker Hub via its own tags API (not the Registry v2 + * `/tags/list`), ordered newest-first, so recent version tags land on the first + * page or two. + * + * No server-side `name=` filter: Hub's `name` param is a SUBSTRING match, so + * narrowing by the current major digit (e.g. `1` for `1.25`) would exclude a + * clean newer major like `2.0` (no `1` in the name) and silently hide the single + * most important upgrade. We page newest-first instead and let the pure + * comparator pick the target. + */ + +/** Newest-first pages; a few pages cover recent version tags across major lines. */ +const MAX_PAGES = 3; +const PAGE_SIZE = 100; + +interface HubTag { + name: string; +} +interface HubTagsResponse { + results?: HubTag[]; + next?: string | null; +} + +/** + * @param repo Full Hub repo path, e.g. `library/nginx` or `grafana/grafana`. + */ +export async function fetchDockerHubTags(repo: string): Promise { + const tags: string[] = []; + + for (let page = 1; page <= MAX_PAGES; page++) { + const url = `https://hub.docker.com/v2/repositories/${repo}/tags?page_size=${PAGE_SIZE}&page=${page}&ordering=last_updated`; + const response = await fetch(url, { headers: { Accept: 'application/json' } }); + if (!response.ok) break; + + const data = (await response.json()) as HubTagsResponse; + tags.push(...(data.results ?? []).map((t) => t.name)); + if (!data.next) break; + } + + return tags; +} diff --git a/src/lib/server/semver/strategies/generic-v2.ts b/src/lib/server/semver/strategies/generic-v2.ts new file mode 100644 index 0000000..c251930 --- /dev/null +++ b/src/lib/server/semver/strategies/generic-v2.ts @@ -0,0 +1,47 @@ +/** + * List tags from any Registry HTTP API v2 host (Codeberg, GitLab, Harbor, + * self-hosted, ...) via `/v2//tags/list`, following the `Link: rel="next"` + * header across pages so version tags aren't lost behind a first page full of + * git-hash tags. Auth uses our shared WWW-Authenticate token flow, which also + * works anonymously for public repos. + */ +import { getRegistryAuthHeader } from '../../docker'; + +/** Stop after this many pages — a guard against pathological repos, not a real limit. */ +const MAX_PAGES = 50; +const PAGE_SIZE = 1000; + +interface TagsListResponse { + tags?: string[]; +} + +/** Parse `; rel="next"` into the next path. */ +function nextPagePath(linkHeader: string | null): string | null { + const match = linkHeader?.match(/<([^>]+)>;\s*rel="next"/); + return match ? match[1] : null; +} + +export async function fetchGenericV2Tags( + registry: string, + repo: string, + credentials?: { username: string; password: string } | null +): Promise { + const base = `https://${registry}`; + const authHeader = await getRegistryAuthHeader(base, `repository:${repo}:pull`, credentials ?? undefined); + const headers: Record = { Accept: 'application/json', 'User-Agent': 'Dockhand/1.0' }; + if (authHeader) headers['Authorization'] = authHeader; + + const tags: string[] = []; + let path: string | null = `/v2/${repo}/tags/list?n=${PAGE_SIZE}`; + + for (let page = 0; page < MAX_PAGES && path; page++) { + const response = await fetch(`${base}${path}`, { headers }); + if (!response.ok) break; // 4xx/5xx: stop with whatever we have. + + const data = (await response.json()) as TagsListResponse; + tags.push(...(data.tags ?? [])); + path = nextPagePath(response.headers.get('link')); + } + + return tags; +} diff --git a/src/lib/server/semver/tag-parser.ts b/src/lib/server/semver/tag-parser.ts new file mode 100644 index 0000000..8ca4092 --- /dev/null +++ b/src/lib/server/semver/tag-parser.ts @@ -0,0 +1,95 @@ +/** + * Parse a Docker image tag into its numeric version and the text around it, so + * two tags of the SAME image can be compared as versions. + * + * A tag is `` where version is the first dotted-number + * run: `v1.25-alpine` -> prefix `v`, version `1.25`, suffix `-alpine`. The + * version becomes a plain numeric tuple (`[1, 25]`), which compares CalVer + * (`2024.1.3`) and SemVer (`1.2.3`) alike with no semver library and no special + * cases. Pure functions, no I/O. + */ + +/** The first dotted-number run in a tag: 2 to 4 numeric segments. */ +const VERSION_RE = /\d+\.\d+(?:\.\d+)?(?:\.\d+)?/; + +export interface ParsedTag { + /** The matched version text, e.g. `1.25` or `2024.1.3`. */ + version: string; + /** Text before the version, e.g. `v` in `v1.25`. */ + prefix: string; + /** Text after the version, e.g. `-alpine` in `1.25-alpine`. This is the "flavor". */ + suffix: string; + /** The version as a numeric tuple, e.g. `[1, 25]`. */ + parts: number[]; +} + +/** Parse a tag into its version parts, or null when it has no version (`latest`, `stable`, a sha). */ +export function parseTag(tag: string): ParsedTag | null { + const match = VERSION_RE.exec(tag); + if (!match) return null; + + const version = match[0]; + return { + version, + prefix: tag.slice(0, match.index), + suffix: tag.slice(match.index + version.length), + parts: version.split('.').map(Number) + }; +} + +/** + * Two prefixes are equivalent, treating `""` and `"v"` as the same — projects + * routinely add or drop the leading `v` between releases (`1.2.3` vs `v1.2.3`). + */ +export function prefixMatches(a: string, b: string): boolean { + const normalize = (p: string) => (p === 'v' ? '' : p); + return normalize(a) === normalize(b); +} + +// A channel marker delimited by start/`-`/`.`, optionally followed by a number +// (`rc1`, `beta.2`) or another delimiter. `\b` won't do: `rc1` has no boundary +// between `c` and `1`. +const PRERELEASE_RE = /(^|[-.])(rc|beta|alpha|nightly|dev|pre|snapshot)(\d|[-.]|$)/i; + +// A trailing commit hash, e.g. `-b2da6b90f` in searxng's `2026.8.16-b2da6b90f`. +// Requiring 7+ pure-hex chars keeps real build variants (`-alpine`, `-ls123`, +// `-ubuntu22.04`) intact - they aren't long hex runs - while collapsing the +// per-release hash so every hashed tag of one image shares a flavor and can be +// compared. Guarded by at least one letter so a plain numeric build (`-12345678`) +// isn't mistaken for a hash. +const COMMIT_HASH_RE = /[-.](?=[0-9a-f]*[a-f])[0-9a-f]{7,}$/i; + +export function isPrerelease(tag: ParsedTag): boolean { + return PRERELEASE_RE.test(tag.suffix); +} + +/** + * The "flavor" is the stable part of the suffix — the build variant like + * `-alpine` or `-ls123` — with any prerelease channel and any trailing commit + * hash stripped off. So `1.0-alpine` and `1.1-alpine` share the flavor `-alpine`, + * `1.0-rc1` / `1.0-rc2` share the flavor `` (they only differ in the prerelease), + * and `2026.8.15-a1b2c3d` / `2026.8.16-b2da6b9` share the flavor `` (they only + * differ in the per-release hash). This lets one hashed tag suggest the next + * while still never mixing `-alpine` with bare. + */ +export function flavorOf(tag: ParsedTag): string { + return tag.suffix + .replace(COMMIT_HASH_RE, '') + .replace(PRERELEASE_RE, (_m, delim: string) => (delim === '.' ? '' : delim)) + .replace(/[-.]+$/, ''); // drop the delimiter the prerelease left dangling +} + +/** A newer version is only offered within the SAME flavor. Exact match, prerelease-agnostic. */ +export function flavorMatches(a: ParsedTag, b: ParsedTag): boolean { + return flavorOf(a) === flavorOf(b); +} + +/** Compare two version tuples pairwise (missing segments count as 0). <0, 0, or >0. */ +export function compareParts(a: ParsedTag, b: ParsedTag): number { + const length = Math.max(a.parts.length, b.parts.length); + for (let i = 0; i < length; i++) { + const diff = (a.parts[i] ?? 0) - (b.parts[i] ?? 0); + if (diff !== 0) return diff; + } + return 0; +} diff --git a/src/lib/server/semver/tag-source.ts b/src/lib/server/semver/tag-source.ts new file mode 100644 index 0000000..0f8e147 --- /dev/null +++ b/src/lib/server/semver/tag-source.ts @@ -0,0 +1,76 @@ +/** + * List every version-looking tag an image's registry publishes, so the pure + * `findNewerVersionTag` can pick the newest. Routes to the right fetch strategy + * by host (Docker Hub API vs generic Registry v2), and caches per repo for 6h — + * the set of published tags changes slowly and this keeps registries un-hammered. + * + * GHCR note: ghcr.io works through the generic v2 path here. GitHub's Packages + * API returns versions newest-first (nicer for repos buried under git-hash tags), + * but it needs a token we don't yet store — a phase-2 optimization, not a gap. + */ +import { parseImageReference, isDockerHub } from '../registry/image-ref'; +import { findRegistryCredentials } from '../docker'; +import { fetchDockerHubTags } from './strategies/docker-hub'; +import { fetchGenericV2Tags } from './strategies/generic-v2'; + +const CACHE_TTL_MS = 6 * 60 * 60 * 1000; + +interface CacheEntry { + tags: string[]; + expiresAt: number; +} +// Keyed by `registry/repo`. Registry credentials are stored per-HOST (not per +// environment) in Dockhand's registries table, so the tag list for a given +// registry/repo is the same regardless of which env asked - the key needs no env +// dimension. A generic V2 registry that needs auth uses the stored credentials +// for that host (same lookup the digest check uses); an unauthenticated repo just +// works anonymously. +const cache = new Map(); +// In-flight fetches keyed the same way, so a burst of same-image containers in +// one check pass (CONCURRENCY=20) collapses to ONE registry request per repo +// instead of one per container. +const inflight = new Map>(); + +/** + * All tags published for the image behind `imageRef`, cached per repo. Returns + * `[]` on any registry failure — the caller treats "no tags" as "nothing newer". + */ +export async function listVersionTags(imageRef: string): Promise { + const { registry, repo } = parseImageReference(imageRef); + const cacheKey = `${registry}/${repo}`; + + const cached = cache.get(cacheKey); + if (cached && cached.expiresAt > Date.now()) return cached.tags; + + const pending = inflight.get(cacheKey); + if (pending) return pending; + + const promise = (async () => { + let tags: string[] = []; + try { + if (isDockerHub(registry)) { + tags = await fetchDockerHubTags(repo); + } else { + const credentials = await findRegistryCredentials(registry).catch(() => null); + tags = await fetchGenericV2Tags(registry, repo, credentials); + } + } catch { + tags = []; + } + cache.set(cacheKey, { tags, expiresAt: Date.now() + CACHE_TTL_MS }); + return tags; + })(); + + inflight.set(cacheKey, promise); + try { + return await promise; + } finally { + inflight.delete(cacheKey); + } +} + +/** Test-only: clear the per-repo tag cache. */ +export function __resetTagSourceCacheForTests(): void { + cache.clear(); + inflight.clear(); +} diff --git a/src/lib/server/stacks.ts b/src/lib/server/stacks.ts index 2f3bd8b..9d8994c 100644 --- a/src/lib/server/stacks.ts +++ b/src/lib/server/stacks.ts @@ -1845,13 +1845,27 @@ export async function listComposeStacks(envId?: number | null): Promise>(); - // Container IDs with pending image updates (populated by manual/scheduled update checks). - // Used to flag stacks that contain at least one outdated container. + // Container IDs with a pending DIGEST image update (the classic amber icon). + // A persisted row can also be a pure newer-version-tag (semver) suggestion with + // no digest update - those must NOT count as an "update available", so filter + // on hasImageUpdate. `newerVersionIds` drives the separate semver stack badge. const pendingUpdateIds = new Set(); + const newerVersionIds = new Set(); + const newerVersionById = new Map(); if (typeof envId === 'number') { try { const pending = await getPendingContainerUpdates(envId); - pending.forEach((p) => pendingUpdateIds.add(p.containerId)); + pending.forEach((p) => { + if (p.hasImageUpdate) pendingUpdateIds.add(p.containerId); + if (p.newerVersion) { + newerVersionIds.add(p.containerId); + try { + newerVersionById.set(p.containerId, JSON.parse(p.newerVersion)); + } catch { + // malformed row - skip the badge for this one + } + } + }); } catch { // Non-fatal: stacks just won't show update markers } @@ -1914,7 +1928,8 @@ export async function listComposeStacks(envId?: number | null): Promise { @@ -1930,6 +1945,8 @@ export async function listComposeStacks(envId?: number | null): Promise pendingUpdateIds.has(c.id)), updateCount: stackContainers.filter((c) => pendingUpdateIds.has(c.id)).length, + // Newer-version-tag (semver) suggestions in this stack - drives the Tag badge. + newerVersionCount: stackContainers.filter((c) => newerVersionIds.has(c.id)).length, status: activeTotal === 0 ? 'stopped' diff --git a/src/lib/stores/containers.ts b/src/lib/stores/containers.ts index 4f6c5bd..66a5b02 100644 --- a/src/lib/stores/containers.ts +++ b/src/lib/stores/containers.ts @@ -4,6 +4,7 @@ import type { ContainerInfo, ContainerStats } from '$lib/types'; import { appendEnvParam, clearStaleEnvironment, environments } from '$lib/stores/environment'; import { appSettings } from '$lib/stores/settings'; import { toast } from 'svelte-sonner'; +import type { NewerVersion } from '$lib/types'; export interface AutoUpdateSetting { enabled: boolean; @@ -29,6 +30,8 @@ export interface ContainerStoreState { failedUpdateIds: string[]; /** Update-check error message keyed by container ID (for tooltip) */ failedUpdateErrors: Map; + /** Newer VERSION tag (semver) suggestions from the last check, keyed by container ID. Advisory. */ + newerVersions: Map; /** Whether the current environment has vulnerability scanning */ envHasScanning: boolean; /** Environment-level vulnerability criteria */ @@ -48,6 +51,7 @@ const INITIAL_STATE: ContainerStoreState = { pendingUpdateNames: new Map(), failedUpdateIds: [], failedUpdateErrors: new Map(), + newerVersions: new Map(), envHasScanning: false, envVulnerabilityCriteria: 'never', loading: true, @@ -290,14 +294,20 @@ function createContainerStore() { const response = await fetch(appendEnvParam('/api/containers/pending-updates', envId)); if (!response.ok) return; const data = await response.json(); - if (data.pendingUpdates && data.pendingUpdates.length > 0) { - patch({ - pendingUpdateIds: data.pendingUpdates.map((u: any) => u.containerId), - pendingUpdateNames: new Map( - data.pendingUpdates.map((u: any) => [u.containerId, u.containerName]) - ) - }); - } + // A persisted row can carry a digest update, a newer-version (semver) + // suggestion, or both. Split them: only hasImageUpdate rows drive the amber + // "update available" state; newerVersion rows drive the Tag badge. Always + // patch (even on an empty list) so a cleared server state clears the store - + // otherwise stale badges/counters linger after the last update is applied. + const rows: any[] = data.pendingUpdates ?? []; + const withImageUpdate = rows.filter((u) => u.hasImageUpdate); + patch({ + pendingUpdateIds: withImageUpdate.map((u) => u.containerId), + pendingUpdateNames: new Map(withImageUpdate.map((u) => [u.containerId, u.containerName])), + newerVersions: new Map( + rows.filter((u) => u.newerVersion).map((u) => [u.containerId, u.newerVersion as NewerVersion]) + ) + }); } catch { // Ignore errors - background load } @@ -357,6 +367,11 @@ function createContainerStore() { patch({ failedUpdateIds: ids, failedUpdateErrors: errors }); }, + /** Record newer-version-tag (semver) suggestions from the last check. Session-only, advisory. */ + setNewerVersions(map: Map) { + patch({ newerVersions: map }); + }, + /** Patch arbitrary fields */ patch }; diff --git a/src/lib/types.ts b/src/lib/types.ts index 207f7b1..2dbf7c3 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,6 +5,13 @@ */ export type SystemContainerType = 'dockhand' | 'hawser'; +/** A newer VERSION tag (semver) suggestion. Mirrors the server's find-newer result. */ +export interface NewerVersion { + tag: string; + bump: 'major' | 'minor' | 'patch'; + skipped: string[]; +} + export interface ContainerInfo { id: string; name: string; @@ -135,6 +142,8 @@ export interface StackContainer { created: number; labels: Record; updateAvailable?: boolean; + /** A newer VERSION tag (semver) for this pinned image, or null. Advisory. */ + newerVersion?: NewerVersion | null; } export interface ComposeStackInfo { @@ -144,6 +153,8 @@ export interface ComposeStackInfo { status: string; updatesAvailable?: boolean; updateCount?: number; + /** How many containers in this stack have a newer version tag (semver). */ + newerVersionCount?: number; sourceType?: 'external' | 'internal' | 'git'; repository?: { id: number; diff --git a/src/lib/utils/markdown.ts b/src/lib/utils/markdown.ts new file mode 100644 index 0000000..6df504d --- /dev/null +++ b/src/lib/utils/markdown.ts @@ -0,0 +1,15 @@ +/** + * Render untrusted markdown (GitHub release bodies) to sanitized HTML. The + * source is arbitrary internet content, so the output is always run through + * DOMPurify before it reaches an {@html} block. + */ +import { marked } from 'marked'; +import DOMPurify from 'isomorphic-dompurify'; + +marked.setOptions({ gfm: true, breaks: false }); + +export function renderMarkdown(md: string): string { + if (!md) return ''; + const raw = marked.parse(md, { async: false }) as string; + return DOMPurify.sanitize(raw, { ADD_ATTR: ['target', 'rel'] }); +} diff --git a/src/routes/api/containers/[id]/version-notes/+server.ts b/src/routes/api/containers/[id]/version-notes/+server.ts new file mode 100644 index 0000000..9c29c5f --- /dev/null +++ b/src/routes/api/containers/[id]/version-notes/+server.ts @@ -0,0 +1,58 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { inspectContainer } from '$lib/server/docker'; +import { authorize } from '$lib/server/authorize'; +import { validateDockerIdParam } from '$lib/server/docker-validation'; +import { resolveReleaseSource } from '$lib/server/semver/release-source'; +import { resolveChangelogUrl } from '$lib/utils/changelog-url'; +import { fetchReleaseNotes } from '$lib/server/semver/release-notes'; + +/** + * GET /api/containers/{id}/version-notes - Release notes for a newer version tag + * + * @openapi + * summary: Resolve GitHub release notes for the versions a semver update-check surfaced (requires the 'view' permission) + * path: id:string! Container ID or name (from GET /api/containers) + * query: env:integer The target environment ID (omit for the local/default Docker host) (from GET /api/environments) + * query: versions:string! Comma-separated version tags to fetch notes for (target + skipped), e.g. "16.3-alpine,16.4-alpine" + * resp-200: {changelogUrl:string, source:string, notes:array<{version:string!, name:string, githubTag:string, body:string, publishedAt:string, url:string!}>!} + * resp-200-example: {"changelogUrl":"https://github.com/go-gitea/gitea/releases","source":"go-gitea/gitea","notes":[{"version":"1.22.0","name":"v1.22.0","githubTag":"v1.22.0","body":"## Changes...","publishedAt":"2024-05-01T00:00:00Z","url":"https://github.com/go-gitea/gitea/releases/tag/v1.22.0"}]} + * resp-403: Permission denied + * resp-500: Failed to resolve release notes + */ +export const GET: RequestHandler = async ({ params, url, cookies }) => { + const invalid = validateDockerIdParam(params.id, 'container'); + if (invalid) return invalid; + + const auth = await authorize(cookies); + + const envId = url.searchParams.get('env'); + const envIdNum = envId ? parseInt(envId) : undefined; + + if (auth.authEnabled && !(await auth.can('containers', 'view', envIdNum))) { + return json({ error: 'Permission denied' }, { status: 403 }); + } + + const versions = (url.searchParams.get('versions') ?? '') + .split(',') + .map((v) => v.trim()) + .filter(Boolean); + + try { + const inspectData = (await inspectContainer(params.id, envIdNum)) as any; + const imageName: string | undefined = inspectData.Config?.Image; + const labels: Record = inspectData.Config?.Labels ?? {}; + + const src = resolveReleaseSource(imageName, labels); + // Prefer the label-based changelog URL; fall back to the forge's releases page. + const changelogUrl = resolveChangelogUrl(imageName, labels) ?? src?.releasesUrl ?? null; + + // No recognizable forge -> no per-version notes, just the changelog link (if any). + const notes = src ? await fetchReleaseNotes(src, versions) : []; + + return json({ changelogUrl, source: src?.slug ?? null, notes }); + } catch (error) { + console.error('Failed to resolve version notes:', error); + return json({ error: 'Failed to resolve release notes' }, { status: 500 }); + } +}; diff --git a/src/routes/api/containers/check-updates/+server.ts b/src/routes/api/containers/check-updates/+server.ts index bea049c..6c17ed4 100644 --- a/src/routes/api/containers/check-updates/+server.ts +++ b/src/routes/api/containers/check-updates/+server.ts @@ -2,10 +2,12 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { authorize } from '$lib/server/authorize'; import { listContainers, inspectContainer, checkImageUpdateAvailable } from '$lib/server/docker'; -import { clearPendingContainerUpdates, addPendingContainerUpdate, getPendingContainerUpdates } from '$lib/server/db'; +import { clearPendingContainerUpdates, addPendingContainerUpdate, getPendingContainerUpdates, getEnvUpdateCheckSettings } from '$lib/server/db'; import { isSystemContainer, isPodmanInfraContainer } from '$lib/server/scheduler/tasks/update-utils'; import { isUpdateDisabledByLabel, isHiddenByLabel } from '$lib/server/container-labels'; import { createJobResponse } from '$lib/server/sse'; +import { checkNewerVersion } from '$lib/server/semver/check'; +import type { NewerVersion } from '$lib/server/semver/find-newer'; export interface UpdateCheckResult { containerId: string; @@ -18,12 +20,23 @@ export interface UpdateCheckResult { isLocalImage?: boolean; systemContainer?: 'dockhand' | 'hawser' | null; updateDisabled?: boolean; + /** A newer VERSION tag is published for a pinned image (advisory, never auto-applied). */ + newerVersion?: NewerVersion; } /** * GET = READ the cached result of the last update check (no side effects, does NOT * trigger a new check). Returns the containers currently flagged as having a pending * image update. Use POST (below) to actually run a fresh check. (issue #1266) + * + * @openapi + * summary: Read the cached pending image-update records for an environment (no fresh check; requires the 'view' permission) + * description: Returns the containers currently flagged as having a pending image update from the last check. Does not trigger a new check — use POST to run a fresh check. + * query: env:integer The target environment ID (omit for the local/default Docker host) (from GET /api/environments) + * resp-200: {environmentId:integer!, pendingUpdates:array<{containerId:string!, containerName:string!, currentImage:string!, checkedAt:string}>!} + * resp-400: Environment ID required + * resp-403: Permission denied + * resp-500: Failed to get pending updates */ export const GET: RequestHandler = async ({ url, cookies }) => { const auth = await authorize(cookies); @@ -59,6 +72,12 @@ export const GET: RequestHandler = async ({ url, cookies }) => { /** * POST = TRIGGER a fresh update check across all containers (job-based). * Returns progress events during checking, final result when done. + * + * @openapi + * summary: Trigger a fresh image-update check across all (non-hidden, non-podman-infra) containers in an environment + * query: env:integer The target environment ID (omit for the local/default Docker host) (from GET /api/environments) + * resp-200: text/event-stream job stream ("progress" events with {checked,total}, final "result" event with {total,updatesFound,results}) — or, with "Accept: application/json", the final result as plain JSON + * resp-403: Permission denied */ export const POST: RequestHandler = async ({ url, cookies, request }) => { const auth = await authorize(cookies); @@ -87,6 +106,17 @@ export const POST: RequestHandler = async ({ url, cookies, request }) => { send('progress', { checked: 0, total: containers.length }); + // Semver "newer version tag" detection is an opt-in branch on this same pass. + // Read the env's update-check config once; if checkPinnedVersions is on, each + // pinned-version container is also compared against the registry's tag list. + const envSettings = envIdNum ? await getEnvUpdateCheckSettings(envIdNum) : null; + const semverEnabled = !!envSettings?.checkPinnedVersions; + const semverOptions = { + maxBump: envSettings?.semverMaxBump ?? 'major', + matchFlavor: envSettings?.semverMatchFlavor ?? true, + includePrerelease: envSettings?.semverIncludePrerelease ?? false + } as const; + // Check container for updates let checked = 0; const checkContainer = async (container: typeof containers[0]): Promise => { @@ -120,6 +150,12 @@ export const POST: RequestHandler = async ({ url, cookies, request }) => { const result = await checkImageUpdateAvailable(imageName, currentImageId, envIdNum); + // Newer-version-tag detection (opt-in). Skips instantly for floating tags, + // so it only hits the registry for pinned versions. Never throws. + const newerVersion = semverEnabled + ? await checkNewerVersion(imageName, semverOptions).catch(() => null) + : null; + return { containerId: container.id, containerName: container.name, @@ -130,7 +166,8 @@ export const POST: RequestHandler = async ({ url, cookies, request }) => { error: result.error, isLocalImage: result.isLocalImage, systemContainer: isSystemContainer(imageName) || null, - updateDisabled + updateDisabled, + newerVersion: newerVersion ?? undefined }; } catch (error: any) { return { @@ -160,17 +197,21 @@ export const POST: RequestHandler = async ({ url, cookies, request }) => { const updatesFound = results.filter(r => r.hasUpdate && !r.systemContainer && !r.updateDisabled).length; - // Save containers with updates to the database for persistence + // Persist a row for anything worth showing on reload: a digest update, a + // newer-version-tag (semver) suggestion, or both. A pure-semver row (no + // digest update) still persists so the badge survives a page reload. if (envIdNum) { for (const result of results) { - if (result.hasUpdate && !result.systemContainer && !result.updateDisabled) { - await addPendingContainerUpdate( - envIdNum, - result.containerId, - result.containerName, - result.imageName - ); - } + if (result.systemContainer || result.updateDisabled) continue; + const hasImageUpdate = result.hasUpdate; + if (!hasImageUpdate && !result.newerVersion) continue; + await addPendingContainerUpdate( + envIdNum, + result.containerId, + result.containerName, + result.imageName, + { hasImageUpdate, newerVersion: result.newerVersion ?? null } + ); } } diff --git a/src/routes/api/containers/pending-updates/+server.ts b/src/routes/api/containers/pending-updates/+server.ts index eb278a4..e9e3e76 100644 --- a/src/routes/api/containers/pending-updates/+server.ts +++ b/src/routes/api/containers/pending-updates/+server.ts @@ -3,8 +3,24 @@ import type { RequestHandler } from './$types'; import { authorize } from '$lib/server/authorize'; import { getPendingContainerUpdates, removePendingContainerUpdate, clearPendingContainerUpdates } from '$lib/server/db'; +function safeParse(json: string): unknown { + try { + return JSON.parse(json); + } catch { + return null; + } +} + /** * Get pending container updates for an environment. + * + * @openapi + * summary: List the containers in an environment that have a pending image update recorded (requires the 'view' permission) + * query: env:integer! The target environment ID (required) (from GET /api/environments) + * resp-200: {environmentId:integer!, pendingUpdates:array<{containerId:string!, containerName:string!, currentImage:string!, checkedAt:string!, hasImageUpdate:boolean!, newerVersion:object}>!} + * resp-400: Environment ID is required + * resp-403: Permission denied + * resp-500: Failed to get the pending updates */ export const GET: RequestHandler = async ({ url, cookies }) => { const auth = await authorize(cookies); @@ -29,7 +45,9 @@ export const GET: RequestHandler = async ({ url, cookies }) => { containerId: u.containerId, containerName: u.containerName, currentImage: u.currentImage, - checkedAt: u.checkedAt + checkedAt: u.checkedAt, + hasImageUpdate: u.hasImageUpdate, + newerVersion: u.newerVersion ? safeParse(u.newerVersion) : null })) }); } catch (error: any) { @@ -40,6 +58,16 @@ export const GET: RequestHandler = async ({ url, cookies }) => { /** * Remove a pending container update (e.g., after manual update). + * + * @openapi + * summary: Clear pending-update records for an environment, or for a single container when containerId is given + * query: env:integer! The target environment ID (required) (from GET /api/environments) + * query: containerId:string The container to clear; omit to clear ALL pending updates in the environment (from GET /api/containers) + * resp-200: {success:boolean!} + * resp-200-example: {"success":true} + * resp-400: Environment ID is required + * resp-403: Permission denied + * resp-500: Failed to remove the pending update */ export const DELETE: RequestHandler = async ({ url, cookies }) => { const auth = await authorize(cookies); diff --git a/src/routes/api/environments/[id]/update-check/+server.ts b/src/routes/api/environments/[id]/update-check/+server.ts index 2850966..daf13e4 100644 --- a/src/routes/api/environments/[id]/update-check/+server.ts +++ b/src/routes/api/environments/[id]/update-check/+server.ts @@ -10,6 +10,15 @@ import { registerSchedule, unregisterSchedule } from '$lib/server/scheduler'; /** * Get update check settings for an environment. + * + * @openapi + * summary: Get the automatic container-image update-check schedule for an environment + * path: id:integer! Environment id (from GET /api/environments) + * resp-200: {settings:{enabled:boolean!, cron:string!, autoUpdate:boolean!, vulnerabilityCriteria:string!}!} + * resp-200-example: {"settings":{"enabled":false,"cron":"0 4 * * *","autoUpdate":false,"vulnerabilityCriteria":"never"}} + * resp-403: Permission denied (RBAC 'environments:view' missing) + * resp-404: Environment not found + * resp-500: Unexpected error while loading the settings */ export const GET: RequestHandler = async ({ params, cookies }) => { const auth = await authorize(cookies); @@ -44,6 +53,16 @@ export const GET: RequestHandler = async ({ params, cookies }) => { /** * Save update check settings for an environment. + * + * @openapi + * summary: Save the automatic image update-check schedule for an environment (registers/unregisters the croner job) + * path: id:integer! Environment id (from GET /api/environments) + * body: {enabled:boolean, cron:string, autoUpdate:boolean, vulnerabilityCriteria:string, checkPinnedVersions:boolean, semverMaxBump:string, semverMatchFlavor:boolean, semverIncludePrerelease:boolean} + * body-example: {"enabled":true,"cron":"0 4 * * *","autoUpdate":false,"vulnerabilityCriteria":"never","checkPinnedVersions":true,"semverMaxBump":"minor","semverMatchFlavor":true,"semverIncludePrerelease":false} + * resp-200: {success:boolean!, settings:{enabled:boolean!, cron:string!, autoUpdate:boolean!, vulnerabilityCriteria:string!}!} + * resp-403: Permission denied (RBAC 'environments:edit' missing) + * resp-404: Environment not found + * resp-500: Unexpected error while saving the settings */ export const POST: RequestHandler = async ({ params, request, cookies }) => { const auth = await authorize(cookies); @@ -66,7 +85,11 @@ export const POST: RequestHandler = async ({ params, request, cookies }) => { enabled: data.enabled ?? false, cron: data.cron || '0 4 * * *', autoUpdate: data.autoUpdate ?? false, - vulnerabilityCriteria: data.vulnerabilityCriteria || 'never' + vulnerabilityCriteria: data.vulnerabilityCriteria || 'never', + checkPinnedVersions: data.checkPinnedVersions ?? false, + semverMaxBump: data.semverMaxBump || 'major', + semverMatchFlavor: data.semverMatchFlavor ?? true, + semverIncludePrerelease: data.semverIncludePrerelease ?? false }; // Save settings to database diff --git a/src/routes/containers/+page.svelte b/src/routes/containers/+page.svelte index 3dd5844..e0eeb2d 100644 --- a/src/routes/containers/+page.svelte +++ b/src/routes/containers/+page.svelte @@ -65,7 +65,8 @@ Cable, Copy, Loader2, - AlertCircle + AlertCircle, + Tag } from 'lucide-svelte'; import { broom } from '@lucide/lab'; import { copyToClipboard } from '$lib/utils/clipboard'; @@ -78,6 +79,8 @@ import BatchUpdateModal from './BatchUpdateModal.svelte'; import CheckUpdatesButton from '$lib/components/CheckUpdatesButton.svelte'; import BatchOperationModal from '$lib/components/BatchOperationModal.svelte'; + import VersionUpdateBadge from '$lib/components/VersionUpdateBadge.svelte'; + import VersionUpdateModal from '$lib/components/VersionUpdateModal.svelte'; import type { ContainerInfo } from '$lib/types'; import { EmptyState, NoEnvironment } from '$lib/components/ui/empty-state'; import { currentEnvironment, environments, appendEnvParam, clearStaleEnvironment } from '$lib/stores/environment'; @@ -274,6 +277,14 @@ const batchUpdateContainerIds = $derived($containerStore.pendingUpdateIds); const batchUpdateContainerNames = $derived($containerStore.pendingUpdateNames); + // Version-update (semver) release-notes modal — opened from the Tag badge. + let versionModalContainer = $state(null); + let versionModalNewer = $state(null); + function openVersionModal(container: ContainerInfo, newer: import('$lib/server/semver/find-newer').NewerVersion) { + versionModalContainer = container; + versionModalNewer = newer; + } + // Single container update mode (doesn't overwrite batch list) let singleUpdateContainerId = $state(null); let singleUpdateContainerName = $state(null); @@ -307,10 +318,16 @@ // Set of container IDs with updates available (for O(1) lookup) const containersWithUpdatesSet = $derived(new Set(batchUpdateContainerIds)); + // Whether any semver badges are showing (may be true with zero digest updates). + const hasNewerVersions = $derived($containerStore.newerVersions.size > 0); + // Container IDs whose last update check failed (e.g. registry rate-limited) — #1255 const containersWithFailedCheckSet = $derived(new Set($containerStore.failedUpdateIds)); const failedUpdateErrors = $derived($containerStore.failedUpdateErrors); + // Newer-version-tag (semver) suggestions from the last check, keyed by container ID. + const newerVersionsMap = $derived($containerStore.newerVersions); + // Filter dropdown entries: real statuses plus the synthetic // "update-available" entry, only offered once we know about a pending // update — picking it on an empty set would just empty the list (#1063). @@ -466,6 +483,7 @@ function handleUpdateCheckComplete(result: { withUpdates: Array<{ containerId: string; containerName: string }>; failed?: Array<{ containerId: string; error: string }>; + newerVersions?: Array<{ containerId: string; newerVersion: import('$lib/server/semver/find-newer').NewerVersion }>; }) { if (result.withUpdates.length === 0) { containerStore.setPendingUpdates([], new Map()); @@ -483,6 +501,9 @@ failed.map((f) => f.containerId), new Map(failed.map((f) => [f.containerId, f.error])) ); + // Newer-version-tag (semver) suggestions — advisory badge, session-only. + const newer = result.newerVersions ?? []; + containerStore.setNewerVersions(new Map(newer.map((n) => [n.containerId, n.newerVersion]))); } // Load pending updates from database (persisted from check-updates or scheduled jobs) @@ -521,6 +542,8 @@ // Failed-check state is session-only — clear it here too so "Clear" // dismisses the red "check failed" icons alongside the amber ones. containerStore.setFailedUpdates([], new Map()); + // Newer-version (semver) badges are session-only too — dismiss them alongside. + containerStore.setNewerVersions(new Map()); } } catch { toast.error('Failed to clear update indicators'); @@ -1432,6 +1455,24 @@ {/snippet} {/if} + {#if hasNewerVersions} + + + {/if} {#if $canAccess('containers', 'remove')} {/if} + {#if newerVersionsMap.has(container.id)} + openVersionModal(container, newerVersionsMap.get(container.id)!)} + /> + {/if} {container.image} {:else if column.id === 'state'} @@ -2442,6 +2490,12 @@ onComplete={handleBatchUpdateComplete} /> + + ('never'); let updateCheckLoading = $state(false); + // Semver (newer-version-tag) detection — persisted in the same update-check JSON blob. + let semverEnabled = $state(false); + let semverMaxBump = $state<'patch' | 'minor' | 'major'>('major'); + let semverMatchFlavor = $state(true); + let semverIncludePrerelease = $state(false); // Image prune settings state let imagePruneEnabled = $state(false); @@ -1222,12 +1228,20 @@ updateCheckCron = data.settings.cron || '0 4 * * *'; updateCheckAutoUpdate = data.settings.autoUpdate ?? false; updateCheckVulnerabilityCriteria = data.settings.vulnerabilityCriteria || 'never'; + semverEnabled = data.settings.checkPinnedVersions ?? false; + semverMaxBump = data.settings.semverMaxBump || 'major'; + semverMatchFlavor = data.settings.semverMatchFlavor ?? true; + semverIncludePrerelease = data.settings.semverIncludePrerelease ?? false; } else { // No settings found - use defaults updateCheckEnabled = false; updateCheckCron = '0 4 * * *'; updateCheckAutoUpdate = false; updateCheckVulnerabilityCriteria = 'never'; + semverEnabled = false; + semverMaxBump = 'major'; + semverMatchFlavor = true; + semverIncludePrerelease = false; } } } catch (error) { @@ -1246,7 +1260,11 @@ enabled: updateCheckEnabled, cron: updateCheckCron, autoUpdate: updateCheckAutoUpdate, - vulnerabilityCriteria: updateCheckVulnerabilityCriteria + vulnerabilityCriteria: updateCheckVulnerabilityCriteria, + checkPinnedVersions: semverEnabled, + semverMaxBump, + semverMatchFlavor, + semverIncludePrerelease }) }); } catch (error) { @@ -2579,6 +2597,10 @@ bind:updateCheckAutoUpdate={updateCheckAutoUpdate} bind:updateCheckVulnerabilityCriteria={updateCheckVulnerabilityCriteria} scannerEnabled={scannerEnabled} + bind:semverEnabled={semverEnabled} + bind:semverMaxBump={semverMaxBump} + bind:semverMatchFlavor={semverMatchFlavor} + bind:semverIncludePrerelease={semverIncludePrerelease} imagePruneLoading={imagePruneLoading} bind:imagePruneEnabled={imagePruneEnabled} bind:imagePruneCron={imagePruneCron} diff --git a/src/routes/settings/environments/tabs/UpdatesTab.svelte b/src/routes/settings/environments/tabs/UpdatesTab.svelte index 059b22b..a8f5234 100644 --- a/src/routes/settings/environments/tabs/UpdatesTab.svelte +++ b/src/routes/settings/environments/tabs/UpdatesTab.svelte @@ -5,6 +5,7 @@ import CronEditor from '$lib/components/cron-editor.svelte'; import TimezoneSelector from '$lib/components/TimezoneSelector.svelte'; import VulnerabilityCriteriaSelector, { type VulnerabilityCriteria } from '$lib/components/VulnerabilityCriteriaSelector.svelte'; + import SemverCheckConfig from '$lib/components/SemverCheckConfig.svelte'; import { CircleFadingArrowUp, CircleArrowUp, RefreshCw, Info, Trash2 } from 'lucide-svelte'; import { formatDateTime } from '$lib/stores/settings'; import { formatBytes } from '$lib/utils/format'; @@ -17,6 +18,11 @@ updateCheckAutoUpdate: boolean; updateCheckVulnerabilityCriteria: VulnerabilityCriteria; scannerEnabled: boolean; + // Semver (newer-version-tag) detection — rides the same update check + semverEnabled: boolean; + semverMaxBump: 'patch' | 'minor' | 'major'; + semverMatchFlavor: boolean; + semverIncludePrerelease: boolean; // Image prune settings imagePruneLoading: boolean; imagePruneEnabled: boolean; @@ -35,6 +41,10 @@ updateCheckAutoUpdate = $bindable(), updateCheckVulnerabilityCriteria = $bindable(), scannerEnabled, + semverEnabled = $bindable(), + semverMaxBump = $bindable(), + semverMatchFlavor = $bindable(), + semverIncludePrerelease = $bindable(), imagePruneLoading, imagePruneEnabled = $bindable(), imagePruneCron = $bindable(), @@ -118,6 +128,13 @@ You'll receive notifications when updates are available. Containers won't be modified. {/if} + + {/if} {/if} diff --git a/src/routes/stacks/+page.svelte b/src/routes/stacks/+page.svelte index 93efad2..29e7bf9 100644 --- a/src/routes/stacks/+page.svelte +++ b/src/routes/stacks/+page.svelte @@ -14,7 +14,7 @@ import * as Tooltip from '$lib/components/ui/tooltip'; import * as Popover from '$lib/components/ui/popover'; import MultiSelectFilter from '$lib/components/MultiSelectFilter.svelte'; - import { Play, Square, Trash2, Plus, ArrowBigDown, Search, Pencil, ExternalLink, GitBranch, RefreshCw, Loader2, FileCode, FileText, FileOutput, Box, RotateCcw, ScrollText, Terminal, Eye, Network, HardDrive, Heart, HeartPulse, HeartOff, ChevronsUpDown, ChevronsDownUp, Rocket, AlertTriangle, X, Layers, Pause, CircleDashed, Skull, FolderOpen, Variable, Clock, RotateCw, Import, Ship, Cable, LayoutPanelLeft, Rows3, GripVertical, Globe, CircleArrowUp, NotepadText } from 'lucide-svelte'; + import { Play, Square, Trash2, Plus, ArrowBigDown, Search, Pencil, ExternalLink, GitBranch, RefreshCw, Loader2, FileCode, FileText, FileOutput, Box, RotateCcw, ScrollText, Terminal, Eye, Network, HardDrive, Heart, HeartPulse, HeartOff, ChevronsUpDown, ChevronsDownUp, Rocket, AlertTriangle, X, Layers, Pause, CircleDashed, Skull, FolderOpen, Variable, Clock, RotateCw, Import, Ship, Cable, LayoutPanelLeft, Rows3, GripVertical, Globe, CircleArrowUp, NotepadText, Tag } from 'lucide-svelte'; import { formatPorts } from '$lib/utils/port-format'; import { parseCustomUrl } from '$lib/utils/custom-url'; import { extractTraefikUrls } from '$lib/utils/traefik-urls'; @@ -24,7 +24,7 @@ import { appSettings } from '$lib/stores/settings'; import ConfirmPopover from '$lib/components/ConfirmPopover.svelte'; import BatchOperationModal from '$lib/components/BatchOperationModal.svelte'; - import type { ComposeStackInfo, ContainerStats } from '$lib/types'; + import type { ComposeStackInfo, ContainerStats, StackContainer } from '$lib/types'; import StackModal from './StackModal.svelte'; import DeleteStackModal from './DeleteStackModal.svelte'; import GitSourceBadge from './GitSourceBadge.svelte'; @@ -36,6 +36,8 @@ import FileBrowserModal from '../containers/FileBrowserModal.svelte'; import BatchUpdateModal from '../containers/BatchUpdateModal.svelte'; import CheckUpdatesButton from '$lib/components/CheckUpdatesButton.svelte'; + import VersionUpdateBadge from '$lib/components/VersionUpdateBadge.svelte'; + import VersionUpdateModal from '$lib/components/VersionUpdateModal.svelte'; import LogsPanel from '../logs/LogsPanel.svelte'; import { currentEnvironment, environments, appendEnvParam, clearStaleEnvironment } from '$lib/stores/environment'; import { onDockerEvent, isContainerListChange } from '$lib/stores/events'; @@ -81,6 +83,12 @@ let showBatchUpdateModal = $state(false); let singleUpdateContainerId = $state(null); let singleUpdateContainerName = $state(null); + + // Version-update (semver) release-notes modal — opened from a container's Tag badge. + let versionModalContainer = $state(null); + function openVersionModal(container: StackContainer) { + versionModalContainer = container; + } let envHasScanning = $state(false); let envVulnerabilityCriteria = $state<'never' | 'any' | 'critical_high' | 'critical' | 'more_than_current'>('never'); @@ -772,7 +780,8 @@ // True when any stack container shows an update-available (amber) or // check-failed (red) indicator — gates the "dismiss indicators" button. const hasUpdateIndicators = $derived( - stacks.some((s) => s.updatesAvailable) || failedUpdateCheckIds.size > 0 + stacks.some((s) => s.updatesAvailable || (s.newerVersionCount ?? 0) > 0) || + failedUpdateCheckIds.size > 0 ); // Dismiss both the amber "update available" and red "check failed" indicators. @@ -1766,6 +1775,17 @@ {/if} {/if} + {#if (stack.newerVersionCount ?? 0) > 0} + + + + {stack.newerVersionCount} + + + {stack.newerVersionCount} container{(stack.newerVersionCount ?? 0) > 1 ? 's have' : ' has'} a newer version tag. + + + {/if} {:else if column.id === 'source'} {#if source.sourceType === 'git'} @@ -2202,6 +2222,13 @@ {/if} + {#if container.newerVersion} + openVersionModal(container)} + /> + {/if} {#if container.health} @@ -2700,6 +2727,12 @@ onComplete={handleSingleUpdateComplete} /> + +