store.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. package dbstore
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "os"
  7. "git.linuxforward.com/byop/byop-engine/models"
  8. _ "github.com/mattn/go-sqlite3" // Import SQLite driver
  9. )
  10. // Store defines the interface for all database operations.
  11. // This will include methods for all models (User, Client, App, Component, Deployment, Ticket, etc.)
  12. type Store interface {
  13. // User methods
  14. CreateUser(ctx context.Context, user models.User) (int, error) // Updated signature
  15. GetUserByEmail(ctx context.Context, email string) (*models.User, error)
  16. GetUserByID(ctx context.Context, id int) (*models.User, error)
  17. GetUsers(ctx context.Context) ([]models.User, error) // Method to get all users
  18. UpdateUser(ctx context.Context, user *models.User) error
  19. DeleteUser(ctx context.Context, id int64) error // Updated signature
  20. // ... other user methods
  21. // Client methods
  22. CreateClient(ctx context.Context, client models.Client) (int, error) // Corrected signature
  23. GetClientByID(ctx context.Context, id int) (*models.Client, error) // Changed to pointer
  24. GetClients(ctx context.Context) ([]models.Client, error)
  25. UpdateClient(ctx context.Context, client *models.Client) error // Changed to pointer
  26. DeleteClient(ctx context.Context, id int) error
  27. // ... other client methods
  28. // App methods
  29. CreateApp(ctx context.Context, app *models.App) (int, error) // Corrected signature
  30. GetAppByID(ctx context.Context, id int) (*models.App, error)
  31. GetAppsByUserID(ctx context.Context, userID int) ([]models.App, error) // Added method
  32. UpdateApp(ctx context.Context, app *models.App) error
  33. DeleteApp(ctx context.Context, id int) error
  34. UpdateAppStatus(ctx context.Context, appID int, status string, message string) error // Added, changed models.AppStatus to string
  35. UpdateAppPreview(ctx context.Context, appID int, previewID int, previewURL string) error // Added
  36. GetAllApps(ctx context.Context) ([]*models.App, error) // Updated signature
  37. UpdateAppCurrentImage(ctx context.Context, appID int, imageTag string, imageURI string) error // Added
  38. // ... other app methods
  39. // Component methods
  40. CreateComponent(ctx context.Context, component *models.Component) (int, error) // Updated signature
  41. GetComponentByID(ctx context.Context, id int) (*models.Component, error)
  42. GetComponentsByUserID(ctx context.Context, userID int) ([]models.Component, error)
  43. UpdateComponent(ctx context.Context, component *models.Component) error
  44. DeleteComponent(ctx context.Context, id int) error
  45. // ... other component methods
  46. // Deployment methods
  47. CreateDeployment(ctx context.Context, deployment models.Deployment) (int, error) // Updated signature
  48. GetDeploymentByID(ctx context.Context, id int) (*models.Deployment, error)
  49. GetDeploymentsByAppID(ctx context.Context, appID int) ([]models.Deployment, error)
  50. GetDeploymentsByClientID(ctx context.Context, clientID int) ([]models.Deployment, error)
  51. GetDeploymentsByUserID(ctx context.Context, userID int) ([]models.Deployment, error) // Assuming deployments can be linked to users indirectly
  52. UpdateDeployment(ctx context.Context, deployment *models.Deployment) error
  53. DeleteDeployment(ctx context.Context, id int) error
  54. // ... other deployment methods
  55. // Preview methods
  56. CreatePreview(ctx context.Context, preview *models.Preview) (int, error) // Corrected signature
  57. GetPreviewByID(ctx context.Context, id int) (*models.Preview, error)
  58. GetPreviewByAppID(ctx context.Context, appID int) (*models.Preview, error)
  59. UpdatePreview(ctx context.Context, preview *models.Preview) error
  60. DeletePreview(ctx context.Context, id int) error
  61. UpdatePreviewVPS(ctx context.Context, previewID int, vpsID string, ipAddress string, previewURL string) error // Added
  62. UpdatePreviewStatus(ctx context.Context, previewID int, status string, errorMsg string) error // Added, changed models.PreviewStatus to string
  63. UpdatePreviewBuildLogs(ctx context.Context, previewID int, logs string) error // Added
  64. UpdatePreviewDeployLogs(ctx context.Context, previewID int, logs string) error // Added
  65. GetPreviewsByStatus(ctx context.Context, status string) ([]models.Preview, error) // Added, changed models.PreviewStatus to string
  66. GetPreviewsByAppID(ctx context.Context, appID int) ([]models.Preview, error) // Added
  67. // ... other preview methods
  68. // Ticket methods
  69. CreateTicket(ctx context.Context, ticket *models.Ticket) error
  70. GetTicketByID(ctx context.Context, id int) (*models.Ticket, error)
  71. GetTickets(ctx context.Context) ([]models.Ticket, error) // Add filters later (status, user, client)
  72. UpdateTicket(ctx context.Context, ticket *models.Ticket) error
  73. // DeleteTicket(ctx context.Context, id int) error // Optional
  74. // TicketComment methods
  75. CreateTicketComment(ctx context.Context, comment *models.TicketComment) error
  76. GetTicketComments(ctx context.Context, ticketID int) ([]models.TicketComment, error)
  77. // ... other ticket comment methods
  78. // BuildJob methods
  79. CreateBuildJob(ctx context.Context, job *models.BuildJob) error
  80. GetBuildJobByID(ctx context.Context, id uint) (*models.BuildJob, error)
  81. UpdateBuildJob(ctx context.Context, job *models.BuildJob) error
  82. UpdateBuildJobStatus(ctx context.Context, id uint, status models.BuildStatus, errorMessage string) error
  83. AppendBuildJobLog(ctx context.Context, id uint, logMessage string) error
  84. GetQueuedBuildJobs(ctx context.Context, limit int) ([]models.BuildJob, error)
  85. GetBuildJobsByAppID(ctx context.Context, appID uint, page, pageSize int) ([]models.BuildJob, int64, error)
  86. // General DB methods
  87. GetDB() *sql.DB
  88. Close() error
  89. }
  90. // SQLiteStore implements the Store interface for SQLite using GORM
  91. type SQLiteStore struct {
  92. db *sql.DB
  93. dsn string
  94. }
  95. // NewSQLiteStore initializes a new SQLiteStore
  96. func NewSQLiteStore(dataSourceName string) (*SQLiteStore, error) {
  97. // First check if the database file exists
  98. isNewDb := !fileExists(dataSourceName)
  99. if isNewDb {
  100. // Create the database file if it doesn't exist
  101. file, err := os.Create(dataSourceName)
  102. if err != nil {
  103. return nil, fmt.Errorf("failed to create SQLite database file: %w", err)
  104. }
  105. defer file.Close()
  106. }
  107. // Open SQLite database and SQLite-specific configuration
  108. db, err := sql.Open("sqlite3", dataSourceName)
  109. if err != nil {
  110. return nil, fmt.Errorf("failed to open database: %w", err)
  111. }
  112. if err := db.Ping(); err != nil {
  113. return nil, fmt.Errorf("failed to ping database: %w", err)
  114. }
  115. err = createTables(db)
  116. if err != nil {
  117. return nil, fmt.Errorf("failed to create tables: %w", err)
  118. }
  119. // Run migrations after table creation
  120. err = runMigrations(db)
  121. if err != nil {
  122. return nil, fmt.Errorf("failed to run migrations: %w", err)
  123. }
  124. // Enable foreign keys in SQLite after migrations
  125. _, err = db.Exec("PRAGMA foreign_keys = ON")
  126. if err != nil {
  127. return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
  128. }
  129. // Check if the database is well-formed
  130. if isNewDb {
  131. // If this is a new database, we can assume it's well-formed after creating tables
  132. } else {
  133. // If the database already exists, we can run a simple query to check its integrity
  134. var count int
  135. err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table'").Scan(&count)
  136. if err != nil {
  137. return nil, fmt.Errorf("failed to check database integrity: %w", err)
  138. }
  139. if count == 0 {
  140. return nil, fmt.Errorf("database is empty or not well-formed")
  141. }
  142. }
  143. return &SQLiteStore{
  144. db: db,
  145. dsn: dataSourceName,
  146. }, nil
  147. }
  148. // createTables creates all necessary tables
  149. func createTables(db *sql.DB) error {
  150. queries := []string{
  151. `CREATE TABLE IF NOT EXISTS users (
  152. id INTEGER PRIMARY KEY AUTOINCREMENT,
  153. email TEXT UNIQUE NOT NULL,
  154. password TEXT NOT NULL,
  155. name TEXT NOT NULL,
  156. role TEXT DEFAULT 'user',
  157. active BOOLEAN DEFAULT true,
  158. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  159. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  160. )`,
  161. `CREATE TABLE IF NOT EXISTS clients (
  162. id INTEGER PRIMARY KEY AUTOINCREMENT,
  163. name TEXT NOT NULL,
  164. description TEXT,
  165. contact_info TEXT,
  166. active BOOLEAN DEFAULT true,
  167. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  168. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  169. )`,
  170. `CREATE TABLE IF NOT EXISTS components (
  171. id INTEGER PRIMARY KEY AUTOINCREMENT,
  172. user_id INTEGER NOT NULL,
  173. name TEXT NOT NULL,
  174. description TEXT,
  175. type TEXT,
  176. status TEXT DEFAULT 'active',
  177. config TEXT DEFAULT '{}',
  178. repository TEXT,
  179. branch TEXT DEFAULT 'main',
  180. error_msg TEXT,
  181. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  182. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  183. FOREIGN KEY (user_id) REFERENCES users(id)
  184. )`,
  185. `CREATE TABLE IF NOT EXISTS deployments (
  186. id INTEGER PRIMARY KEY AUTOINCREMENT,
  187. app_id INTEGER NOT NULL,
  188. client_id INTEGER NOT NULL,
  189. name TEXT NOT NULL,
  190. description TEXT,
  191. environment TEXT DEFAULT 'development',
  192. status TEXT DEFAULT 'pending',
  193. url TEXT,
  194. config TEXT DEFAULT '{}',
  195. deployed_at DATETIME,
  196. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  197. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  198. FOREIGN KEY (app_id) REFERENCES apps(id),
  199. FOREIGN KEY (client_id) REFERENCES clients(id)
  200. )`,
  201. `CREATE TABLE IF NOT EXISTS apps (
  202. id INTEGER PRIMARY KEY AUTOINCREMENT,
  203. user_id INTEGER NOT NULL,
  204. name TEXT NOT NULL,
  205. description TEXT,
  206. status TEXT DEFAULT 'building',
  207. components TEXT DEFAULT '[]', -- JSON array of component IDs
  208. preview_id INTEGER,
  209. preview_url TEXT DEFAULT '',
  210. current_image_tag TEXT DEFAULT '',
  211. current_image_uri TEXT DEFAULT '',
  212. error_msg TEXT DEFAULT '',
  213. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  214. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  215. FOREIGN KEY (user_id) REFERENCES users(id),
  216. FOREIGN KEY (preview_id) REFERENCES previews(id) ON DELETE SET NULL
  217. )`,
  218. `CREATE TABLE IF NOT EXISTS providers (
  219. id INTEGER PRIMARY KEY AUTOINCREMENT,
  220. name TEXT NOT NULL,
  221. type TEXT NOT NULL,
  222. config TEXT DEFAULT '{}',
  223. active BOOLEAN DEFAULT true,
  224. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  225. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  226. )`,
  227. `CREATE TABLE IF NOT EXISTS tickets (
  228. id INTEGER PRIMARY KEY AUTOINCREMENT,
  229. client_id INTEGER NOT NULL,
  230. title TEXT NOT NULL,
  231. description TEXT,
  232. status TEXT DEFAULT 'open',
  233. priority TEXT DEFAULT 'medium',
  234. assigned_to INTEGER,
  235. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  236. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  237. FOREIGN KEY (client_id) REFERENCES clients(id),
  238. FOREIGN KEY (assigned_to) REFERENCES users(id)
  239. )`,
  240. `CREATE TABLE IF NOT EXISTS previews (
  241. id INTEGER PRIMARY KEY AUTOINCREMENT,
  242. app_id INTEGER NOT NULL,
  243. status TEXT NOT NULL DEFAULT 'building',
  244. url TEXT DEFAULT '',
  245. vps_id TEXT DEFAULT '',
  246. ip_address TEXT DEFAULT '',
  247. error_msg TEXT DEFAULT '',
  248. build_logs TEXT DEFAULT '',
  249. deploy_logs TEXT DEFAULT '',
  250. expires_at TEXT NOT NULL,
  251. created_at TEXT DEFAULT CURRENT_TIMESTAMP,
  252. updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
  253. FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
  254. );`,
  255. `CREATE TABLE IF NOT EXISTS ticket_comments (
  256. id INTEGER PRIMARY KEY AUTOINCREMENT,
  257. ticket_id INTEGER NOT NULL,
  258. user_id INTEGER NOT NULL,
  259. content TEXT NOT NULL,
  260. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  261. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  262. FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
  263. FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
  264. )`,
  265. `CREATE TABLE IF NOT EXISTS build_jobs (
  266. id INTEGER PRIMARY KEY AUTOINCREMENT,
  267. component_id INTEGER NOT NULL,
  268. request_id TEXT UNIQUE,
  269. source_url TEXT NOT NULL,
  270. version TEXT,
  271. status TEXT NOT NULL,
  272. image_name TEXT,
  273. image_tag TEXT,
  274. full_image_uri TEXT,
  275. registry_url TEXT,
  276. registry_user TEXT,
  277. registry_password TEXT,
  278. build_context TEXT,
  279. dockerfile TEXT,
  280. llb_definition BLOB,
  281. dockerfile_content TEXT,
  282. no_cache BOOLEAN,
  283. build_args TEXT,
  284. logs TEXT,
  285. error_message TEXT,
  286. requested_at DATETIME NOT NULL,
  287. started_at DATETIME,
  288. finished_at DATETIME,
  289. worker_node_id TEXT,
  290. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  291. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  292. FOREIGN KEY (component_id) REFERENCES components(id) ON DELETE CASCADE
  293. )`,
  294. }
  295. for _, query := range queries {
  296. if _, err := db.Exec(query); err != nil {
  297. return fmt.Errorf("failed to create table: %w", err)
  298. }
  299. }
  300. return nil
  301. }
  302. // runMigrations handles database schema migrations for existing databases
  303. func runMigrations(db *sql.DB) error {
  304. // Migration 1: Add image tracking columns to components table
  305. err := addComponentImageColumns(db)
  306. if err != nil {
  307. return fmt.Errorf("failed to add image columns to components table: %w", err)
  308. }
  309. // Migration 2: Add dockerfile_content column to build_jobs table
  310. err = addDockerfileContentColumn(db)
  311. if err != nil {
  312. return fmt.Errorf("failed to add dockerfile_content column to build_jobs table: %w", err)
  313. }
  314. return nil
  315. }
  316. // addComponentImageColumns adds current_image_tag and current_image_uri columns to components table
  317. func addComponentImageColumns(db *sql.DB) error {
  318. // Check if columns already exist
  319. var count int
  320. err := db.QueryRow(`
  321. SELECT COUNT(*)
  322. FROM pragma_table_info('components')
  323. WHERE name IN ('current_image_tag', 'current_image_uri')
  324. `).Scan(&count)
  325. if err != nil {
  326. return fmt.Errorf("failed to check existing columns: %w", err)
  327. }
  328. // If both columns already exist, skip migration
  329. if count >= 2 {
  330. return nil
  331. }
  332. // Add the missing columns
  333. migrations := []string{
  334. `ALTER TABLE components ADD COLUMN current_image_tag TEXT DEFAULT ''`,
  335. `ALTER TABLE components ADD COLUMN current_image_uri TEXT DEFAULT ''`,
  336. }
  337. for _, migration := range migrations {
  338. _, err := db.Exec(migration)
  339. if err != nil {
  340. // Ignore "duplicate column name" errors in case the column already exists
  341. if err.Error() != "duplicate column name: current_image_tag" &&
  342. err.Error() != "duplicate column name: current_image_uri" {
  343. return fmt.Errorf("failed to execute migration '%s': %w", migration, err)
  344. }
  345. }
  346. }
  347. return nil
  348. }
  349. // addDockerfileContentColumn adds dockerfile_content column to build_jobs table
  350. func addDockerfileContentColumn(db *sql.DB) error {
  351. // Check if column already exists
  352. var count int
  353. err := db.QueryRow(`
  354. SELECT COUNT(*)
  355. FROM pragma_table_info('build_jobs')
  356. WHERE name = 'dockerfile_content'
  357. `).Scan(&count)
  358. if err != nil {
  359. return fmt.Errorf("failed to check dockerfile_content column: %w", err)
  360. }
  361. // If column already exists, skip migration
  362. if count > 0 {
  363. return nil
  364. }
  365. // Add the missing column
  366. _, err = db.Exec(`ALTER TABLE build_jobs ADD COLUMN dockerfile_content TEXT`)
  367. if err != nil {
  368. // Ignore "duplicate column name" errors in case the column already exists
  369. if err.Error() != "duplicate column name: dockerfile_content" {
  370. return fmt.Errorf("failed to add dockerfile_content column: %w", err)
  371. }
  372. }
  373. return nil
  374. }
  375. // fileExists checks if a file exists
  376. func fileExists(filename string) bool {
  377. _, err := os.Stat(filename)
  378. return !os.IsNotExist(err)
  379. }
  380. // GetDB returns the GORM database instance
  381. func (m *SQLiteStore) GetDB() *sql.DB {
  382. return m.db
  383. }
  384. // Connect establishes a connection to the SQLite database
  385. func (m *SQLiteStore) Connect() error {
  386. // Connection is already established in NewSQLiteStore
  387. return nil
  388. }
  389. // Disconnect closes the connection to the SQLite database
  390. func (m *SQLiteStore) Disconnect() error {
  391. return m.db.Close()
  392. }
  393. // Close provides a more standard name for closing the database connection.
  394. // It simply calls Disconnect.
  395. func (m *SQLiteStore) Close() error {
  396. return m.Disconnect()
  397. }