From c0061b99e3a084459f447cbd580ddbde801a6f8b Mon Sep 17 00:00:00 2001
From: Lukasz Raczylo
Date: Sat, 3 Jan 2026 20:44:23 +0000
Subject: [PATCH] chore(schema): migrate to GORM V2 with multi-database support
- [x] Implement GORM V2 metadata store with SQLite, PostgreSQL, and MySQL support
- [x] Add database migration system using gormigrate for schema versioning
- [x] Create migration CLI tool with support for migrate, rollback, and status commands
- [x] Add Docker support for migration container (Dockerfile.migrate)
- [x] Implement automatic partition management for PostgreSQL time-series tables
- [x] Add background aggregation worker for download statistics
- [x] Support connection pooling configuration (max_open_conns, max_idle_conns, conn_max_lifetime)
- [x] Add blocking mechanism based on vulnerability thresholds in stats and handlers
- [x] Update Helm charts with migration init containers and multi-database configuration
- [x] Replace deprecated SQLite store with optimized GORM implementation
- [x] Add comprehensive integration tests for MySQL and PostgreSQL
- [x] Update frontend to display blocked packages and storage utilization
- [x] Add goreleaser configuration for migrate binary and container image
- [x] Update configuration examples with database backend options and recommendations
---
.goreleaser.yaml | 40 +
Dockerfile.migrate | 30 +
Makefile | 19 +-
cmd/migrate/main.go | 258 ++++
config.yaml.example | 41 +-
frontend/src/components/PackageList.vue | 1 +
frontend/src/components/Stats.vue | 63 +-
.../src/components/VulnerabilityBadge.vue | 12 +
.../src/components/VulnerablePackages.vue | 26 +-
frontend/src/router/index.ts | 5 +
frontend/src/stores/packages.ts | 1 +
go.mod | 72 +-
go.sum | 219 +++-
helm/gohoarder/templates/_helpers.tpl | 2 +-
.../templates/deployment-scanner.yaml | 117 ++
.../templates/deployment-server.yaml | 94 ++
helm/gohoarder/templates/secret.yaml | 13 +
helm/gohoarder/values.yaml | 60 +-
migrate | Bin 0 -> 22530882 bytes
migrations/001_create_schema_v2_mysql.sql | 367 ++++++
migrations/001_create_schema_v2_postgres.sql | 470 +++++++
pkg/app/app.go | 79 +-
pkg/app/handlers.go | 33 +-
pkg/cache/cache.go | 21 +-
pkg/config/config.go | 33 +-
pkg/metadata/gormstore/aggregation_worker.go | 357 ++++++
pkg/metadata/gormstore/config.go | 78 ++
pkg/metadata/gormstore/gormstore_v2.go | 1000 +++++++++++++++
.../gormstore/gormstore_v2_mysql_test.go | 279 +++++
.../gormstore/gormstore_v2_postgres_test.go | 202 +++
pkg/metadata/gormstore/gormstore_v2_test.go | 871 +++++++++++++
pkg/metadata/gormstore/migrations.go | 228 ++++
pkg/metadata/gormstore/models_v2.go | 328 +++++
pkg/metadata/gormstore/partition_manager.go | 380 ++++++
pkg/metadata/interface.go | 19 +-
pkg/metadata/sqlite/sqlite.go | 1096 -----------------
pkg/proxy/pypi/pypi.go | 19 +-
37 files changed, 5711 insertions(+), 1222 deletions(-)
create mode 100644 Dockerfile.migrate
create mode 100644 cmd/migrate/main.go
create mode 100755 migrate
create mode 100644 migrations/001_create_schema_v2_mysql.sql
create mode 100644 migrations/001_create_schema_v2_postgres.sql
create mode 100644 pkg/metadata/gormstore/aggregation_worker.go
create mode 100644 pkg/metadata/gormstore/config.go
create mode 100644 pkg/metadata/gormstore/gormstore_v2.go
create mode 100644 pkg/metadata/gormstore/gormstore_v2_mysql_test.go
create mode 100644 pkg/metadata/gormstore/gormstore_v2_postgres_test.go
create mode 100644 pkg/metadata/gormstore/gormstore_v2_test.go
create mode 100644 pkg/metadata/gormstore/migrations.go
create mode 100644 pkg/metadata/gormstore/models_v2.go
create mode 100644 pkg/metadata/gormstore/partition_manager.go
delete mode 100644 pkg/metadata/sqlite/sqlite.go
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 99a8d22..e8a7332 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -32,6 +32,24 @@ builds:
- -X github.com/lukaszraczylo/gohoarder/internal/version.GitCommit={{.ShortCommit}}
- -X github.com/lukaszraczylo/gohoarder/internal/version.BuildTime={{.Date}}
+ - id: migrate
+ main: ./cmd/migrate
+ binary: migrate
+ env:
+ - CGO_ENABLED=0
+ goos:
+ - linux
+ - darwin
+ - windows
+ goarch:
+ - amd64
+ - arm64
+ ldflags:
+ - -s -w
+ - -X main.Version={{.Version}}
+ - -X main.GitCommit={{.ShortCommit}}
+ - -X main.BuildTime={{.Date}}
+
# Archives for releases
archives:
- id: default
@@ -182,6 +200,28 @@ dockers_v2:
org.opencontainers.image.created: "{{ .Date }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"
+ # 5. Migration Engine - Database migration tool
+ - id: gohoarder-migrate
+ ids:
+ - migrate
+ images:
+ - ghcr.io/lukaszraczylo/gohoarder-migrate
+ tags:
+ - "{{ .Version }}"
+ - latest
+ platforms:
+ - linux/amd64
+ - linux/arm64
+ dockerfile: Dockerfile.migrate
+ labels:
+ org.opencontainers.image.title: GoHoarder Migrate
+ org.opencontainers.image.description: Database migration tool for GoHoarder V2 schema
+ org.opencontainers.image.url: https://github.com/lukaszraczylo/gohoarder
+ org.opencontainers.image.source: https://github.com/lukaszraczylo/gohoarder
+ org.opencontainers.image.version: "{{ .Version }}"
+ org.opencontainers.image.created: "{{ .Date }}"
+ org.opencontainers.image.revision: "{{ .FullCommit }}"
+
# Artifact signing with cosign
signs:
- cmd: cosign
diff --git a/Dockerfile.migrate b/Dockerfile.migrate
new file mode 100644
index 0000000..b5a394e
--- /dev/null
+++ b/Dockerfile.migrate
@@ -0,0 +1,30 @@
+# Migration Engine - Database Migration Tool
+ARG TARGETOS
+ARG TARGETARCH
+
+FROM alpine:latest
+
+# Install runtime dependencies
+RUN apk add --no-cache \
+ ca-certificates \
+ postgresql-client \
+ mysql-client \
+ busybox-extras \
+ && update-ca-certificates
+
+# Create non-root user
+RUN addgroup -g 1000 gohoarder && \
+ adduser -D -u 1000 -G gohoarder gohoarder
+
+# Copy binary (from platform-specific path)
+ARG TARGETOS
+ARG TARGETARCH
+COPY ${TARGETOS}/${TARGETARCH}/migrate /usr/local/bin/migrate
+RUN chmod +x /usr/local/bin/migrate
+
+WORKDIR /app
+USER gohoarder
+
+# Run migrations
+ENTRYPOINT ["/usr/local/bin/migrate"]
+CMD ["--action=migrate"]
diff --git a/Makefile b/Makefile
index 19c9b16..88e3587 100644
--- a/Makefile
+++ b/Makefile
@@ -78,17 +78,24 @@ clean: ## Clean build artifacts
@rm -f *.db *.db-shm *.db-wal
@echo "Clean complete"
-clean-db: ## Clean all local cache and database files (from config.yaml paths)
+clean-db: ## Clean all local cache and database files (requires confirmation)
@echo "WARNING: This will delete all cached packages and scan results!"
- @echo "Paths from config.yaml:"
+ @echo "Paths to be cleaned:"
@echo " - ./data/storage (package cache)"
- @echo " - ./data/gohoarder.db (metadata database)"
+ @echo " - ./data/gohoarder.db and gohoarder.db (metadata database)"
@echo " - /tmp/trivy (Trivy cache)"
@echo ""
- @read -p "Are you sure you want to continue? [y/N] " confirm && [ "$$confirm" = "y" ] || exit 1
+ @printf "Are you sure you want to continue? [y/N] " && read confirm && [ "$$confirm" = "y" ] || (echo "Cancelled." && exit 1)
@echo "Cleaning database and cache..."
- @rm -rf ./data/storage
- @rm -f ./data/gohoarder.db ./data/gohoarder.db-shm ./data/gohoarder.db-wal
+ @rm -rf ./data/storage ./data
+ @rm -f gohoarder.db gohoarder.db-shm gohoarder.db-wal
+ @rm -rf /tmp/trivy
+ @echo "Database and cache cleaned successfully"
+
+clean-db-force: ## Clean all local cache and database files (no confirmation)
+ @echo "Cleaning database and cache..."
+ @rm -rf ./data/storage ./data
+ @rm -f gohoarder.db gohoarder.db-shm gohoarder.db-wal
@rm -rf /tmp/trivy
@echo "Database and cache cleaned successfully"
diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go
new file mode 100644
index 0000000..f45ea7b
--- /dev/null
+++ b/cmd/migrate/main.go
@@ -0,0 +1,258 @@
+package main
+
+import (
+ "context"
+ "database/sql"
+ "flag"
+ "fmt"
+ stdlog "log"
+ "os"
+ "time"
+
+ "github.com/go-gormigrate/gormigrate/v2"
+ _ "github.com/go-sql-driver/mysql"
+ _ "github.com/lib/pq"
+ "github.com/lukaszraczylo/gohoarder/pkg/metadata/gormstore"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+ "gorm.io/driver/mysql"
+ "gorm.io/driver/postgres"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+)
+
+type MigrationConfig struct {
+ Driver string
+ DSN string
+ Timeout time.Duration
+ Action string // migrate, rollback, rollback-to, list
+ TargetID string // For rollback-to
+ LogLevel string
+}
+
+func main() {
+ cfg := MigrationConfig{}
+
+ flag.StringVar(&cfg.Driver, "driver", os.Getenv("DB_DRIVER"), "Database driver (postgres, mysql, sqlite)")
+ flag.StringVar(&cfg.DSN, "dsn", os.Getenv("DATABASE_URL"), "Database connection string")
+ flag.DurationVar(&cfg.Timeout, "timeout", 10*time.Minute, "Migration timeout")
+ flag.StringVar(&cfg.Action, "action", "migrate", "Action: migrate, rollback, rollback-to, list")
+ flag.StringVar(&cfg.TargetID, "target", "", "Target migration ID (for rollback-to)")
+ flag.StringVar(&cfg.LogLevel, "log-level", "info", "Log level: debug, info, warn, error")
+ flag.Parse()
+
+ // Setup logging
+ setupLogging(cfg.LogLevel)
+
+ log.Info().
+ Str("driver", cfg.Driver).
+ Str("action", cfg.Action).
+ Msg("Starting database migration")
+
+ if err := RunMigration(cfg); err != nil {
+ log.Fatal().Err(err).Msg("Migration failed")
+ }
+
+ log.Info().Msg("Migration completed successfully")
+}
+
+func setupLogging(level string) {
+ zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
+
+ switch level {
+ case "debug":
+ zerolog.SetGlobalLevel(zerolog.DebugLevel)
+ case "info":
+ zerolog.SetGlobalLevel(zerolog.InfoLevel)
+ case "warn":
+ zerolog.SetGlobalLevel(zerolog.WarnLevel)
+ case "error":
+ zerolog.SetGlobalLevel(zerolog.ErrorLevel)
+ default:
+ zerolog.SetGlobalLevel(zerolog.InfoLevel)
+ }
+}
+
+func RunMigration(cfg MigrationConfig) error {
+ // Validate config
+ if cfg.Driver == "" {
+ return fmt.Errorf("driver is required (set DB_DRIVER or --driver)")
+ }
+ if cfg.DSN == "" {
+ return fmt.Errorf("DSN is required (set DATABASE_URL or --dsn)")
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
+ defer cancel()
+
+ // Connect to database
+ db, err := connectToDatabase(cfg.Driver, cfg.DSN)
+ if err != nil {
+ return fmt.Errorf("failed to connect to database: %w", err)
+ }
+
+ sqlDB, err := db.DB()
+ if err != nil {
+ return fmt.Errorf("failed to get sql.DB: %w", err)
+ }
+ defer sqlDB.Close()
+
+ // Wait for database to be ready
+ if err := waitForDB(ctx, sqlDB, 60*time.Second); err != nil {
+ return fmt.Errorf("database not ready: %w", err)
+ }
+
+ // Initialize gormigrate with custom options
+ opts := gormigrate.DefaultOptions
+ opts.TableName = "gohoarder_migrations"
+ m := gormigrate.New(db, opts, gormstore.GetMigrations())
+
+ log.Info().
+ Str("table", "gohoarder_migrations").
+ Msg("Migration tracking table initialized")
+
+ // Execute action
+ switch cfg.Action {
+ case "migrate":
+ return runMigrate(m)
+ case "rollback":
+ return runRollback(m)
+ case "rollback-to":
+ if cfg.TargetID == "" {
+ return fmt.Errorf("target migration ID required for rollback-to")
+ }
+ return runRollbackTo(m, cfg.TargetID)
+ case "list":
+ return listMigrations(db)
+ default:
+ return fmt.Errorf("unknown action: %s (use: migrate, rollback, rollback-to, list)", cfg.Action)
+ }
+}
+
+func connectToDatabase(driver, dsn string) (*gorm.DB, error) {
+ // Configure GORM logger using standard library log
+ gormLogger := logger.New(
+ stdlog.New(os.Stdout, "\r\n", stdlog.LstdFlags),
+ logger.Config{
+ SlowThreshold: 200 * time.Millisecond,
+ LogLevel: logger.Info,
+ IgnoreRecordNotFoundError: true,
+ Colorful: true,
+ },
+ )
+
+ var dialector gorm.Dialector
+ switch driver {
+ case "sqlite":
+ dialector = sqlite.Open(dsn)
+ case "postgres", "postgresql":
+ dialector = postgres.Open(dsn)
+ case "mysql":
+ dialector = mysql.Open(dsn)
+ default:
+ return nil, fmt.Errorf("unsupported driver: %s", driver)
+ }
+
+ db, err := gorm.Open(dialector, &gorm.Config{
+ Logger: gormLogger,
+ SkipDefaultTransaction: false, // Migrations should be transactional
+ PrepareStmt: true,
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ return db, nil
+}
+
+func waitForDB(ctx context.Context, db *sql.DB, timeout time.Duration) error {
+ deadline := time.Now().Add(timeout)
+ attempt := 0
+
+ for {
+ attempt++
+ if time.Now().After(deadline) {
+ return fmt.Errorf("timeout waiting for database after %d attempts", attempt)
+ }
+
+ if err := db.PingContext(ctx); err == nil {
+ log.Info().
+ Int("attempts", attempt).
+ Msg("Database is ready")
+ return nil
+ }
+
+ log.Debug().
+ Int("attempt", attempt).
+ Msg("Waiting for database...")
+ time.Sleep(2 * time.Second)
+ }
+}
+
+func runMigrate(m *gormigrate.Gormigrate) error {
+ log.Info().Msg("Running migrations...")
+
+ if err := m.Migrate(); err != nil {
+ return fmt.Errorf("migration failed: %w", err)
+ }
+
+ log.Info().Msg("✓ All migrations applied successfully")
+ return nil
+}
+
+func runRollback(m *gormigrate.Gormigrate) error {
+ log.Warn().Msg("Rolling back last migration...")
+
+ if err := m.RollbackLast(); err != nil {
+ return fmt.Errorf("rollback failed: %w", err)
+ }
+
+ log.Info().Msg("✓ Rollback completed")
+ return nil
+}
+
+func runRollbackTo(m *gormigrate.Gormigrate, targetID string) error {
+ log.Warn().
+ Str("target_id", targetID).
+ Msg("Rolling back to migration...")
+
+ if err := m.RollbackTo(targetID); err != nil {
+ return fmt.Errorf("rollback to %s failed: %w", targetID, err)
+ }
+
+ log.Info().
+ Str("target_id", targetID).
+ Msg("✓ Rollback completed")
+ return nil
+}
+
+func listMigrations(db *gorm.DB) error {
+ log.Info().Msg("Applied migrations:")
+
+ type Migration struct {
+ ID string
+ }
+
+ var migrations []Migration
+ if err := db.Table("gohoarder_migrations").Find(&migrations).Error; err != nil {
+ return fmt.Errorf("failed to list migrations: %w", err)
+ }
+
+ if len(migrations) == 0 {
+ log.Info().Msg(" (no migrations applied yet)")
+ return nil
+ }
+
+ for _, m := range migrations {
+ log.Info().Str("id", m.ID).Msg(" ✓")
+ }
+
+ log.Info().
+ Int("total", len(migrations)).
+ Msg("Applied migrations")
+
+ return nil
+}
diff --git a/config.yaml.example b/config.yaml.example
index 49a5f75..fd5f4bb 100644
--- a/config.yaml.example
+++ b/config.yaml.example
@@ -40,20 +40,53 @@ storage:
domain: ""
metadata:
- backend: "sqlite" # sqlite, postgresql, file
- connection: "file:gohoarder.db?cache=shared&mode=rwc"
+ # Backend: sqlite, postgresql, mysql, mariadb, file
+ #
+ # Choose based on your deployment:
+ # - sqlite: Single instance, local storage (NOT for network filesystems like SMB/NFS!)
+ # - postgresql: Production, multiple replicas, works with any storage including SMB/NFS
+ # - mysql: Production alternative to PostgreSQL
+ # - file: Simple file-based metadata (limited features)
+ #
+ # IMPORTANT: SQLite + SMB/NFS = Database locked errors!
+ # For network storage (SMB, NFS), use PostgreSQL or MySQL.
+ backend: "sqlite"
+ connection: "file:gohoarder.db?cache=shared&mode=rwc" # Legacy, not used with GORM
+ # SQLite configuration (for local storage only)
+ # Use with local storage classes (local-path, hostPath, or RWX like longhorn)
+ # DO NOT use with SMB/NFS network storage!
sqlite:
path: "gohoarder.db"
- wal_mode: true
+ wal_mode: true # Set to false for network filesystems if you must use SQLite
+ # PostgreSQL configuration (recommended for production)
+ # Works with any storage including SMB/NFS
+ # Supports multiple replicas and high availability
postgresql:
host: "localhost"
port: 5432
database: "gohoarder"
user: "gohoarder"
password: ""
- ssl_mode: "disable"
+ ssl_mode: "disable" # disable, require, verify-ca, verify-full
+
+ # MySQL/MariaDB configuration (alternative to PostgreSQL)
+ # Works with any storage including SMB/NFS
+ mysql:
+ host: "localhost"
+ port: 3306
+ database: "gohoarder"
+ user: "gohoarder"
+ password: ""
+ charset: "utf8mb4"
+ parse_time: true
+
+ # GORM connection pool settings (applies to all database backends)
+ max_open_conns: 25 # Maximum number of open connections to the database
+ max_idle_conns: 5 # Maximum number of idle connections in the pool
+ conn_max_lifetime: 3600 # Maximum lifetime of a connection in seconds (1 hour)
+ log_level: "warn" # GORM log level: silent, error, warn, info
cache:
default_ttl: "168h" # 7 days
diff --git a/frontend/src/components/PackageList.vue b/frontend/src/components/PackageList.vue
index 6c49a9a..6593cde 100644
--- a/frontend/src/components/PackageList.vue
+++ b/frontend/src/components/PackageList.vue
@@ -128,6 +128,7 @@
:counts="version.vulnerabilities.counts"
:total="version.vulnerabilities.total"
:scannedAt="version.vulnerabilities.scannedAt"
+ :isBlocked="version.vulnerabilities.isBlocked"
@click="showVulnerabilityDetails(group.registry, group.name, version.version)"
/>
diff --git a/frontend/src/components/Stats.vue b/frontend/src/components/Stats.vue
index 5dc5723..d60acf8 100644
--- a/frontend/src/components/Stats.vue
+++ b/frontend/src/components/Stats.vue
@@ -29,11 +29,26 @@
Total Packages
-
-
- {{ formatBytes(stats?.total_size || 0) }}
-
-
Total Storage Used
+
+
+
+ {{ formatBytes(stats?.total_size || 0) }} / {{ formatBytes(stats?.max_cache_size || 0) }}
+
+
Storage Used
+
+
+
{{ storagePercentage.toFixed(1) }}% used
@@ -51,7 +66,7 @@
Security Scanning
-
+
-
+
{{ formatNumber(stats?.vulnerable_packages || 0) }}
@@ -75,7 +90,23 @@
(click to view)
-
+
+
+
+
+
+ {{ formatNumber(stats?.blocked_packages || 0) }}
+
+
+ Blocked Packages
+ (click to view)
+
+
+
@@ -141,6 +172,14 @@ function showVulnerablePackages() {
router.push('/vulnerable-packages')
}
+function showBlockedPackages() {
+ if ((stats.value?.blocked_packages || 0) === 0) {
+ return
+ }
+
+ router.push('/blocked-packages')
+}
+
// Registry configuration for icons and colors
const registryConfig: Record
= {
npm: {
@@ -180,6 +219,12 @@ const registries = computed(() => {
})
})
+const storagePercentage = computed(() => {
+ const totalSize = stats.value?.total_size || 0
+ const maxSize = stats.value?.max_cache_size || 1
+ return (totalSize / maxSize) * 100
+})
+
function formatNumber(num: number): string {
return new Intl.NumberFormat().format(num)
}
diff --git a/frontend/src/components/VulnerabilityBadge.vue b/frontend/src/components/VulnerabilityBadge.vue
index 085d9c8..05201a5 100644
--- a/frontend/src/components/VulnerabilityBadge.vue
+++ b/frontend/src/components/VulnerabilityBadge.vue
@@ -1,5 +1,15 @@
+
+
+
+ BLOCKED
+
+
(), {
scanned: false,
status: 'not_scanned',
total: 0,
+ isBlocked: false,
})
const emit = defineEmits<{
diff --git a/frontend/src/components/VulnerablePackages.vue b/frontend/src/components/VulnerablePackages.vue
index 3db4db0..b9a7b41 100644
--- a/frontend/src/components/VulnerablePackages.vue
+++ b/frontend/src/components/VulnerablePackages.vue
@@ -7,11 +7,16 @@
Back to Stats
-
+
-
Vulnerable Packages
+
+ {{ showOnlyBlocked ? 'Blocked Packages' : 'Vulnerable Packages' }}
+
- Packages with known security vulnerabilities, sorted by risk
+ {{ showOnlyBlocked
+ ? 'Packages blocked from download due to exceeding vulnerability thresholds'
+ : 'Packages with known security vulnerabilities, sorted by risk'
+ }}
@@ -132,6 +137,7 @@
:counts="version.vulnerabilities.counts"
:total="version.vulnerabilities.total"
:scannedAt="version.vulnerabilities.scannedAt"
+ :isBlocked="version.vulnerabilities.isBlocked"
@click.stop="navigateToPackage(version)"
/>
@@ -170,11 +176,15 @@ import { getRegistryBadgeClass } from '@/composables/useBadgeStyles'
const store = usePackageStore()
const router = useRouter()
+const route = useRoute()
const loading = ref(false)
const error = ref(null)
const vulnerablePackages = ref([])
+// Check if we should filter to show only blocked packages
+const showOnlyBlocked = computed(() => route.path === '/blocked-packages')
+
onMounted(async () => {
await fetchVulnerablePackages()
})
@@ -185,9 +195,13 @@ async function fetchVulnerablePackages() {
try {
await store.fetchPackages()
- vulnerablePackages.value = store.packages.filter(
- pkg => pkg.vulnerabilities?.status === 'vulnerable'
- )
+ vulnerablePackages.value = store.packages.filter(pkg => {
+ const isVulnerable = pkg.vulnerabilities?.status === 'vulnerable'
+ if (showOnlyBlocked.value) {
+ return isVulnerable && pkg.vulnerabilities?.isBlocked === true
+ }
+ return isVulnerable
+ })
} catch (err: any) {
console.error('Failed to load vulnerable packages:', err)
error.value = err.message || 'Failed to load vulnerable packages'
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 206dfe3..1a412cd 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -37,6 +37,11 @@ const router = createRouter({
name: 'vulnerable-packages',
component: VulnerablePackages,
},
+ {
+ path: '/blocked-packages',
+ name: 'blocked-packages',
+ component: VulnerablePackages,
+ },
{
path: '/admin/bypasses',
name: 'bypasses',
diff --git a/frontend/src/stores/packages.ts b/frontend/src/stores/packages.ts
index 6380be2..3d644a8 100644
--- a/frontend/src/stores/packages.ts
+++ b/frontend/src/stores/packages.ts
@@ -15,6 +15,7 @@ export interface VulnerabilityInfo {
counts?: VulnerabilityCounts
total?: number
scannedAt?: string // ISO 8601 timestamp
+ isBlocked?: boolean // Whether download is blocked due to vulnerability thresholds
}
export interface Package {
diff --git a/go.mod b/go.mod
index e51e58d..919a5f8 100644
--- a/go.mod
+++ b/go.mod
@@ -7,23 +7,35 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.32.6
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
+ github.com/go-gormigrate/gormigrate/v2 v2.1.5
+ github.com/go-sql-driver/mysql v1.9.3
github.com/goccy/go-json v0.10.5
github.com/gofiber/fiber/v2 v2.52.10
github.com/gorilla/websocket v1.5.3
github.com/hirochachacha/go-smb2 v1.1.0
+ github.com/lib/pq v1.10.9
github.com/prometheus/client_golang v1.23.2
- github.com/redis/go-redis/v9 v9.17.2
github.com/rs/zerolog v1.34.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
+ github.com/testcontainers/testcontainers-go v0.40.0
+ github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0
+ github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
golang.org/x/crypto v0.46.0
golang.org/x/sync v0.19.0
golang.org/x/time v0.14.0
- modernc.org/sqlite v1.42.2
+ gorm.io/driver/mysql v1.6.0
+ gorm.io/driver/postgres v1.6.0
+ gorm.io/driver/sqlite v1.6.0
+ gorm.io/gorm v1.31.1
)
require (
+ dario.cat/mergo v1.0.2 // indirect
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
@@ -41,45 +53,85 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
+ github.com/containerd/errdefs v1.0.0 // indirect
+ github.com/containerd/errdefs/pkg v0.3.0 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/containerd/platforms v0.2.1 // indirect
+ github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
- github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/docker v28.5.2+incompatible // indirect
+ github.com/docker/go-connections v0.6.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/ebitengine/purego v0.9.1 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/geoffgarside/ber v1.2.0 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+ github.com/jackc/pgx/v5 v5.8.0 // indirect
+ github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.18.2 // indirect
+ github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
+ github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
+ github.com/mattn/go-sqlite3 v1.14.33 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/go-archive v0.2.0 // indirect
+ github.com/moby/patternmatcher v0.6.0 // indirect
+ github.com/moby/sys/sequential v0.6.0 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
+ github.com/moby/sys/userns v0.1.0 // indirect
+ github.com/moby/term v0.5.2 // indirect
+ github.com/morikuni/aec v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
- github.com/ncruces/go-strftime v1.0.0 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
- github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
+ github.com/shirou/gopsutil/v4 v4.25.12 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/tklauser/go-sysconf v0.3.16 // indirect
+ github.com/tklauser/numcpus v0.11.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.68.0 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
+ go.opentelemetry.io/otel v1.39.0 // indirect
+ go.opentelemetry.io/otel/metric v1.39.0 // indirect
+ go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
- golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
+ google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- modernc.org/libc v1.67.4 // indirect
- modernc.org/mathutil v1.7.1 // indirect
- modernc.org/memory v1.11.0 // indirect
)
diff --git a/go.sum b/go.sum
index bf46690..41a7dcc 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,13 @@
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
@@ -40,24 +50,43 @@ github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
-github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
-github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
-github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
+github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
-github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
+github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
+github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -65,6 +94,18 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8
github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
+github.com/go-gormigrate/gormigrate/v2 v2.1.5 h1:1OyorA5LtdQw12cyJDEHuTrEV3GiXiIhS4/QTTa/SM8=
+github.com/go-gormigrate/gormigrate/v2 v2.1.5/go.mod h1:mj9ekk/7CPF3VjopaFvWKN2v7fN3D9d3eEOAXRhi/+M=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
+github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@@ -74,18 +115,28 @@ github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
-github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
-github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI=
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
+github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -94,6 +145,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
+github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -103,15 +160,42 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
+github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
+github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
+github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
+github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
+github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
-github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
+github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
@@ -120,18 +204,18 @@ github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+L
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
-github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
-github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
-github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
-github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
-github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
-github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
+github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
+github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -143,18 +227,53 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
+github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
+github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0 h1:P9Txfy5Jothx2wFdcus0QoSmX/PKSIXZxrTbZPVJswA=
+github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0/go.mod h1:oZPHHqJqXG7FD8OB/yWH7gLnDvZUlFHAVJNrGftL+eg=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ=
+github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
+github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
+github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
+github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
+go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
+go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
+go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
+go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
+go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
+go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
+go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
+go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
+go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
+go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
@@ -165,59 +284,51 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
-golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
-golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
-golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
-golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
+golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
-golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
-golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
+google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
+google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
+google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
-modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
-modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
-modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
-modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
-modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
-modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
-modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
-modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
-modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
-modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
-modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
-modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
-modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
-modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
-modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
-modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
-modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
-modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
-modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
-modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
-modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
-modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
-modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
-modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
-modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
-modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
-modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
+gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
+gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
+gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
+gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
+gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
+gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
+gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
diff --git a/helm/gohoarder/templates/_helpers.tpl b/helm/gohoarder/templates/_helpers.tpl
index 859f65a..d4585a0 100644
--- a/helm/gohoarder/templates/_helpers.tpl
+++ b/helm/gohoarder/templates/_helpers.tpl
@@ -181,7 +181,7 @@ Validate SQLite configuration - SQLite cannot be used with SMB/NFS network stora
{{- if .Values.metadata.sqlite.persistence.enabled }}
{{- $storageClass := .Values.metadata.sqlite.persistence.storageClass | default .Values.storage.storageClass }}
{{- if or (contains "smb" ($storageClass | lower)) (contains "cifs" ($storageClass | lower)) (contains "nfs" ($storageClass | lower)) }}
- {{- fail "\n\n❌ ERROR: SQLite cannot be used with SMB/CIFS/NFS network storage!\n\nSQLite requires POSIX file locking which is not reliably supported over network filesystems.\nThis will cause 'database is locked' errors and data corruption.\n\nPlease choose ONE of the following solutions:\n\n1. Use PostgreSQL for network storage (RECOMMENDED for production):\n metadata:\n backend: postgresql\n postgresql:\n host: your-postgres-host\n ...\n\n2. Use local storage for SQLite (OK for development):\n metadata:\n sqlite:\n persistence:\n enabled: true\n storageClass: local-path # or another local storage class\n\n3. Disable persistence (data will be lost on pod restart):\n metadata:\n sqlite:\n persistence:\n enabled: false\n\nFor more information, see: https://www.sqlite.org/lockingv3.html\n" }}
+ {{- fail "\n\n❌ ERROR: SQLite cannot be used with SMB/CIFS/NFS network storage!\n\nSQLite requires POSIX file locking which is not reliably supported over network filesystems.\nThis will cause 'database is locked' errors and data corruption.\n\nPlease choose ONE of the following solutions:\n\n1. Use PostgreSQL for network storage (RECOMMENDED for production):\n metadata:\n backend: postgresql\n postgresql:\n host: your-postgres-host\n ...\n\n2. Use MySQL/MariaDB for network storage (alternative to PostgreSQL):\n metadata:\n backend: mysql\n mysql:\n host: your-mysql-host\n ...\n\n3. Use local storage for SQLite (OK for development):\n metadata:\n sqlite:\n persistence:\n enabled: true\n storageClass: local-path # or another local storage class\n\n4. Disable persistence (data will be lost on pod restart):\n metadata:\n sqlite:\n persistence:\n enabled: false\n\nFor more information, see: https://www.sqlite.org/lockingv3.html\n" }}
{{- end }}
{{- end }}
{{- end }}
diff --git a/helm/gohoarder/templates/deployment-scanner.yaml b/helm/gohoarder/templates/deployment-scanner.yaml
index d0fbe92..308db42 100644
--- a/helm/gohoarder/templates/deployment-scanner.yaml
+++ b/helm/gohoarder/templates/deployment-scanner.yaml
@@ -29,6 +29,77 @@ spec:
serviceAccountName: {{ include "gohoarder.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
+ {{- if .Values.migration.enabled }}
+ initContainers:
+ # Wait for database to be ready
+ - name: wait-for-db
+ image: busybox:1.36
+ command:
+ - sh
+ - -c
+ - |
+ echo "Waiting for database..."
+ {{- if eq .Values.metadata.backend "postgresql" }}
+ until nc -z {{ .Values.metadata.postgresql.host }} {{ .Values.metadata.postgresql.port }}; do
+ echo " PostgreSQL not ready, retrying in 2s..."
+ sleep 2
+ done
+ echo "✓ PostgreSQL is ready"
+ {{- else if eq .Values.metadata.backend "mysql" }}
+ until nc -z {{ .Values.metadata.mysql.host }} {{ .Values.metadata.mysql.port }}; do
+ echo " MySQL not ready, retrying in 2s..."
+ sleep 2
+ done
+ echo "✓ MySQL is ready"
+ {{- else }}
+ echo "✓ SQLite (no wait needed)"
+ {{- end }}
+ securityContext:
+ allowPrivilegeEscalation: false
+ runAsNonRoot: true
+ runAsUser: 1000
+ resources:
+ limits:
+ cpu: 100m
+ memory: 64Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ # Run database migrations
+ - name: migrate
+ image: "{{ .Values.migration.image.repository }}:{{ .Values.migration.image.tag | default .Chart.AppVersion }}"
+ imagePullPolicy: {{ .Values.migration.image.pullPolicy }}
+ env:
+ - name: DB_DRIVER
+ value: {{ .Values.metadata.backend | quote }}
+ {{- if eq .Values.metadata.backend "postgresql" }}
+ - name: DATABASE_URL
+ value: "postgresql://{{ .Values.metadata.postgresql.username }}:{{ .Values.metadata.postgresql.password }}@{{ .Values.metadata.postgresql.host }}:{{ .Values.metadata.postgresql.port }}/{{ .Values.metadata.postgresql.database }}?sslmode={{ .Values.metadata.postgresql.sslMode }}"
+ {{- else if eq .Values.metadata.backend "mysql" }}
+ - name: DATABASE_URL
+ value: "{{ .Values.metadata.mysql.username }}:{{ .Values.metadata.mysql.password }}@tcp({{ .Values.metadata.mysql.host }}:{{ .Values.metadata.mysql.port }})/{{ .Values.metadata.mysql.database }}?charset={{ .Values.metadata.mysql.charset }}&parseTime={{ .Values.metadata.mysql.parseTime }}"
+ {{- else }}
+ - name: DATABASE_URL
+ value: "/var/lib/gohoarder/metadata/gohoarder.db"
+ {{- end }}
+ args:
+ - --driver=$(DB_DRIVER)
+ - --dsn=$(DATABASE_URL)
+ - --action=migrate
+ - --log-level={{ .Values.migration.logLevel | default "info" }}
+ - --timeout={{ .Values.migration.timeout | default "5m" }}
+ securityContext:
+ allowPrivilegeEscalation: false
+ runAsNonRoot: true
+ runAsUser: 1000
+ resources:
+ {{- toYaml .Values.migration.resources | nindent 10 }}
+ {{- if eq .Values.metadata.backend "sqlite" }}
+ volumeMounts:
+ - name: metadata
+ mountPath: /var/lib/gohoarder/metadata
+ {{- end }}
+ {{- end }}
containers:
- name: scanner
securityContext:
@@ -38,6 +109,52 @@ spec:
env:
- name: CONFIG_FILE
value: /etc/gohoarder/config.yaml
+ {{- if and (eq .Values.metadata.backend "postgresql") .Values.metadata.postgresql.existingSecret }}
+ - name: POSTGRES_USER
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.metadata.postgresql.existingSecret }}
+ key: username
+ - name: POSTGRES_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.metadata.postgresql.existingSecret }}
+ key: password
+ {{- else if and (eq .Values.metadata.backend "postgresql") .Values.metadata.postgresql.username }}
+ - name: POSTGRES_USER
+ valueFrom:
+ secretKeyRef:
+ name: {{ include "gohoarder.fullname" . }}-postgresql
+ key: username
+ - name: POSTGRES_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ include "gohoarder.fullname" . }}-postgresql
+ key: password
+ {{- end }}
+ {{- if and (or (eq .Values.metadata.backend "mysql") (eq .Values.metadata.backend "mariadb")) .Values.metadata.mysql.existingSecret }}
+ - name: MYSQL_USER
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.metadata.mysql.existingSecret }}
+ key: username
+ - name: MYSQL_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.metadata.mysql.existingSecret }}
+ key: password
+ {{- else if and (or (eq .Values.metadata.backend "mysql") (eq .Values.metadata.backend "mariadb")) .Values.metadata.mysql.username }}
+ - name: MYSQL_USER
+ valueFrom:
+ secretKeyRef:
+ name: {{ include "gohoarder.fullname" . }}-mysql
+ key: username
+ - name: MYSQL_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ include "gohoarder.fullname" . }}-mysql
+ key: password
+ {{- end }}
{{- if and .Values.security.scanners.ghsa.enabled .Values.security.scanners.ghsa.existingSecret }}
- name: GHSA_TOKEN
valueFrom:
diff --git a/helm/gohoarder/templates/deployment-server.yaml b/helm/gohoarder/templates/deployment-server.yaml
index 92dd164..3cfb70c 100644
--- a/helm/gohoarder/templates/deployment-server.yaml
+++ b/helm/gohoarder/templates/deployment-server.yaml
@@ -30,6 +30,77 @@ spec:
serviceAccountName: {{ include "gohoarder.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
+ {{- if .Values.migration.enabled }}
+ initContainers:
+ # Wait for database to be ready
+ - name: wait-for-db
+ image: busybox:1.36
+ command:
+ - sh
+ - -c
+ - |
+ echo "Waiting for database..."
+ {{- if eq .Values.metadata.backend "postgresql" }}
+ until nc -z {{ .Values.metadata.postgresql.host }} {{ .Values.metadata.postgresql.port }}; do
+ echo " PostgreSQL not ready, retrying in 2s..."
+ sleep 2
+ done
+ echo "✓ PostgreSQL is ready"
+ {{- else if eq .Values.metadata.backend "mysql" }}
+ until nc -z {{ .Values.metadata.mysql.host }} {{ .Values.metadata.mysql.port }}; do
+ echo " MySQL not ready, retrying in 2s..."
+ sleep 2
+ done
+ echo "✓ MySQL is ready"
+ {{- else }}
+ echo "✓ SQLite (no wait needed)"
+ {{- end }}
+ securityContext:
+ allowPrivilegeEscalation: false
+ runAsNonRoot: true
+ runAsUser: 1000
+ resources:
+ limits:
+ cpu: 100m
+ memory: 64Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ # Run database migrations
+ - name: migrate
+ image: "{{ .Values.migration.image.repository }}:{{ .Values.migration.image.tag | default .Chart.AppVersion }}"
+ imagePullPolicy: {{ .Values.migration.image.pullPolicy }}
+ env:
+ - name: DB_DRIVER
+ value: {{ .Values.metadata.backend | quote }}
+ {{- if eq .Values.metadata.backend "postgresql" }}
+ - name: DATABASE_URL
+ value: "postgresql://{{ .Values.metadata.postgresql.username }}:{{ .Values.metadata.postgresql.password }}@{{ .Values.metadata.postgresql.host }}:{{ .Values.metadata.postgresql.port }}/{{ .Values.metadata.postgresql.database }}?sslmode={{ .Values.metadata.postgresql.sslMode }}"
+ {{- else if eq .Values.metadata.backend "mysql" }}
+ - name: DATABASE_URL
+ value: "{{ .Values.metadata.mysql.username }}:{{ .Values.metadata.mysql.password }}@tcp({{ .Values.metadata.mysql.host }}:{{ .Values.metadata.mysql.port }})/{{ .Values.metadata.mysql.database }}?charset={{ .Values.metadata.mysql.charset }}&parseTime={{ .Values.metadata.mysql.parseTime }}"
+ {{- else }}
+ - name: DATABASE_URL
+ value: "/var/lib/gohoarder/metadata/gohoarder.db"
+ {{- end }}
+ args:
+ - --driver=$(DB_DRIVER)
+ - --dsn=$(DATABASE_URL)
+ - --action=migrate
+ - --log-level={{ .Values.migration.logLevel | default "info" }}
+ - --timeout={{ .Values.migration.timeout | default "5m" }}
+ securityContext:
+ allowPrivilegeEscalation: false
+ runAsNonRoot: true
+ runAsUser: 1000
+ resources:
+ {{- toYaml .Values.migration.resources | nindent 10 }}
+ {{- if eq .Values.metadata.backend "sqlite" }}
+ volumeMounts:
+ - name: metadata
+ mountPath: /var/lib/gohoarder/metadata
+ {{- end }}
+ {{- end }}
containers:
- name: server
securityContext:
@@ -125,6 +196,29 @@ spec:
name: {{ include "gohoarder.fullname" . }}-postgresql
key: password
{{- end }}
+ {{- if and (or (eq .Values.metadata.backend "mysql") (eq .Values.metadata.backend "mariadb")) .Values.metadata.mysql.existingSecret }}
+ - name: MYSQL_USER
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.metadata.mysql.existingSecret }}
+ key: username
+ - name: MYSQL_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.metadata.mysql.existingSecret }}
+ key: password
+ {{- else if and (or (eq .Values.metadata.backend "mysql") (eq .Values.metadata.backend "mariadb")) .Values.metadata.mysql.username }}
+ - name: MYSQL_USER
+ valueFrom:
+ secretKeyRef:
+ name: {{ include "gohoarder.fullname" . }}-mysql
+ key: username
+ - name: MYSQL_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ include "gohoarder.fullname" . }}-mysql
+ key: password
+ {{- end }}
{{- if and .Values.security.scanners.ghsa.enabled .Values.security.scanners.ghsa.existingSecret }}
- name: GHSA_TOKEN
valueFrom:
diff --git a/helm/gohoarder/templates/secret.yaml b/helm/gohoarder/templates/secret.yaml
index cfa1876..656ffd9 100644
--- a/helm/gohoarder/templates/secret.yaml
+++ b/helm/gohoarder/templates/secret.yaml
@@ -53,6 +53,19 @@ data:
password: {{ .Values.metadata.postgresql.password | b64enc | quote }}
{{- end }}
---
+{{- if and (or (eq .Values.metadata.backend "mysql") (eq .Values.metadata.backend "mariadb")) (not .Values.metadata.mysql.existingSecret) .Values.metadata.mysql.username }}
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ include "gohoarder.fullname" . }}-mysql
+ labels:
+ {{- include "gohoarder.labels" . | nindent 4 }}
+type: Opaque
+data:
+ username: {{ .Values.metadata.mysql.username | b64enc | quote }}
+ password: {{ .Values.metadata.mysql.password | b64enc | quote }}
+{{- end }}
+---
{{- if and .Values.security.scanners.ghsa.enabled (not .Values.security.scanners.ghsa.existingSecret) .Values.security.scanners.ghsa.token }}
apiVersion: v1
kind: Secret
diff --git a/helm/gohoarder/values.yaml b/helm/gohoarder/values.yaml
index 205f54f..3632c83 100644
--- a/helm/gohoarder/values.yaml
+++ b/helm/gohoarder/values.yaml
@@ -272,7 +272,7 @@ storage:
# Metadata storage configuration
metadata:
- # Backend: sqlite, postgresql
+ # Backend: sqlite, postgresql, mysql
#
# IMPORTANT: SQLite CANNOT be used with SMB/CIFS/NFS network storage!
# SQLite requires POSIX file locking which causes "database is locked" errors on network filesystems.
@@ -286,6 +286,13 @@ metadata:
# 2. PostgreSQL with any storage (RECOMMENDED for production)
# - Set backend: postgresql
# - Configure postgresql settings below
+ # - Works with any storage including SMB/NFS
+ # - Supports multiple replicas and high availability
+ #
+ # 3. MySQL/MariaDB with any storage (alternative to PostgreSQL)
+ # - Set backend: mysql
+ # - Configure mysql settings below
+ # - Works with any storage including SMB/NFS
#
backend: "sqlite"
@@ -305,6 +312,8 @@ metadata:
walMode: false
# PostgreSQL configuration
+ # Works with any storage including SMB/NFS
+ # Recommended for production deployments
postgresql:
# Use bundled PostgreSQL (sets up postgresql subchart)
enabled: false
@@ -313,10 +322,57 @@ metadata:
database: "gohoarder"
username: "gohoarder"
password: ""
- sslMode: "disable"
+ sslMode: "disable" # disable, require, verify-ca, verify-full
# Use existing secret for PostgreSQL credentials
existingSecret: ""
+ # MySQL/MariaDB configuration
+ # Works with any storage including SMB/NFS
+ # Alternative to PostgreSQL for production deployments
+ mysql:
+ host: "localhost"
+ port: 3306
+ database: "gohoarder"
+ username: "gohoarder"
+ password: ""
+ charset: "utf8mb4"
+ parseTime: true
+ # Use existing secret for MySQL credentials
+ existingSecret: ""
+
+ # GORM connection pool settings (applies to all database backends)
+ # These settings control database connection pooling and performance
+ maxOpenConns: 25 # Maximum number of open connections to the database
+ maxIdleConns: 5 # Maximum number of idle connections in the pool
+ connMaxLifetime: 3600 # Maximum lifetime of a connection in seconds (1 hour)
+ logLevel: "warn" # GORM log level: silent, error, warn, info
+
+# Database migration configuration
+migration:
+ # Enable automatic database migrations via init containers
+ # When enabled, each pod will run migrations before starting the main container
+ # Gormigrate handles concurrency automatically - safe for multiple pods
+ enabled: true
+
+ # Migration image configuration
+ image:
+ repository: ghcr.io/lukaszraczylo/gohoarder-migrate
+ pullPolicy: IfNotPresent
+ tag: "latest" # Should match the application version
+
+ # Migration settings
+ logLevel: "info" # debug, info, warn, error
+ timeout: "5m" # Maximum time for migrations to complete
+
+ # Resource limits for migration init container
+ resources:
+ limits:
+ cpu: 500m
+ memory: 256Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+
# Cache configuration
cache:
defaultTTL: "168h" # 7 days
diff --git a/migrate b/migrate
new file mode 100755
index 0000000000000000000000000000000000000000..0f5111546d13d7b2a8e0a8ac596b84695370ab94
GIT binary patch
literal 22530882
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