apps.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. package handlers
  2. import (
  3. "context" // Ensure context is imported
  4. "errors" // Added for errors.As
  5. "fmt"
  6. "net/http"
  7. "strconv"
  8. "git.linuxforward.com/byop/byop-engine/dbstore"
  9. "git.linuxforward.com/byop/byop-engine/models"
  10. "git.linuxforward.com/byop/byop-engine/services"
  11. "github.com/gin-gonic/gin"
  12. "github.com/sirupsen/logrus"
  13. )
  14. // AppsHandler handles app-related operations and contains integrated service logic
  15. type AppsHandler struct {
  16. store *dbstore.SQLiteStore
  17. entry *logrus.Entry
  18. previewService services.PreviewService
  19. }
  20. // NewAppsHandler creates a new AppsHandler
  21. func NewAppsHandler(store *dbstore.SQLiteStore, previewService services.PreviewService) *AppsHandler {
  22. return &AppsHandler{
  23. store: store,
  24. entry: logrus.WithField("component", "AppsHandler"),
  25. previewService: previewService,
  26. }
  27. }
  28. // ListApps returns all apps with optional filtering
  29. func (h *AppsHandler) ListApps(c *gin.Context) {
  30. ctx := c.Request.Context() // Get context
  31. filter := make(map[string]interface{})
  32. // Attempt to bind query parameters, but allow empty filters
  33. if err := c.ShouldBindQuery(&filter); err != nil && len(filter) > 0 {
  34. models.RespondWithError(c, models.NewErrValidation("Invalid query parameters", nil, err))
  35. return
  36. }
  37. // Get apps directly from store
  38. apps, err := h.store.GetAllApps(ctx) // Pass context
  39. if err != nil {
  40. models.RespondWithError(c, err) // Pass db error directly
  41. return
  42. }
  43. // If empty, return an empty list
  44. if len(apps) == 0 {
  45. c.JSON(http.StatusOK, []*models.App{}) // Return empty slice of pointers
  46. return
  47. }
  48. c.JSON(http.StatusOK, apps)
  49. }
  50. // CreateApp creates a new deployment app
  51. func (h *AppsHandler) CreateApp(c *gin.Context) {
  52. ctx := c.Request.Context() // Get context
  53. app := &models.App{}
  54. if err := c.ShouldBindJSON(&app); err != nil {
  55. h.entry.WithField("error", err).Error("Failed to bind JSON to App struct")
  56. models.RespondWithError(c, models.NewErrValidation("Invalid request body", nil, err))
  57. return
  58. }
  59. // Get the user ID from the context (set by auth middleware)
  60. userIDInterface, exists := c.Get("user_id")
  61. if !exists {
  62. models.RespondWithError(c, models.NewErrUnauthorized("User ID not found in context", nil))
  63. return
  64. }
  65. // Convert user_id to int - it might be a string from JWT
  66. var userID int
  67. switch v := userIDInterface.(type) {
  68. case string:
  69. if parsedID, err := strconv.Atoi(v); err == nil {
  70. userID = parsedID
  71. } else {
  72. h.entry.Warnf("Failed to parse user_id string '%s' to int, defaulting to 1. Error: %v", v, err)
  73. userID = 1
  74. }
  75. case int:
  76. userID = v
  77. case int64:
  78. userID = int(v)
  79. default:
  80. h.entry.Warnf("User_id in context is of unexpected type %T, defaulting to 1.", v)
  81. userID = 1
  82. }
  83. // Set the user ID on the app
  84. app.UserID = userID
  85. h.entry.WithField("app", app).Info("JSON binding successful, starting validation")
  86. // Validate app configuration
  87. if err := h.validateAppConfig(ctx, app.Components); err != nil { // Pass context
  88. h.entry.WithField("error", err).Error("App configuration validation failed")
  89. // Check if the error from validateAppConfig is already a CustomError
  90. var customErr models.CustomError
  91. if errors.As(err, &customErr) {
  92. models.RespondWithError(c, customErr)
  93. } else {
  94. models.RespondWithError(c, models.NewErrValidation(fmt.Sprintf("Invalid app configuration: %v", err), nil, err))
  95. }
  96. return
  97. }
  98. h.entry.Info("App configuration validation passed")
  99. // Set initial status
  100. app.Status = "building"
  101. h.entry.WithField("app", app).Info("About to create app in database")
  102. // Create the app
  103. id, err := h.store.CreateApp(ctx, app) // Pass context
  104. if err != nil {
  105. h.entry.WithField("error", err).Error("Failed to create app in database")
  106. models.RespondWithError(c, err) // Pass db error directly
  107. return
  108. }
  109. app.ID = id
  110. // Automatically create preview - this happens async
  111. h.entry.WithField("app_id", app.ID).Info("Starting automatic preview creation")
  112. go h.createAppPreviewAsync(context.Background(), id) // Use background context for async operation
  113. c.JSON(http.StatusCreated, app)
  114. }
  115. // GetApp returns a specific app
  116. func (h *AppsHandler) GetApp(c *gin.Context) {
  117. ctx := c.Request.Context() // Get context
  118. idStr := c.Param("id")
  119. id, err := strconv.ParseInt(idStr, 10, 64)
  120. if err != nil {
  121. models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
  122. return
  123. }
  124. // Get app directly from store
  125. app, err := h.store.GetAppByID(ctx, int(id)) // Pass context and cast id
  126. if err != nil {
  127. models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
  128. return
  129. }
  130. // GetAppByID now returns models.NewErrNotFound, so this check might be redundant if RespondWithError handles nil correctly
  131. // However, GetAppByID returns (*models.App, error), so if err is nil, app could still be nil (though current db logic prevents this for NotFound)
  132. if app == nil && err == nil { // Explicitly handle if GetAppByID somehow returns nil, nil without specific error
  133. models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
  134. return
  135. }
  136. c.JSON(http.StatusOK, app)
  137. }
  138. // Updated UpdateApp method
  139. func (h *AppsHandler) UpdateApp(c *gin.Context) {
  140. ctx := c.Request.Context() // Get context
  141. idStr := c.Param("id")
  142. id, err := strconv.ParseInt(idStr, 10, 64)
  143. if err != nil {
  144. models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
  145. return
  146. }
  147. updatedApp := &models.App{}
  148. if err := c.ShouldBindJSON(updatedApp); err != nil {
  149. models.RespondWithError(c, models.NewErrValidation("Invalid request body", nil, err))
  150. return
  151. }
  152. // Ensure the ID matches the URL parameter
  153. updatedApp.ID = int(id)
  154. // Validate app data
  155. if err := h.validateAppConfig(ctx, updatedApp.Components); err != nil { // Pass context
  156. var customErr models.CustomError
  157. if errors.As(err, &customErr) {
  158. models.RespondWithError(c, customErr)
  159. } else {
  160. models.RespondWithError(c, models.NewErrValidation(fmt.Sprintf("Invalid app data: %v", err), nil, err))
  161. }
  162. return
  163. }
  164. // Check if app exists (though UpdateApp in store will also do this and return ErrNotFound)
  165. // This is more for a clear early exit if needed, but store.UpdateApp handles it.
  166. // For consistency, we can rely on store.UpdateApp's error.
  167. // Validate all components exist and are valid
  168. for _, componentID := range updatedApp.Components {
  169. component, err := h.store.GetComponentByID(ctx, componentID) // Pass context
  170. if err != nil {
  171. models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
  172. return
  173. }
  174. // GetComponentByID now returns models.NewErrNotFound, so this check might be redundant
  175. if component == nil && err == nil { // Explicitly handle if GetComponentByID somehow returns nil, nil
  176. models.RespondWithError(c, models.NewErrValidation(fmt.Sprintf("Component %d not found during app update validation", componentID), nil, nil))
  177. return
  178. }
  179. if component.Status != "valid" {
  180. models.RespondWithError(c, models.NewErrValidation(fmt.Sprintf("Component %d is not valid (status: %s)", componentID, component.Status), nil, nil))
  181. return
  182. }
  183. }
  184. // Set status to building (will be updated by preview creation)
  185. updatedApp.Status = "building"
  186. // Update the app
  187. if err := h.store.UpdateApp(ctx, updatedApp); err != nil { // Pass context
  188. models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
  189. return
  190. }
  191. // Stop any existing previews for this app
  192. go h.stopExistingPreviews(context.Background(), updatedApp.ID) // Use background context
  193. // Automatically create new preview
  194. h.entry.WithField("app_id", updatedApp.ID).Info("Starting automatic preview creation after update")
  195. go h.createAppPreviewAsync(context.Background(), updatedApp.ID) // Use background context
  196. c.JSON(http.StatusOK, updatedApp)
  197. }
  198. // DeleteApp deletes an app
  199. func (h *AppsHandler) DeleteApp(c *gin.Context) {
  200. ctx := c.Request.Context() // Get context
  201. idStr := c.Param("id")
  202. id, err := strconv.ParseInt(idStr, 10, 64)
  203. if err != nil {
  204. models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
  205. return
  206. }
  207. // Call the store delete method
  208. if err := h.store.DeleteApp(ctx, int(id)); err != nil { // Pass context
  209. models.RespondWithError(c, err) // Pass db error directly (handles NotFound, Conflict)
  210. return
  211. }
  212. c.JSON(http.StatusOK, gin.H{"message": "App deleted successfully"})
  213. }
  214. // GetAppDeployments returns all deployments for an app
  215. func (h *AppsHandler) GetAppDeployments(c *gin.Context) {
  216. ctx := c.Request.Context() // Get context
  217. idStr := c.Param("id")
  218. id, err := strconv.ParseInt(idStr, 10, 64)
  219. if err != nil {
  220. models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
  221. return
  222. }
  223. // Check if app exists first
  224. app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
  225. if err != nil {
  226. models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
  227. return
  228. }
  229. if app == nil && err == nil { // Should be caught by GetAppByID's error handling
  230. models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
  231. return
  232. }
  233. // Get deployments for this app
  234. deployments, err := h.store.GetDeploymentsByAppID(ctx, int(id)) // Pass context
  235. if err != nil {
  236. models.RespondWithError(c, err) // Pass db error directly
  237. return
  238. }
  239. c.JSON(http.StatusOK, deployments)
  240. }
  241. // GetAppByVersion handles retrieval of an app by name and version
  242. func (h *AppsHandler) GetAppByVersion(c *gin.Context) {
  243. ctx := c.Request.Context() // Get context
  244. name := c.Query("name")
  245. version := c.Query("version")
  246. if name == "" || version == "" {
  247. models.RespondWithError(c, models.NewErrValidation("Both name and version query parameters are required", nil, nil))
  248. return
  249. }
  250. // Get app by name and version directly from store
  251. app, err := h.getAppByNameAndVersion(ctx, name, version) // Pass context
  252. if err != nil {
  253. var customErr models.CustomError
  254. if errors.As(err, &customErr) {
  255. models.RespondWithError(c, customErr)
  256. } else {
  257. models.RespondWithError(c, models.NewErrInternalServer(fmt.Sprintf("Failed to fetch app by name '%s' and version '%s'", name, version), err))
  258. }
  259. return
  260. }
  261. if app == nil {
  262. models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with name '%s' and version '%s' not found", name, version), nil))
  263. return
  264. }
  265. c.JSON(http.StatusOK, app)
  266. }
  267. // validateAppConfig checks if the app configuration is valid
  268. func (h *AppsHandler) validateAppConfig(ctx context.Context, componentIds []int) error { // Added context
  269. if len(componentIds) == 0 {
  270. return models.NewErrValidation("App must have at least one component", nil, nil) // Return custom error
  271. }
  272. for _, compId := range componentIds {
  273. if compId == 0 {
  274. return models.NewErrValidation("Component ID cannot be zero", nil, nil) // Return custom error
  275. }
  276. comp, err := h.store.GetComponentByID(ctx, compId) // Pass context
  277. if err != nil {
  278. return err
  279. }
  280. if comp == nil && err == nil {
  281. return models.NewErrValidation(fmt.Sprintf("Component with ID %d does not exist (validation check)", compId), nil, nil)
  282. }
  283. if comp.Name == "" {
  284. return models.NewErrValidation(fmt.Sprintf("Component with ID %d has an empty name", compId), nil, nil) // Return custom error
  285. }
  286. if comp.Type == "" {
  287. return models.NewErrValidation(fmt.Sprintf("Component with ID %d has an empty type", compId), nil, nil) // Return custom error
  288. }
  289. }
  290. return nil
  291. }
  292. // getAppByNameAndVersion retrieves an app by name and version from the store
  293. func (h *AppsHandler) getAppByNameAndVersion(ctx context.Context, name, version string) (*models.App, error) { // Added context
  294. apps, err := h.store.GetAllApps(ctx) // Pass context
  295. if err != nil {
  296. return nil, err
  297. }
  298. for _, app := range apps {
  299. if app.Name == name && app.Description == version { // Using description as version for now
  300. return app, nil
  301. }
  302. }
  303. return nil, models.NewErrNotFound(fmt.Sprintf("App with name '%s' and version '%s' not found (helper)", name, version), nil) // Return custom error
  304. }
  305. // createAppPreviewAsync creates a preview automatically and updates app status
  306. func (h *AppsHandler) createAppPreviewAsync(ctx context.Context, appId int) { // Added context
  307. preview, err := h.previewService.CreatePreview(ctx, appId) // Pass context
  308. if err != nil {
  309. h.entry.WithField("app_id", appId).Errorf("Failed to create preview: %v", err)
  310. if updateErr := h.store.UpdateAppStatus(ctx, appId, "failed", fmt.Sprintf("Failed to create preview: %v", err)); updateErr != nil {
  311. h.entry.WithField("app_id", appId).Errorf("Additionally failed to update app status after preview failure: %v", updateErr)
  312. }
  313. return
  314. }
  315. if err := h.store.UpdateAppPreview(ctx, appId, preview.ID, preview.URL); err != nil { // Pass context
  316. h.entry.WithField("app_id", appId).Errorf("Failed to update app with preview info: %v", err)
  317. }
  318. h.entry.WithField("app_id", appId).WithField("preview_id", preview.ID).Info("Automatic preview creation initiated")
  319. }
  320. // stopExistingPreviews stops any running previews for an app
  321. func (h *AppsHandler) stopExistingPreviews(ctx context.Context, appID int) { // Added context
  322. previews, err := h.store.GetPreviewsByAppID(ctx, appID) // Pass context
  323. if err != nil {
  324. h.entry.WithField("app_id", appID).Errorf("Failed to get existing previews: %v", err)
  325. return
  326. }
  327. for _, preview := range previews {
  328. if preview.Status == "running" {
  329. if err := h.previewService.StopPreview(ctx, preview.ID); err != nil { // Pass context
  330. h.entry.WithField("preview_id", preview.ID).Errorf("Failed to stop existing preview: %v", err)
  331. }
  332. }
  333. }
  334. }
  335. // CreateAppPreview creates a new preview for an app
  336. func (h *AppsHandler) CreateAppPreview(c *gin.Context) {
  337. ctx := c.Request.Context() // Get context
  338. idStr := c.Param("id")
  339. id, err := strconv.ParseInt(idStr, 10, 64)
  340. if err != nil {
  341. models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
  342. return
  343. }
  344. app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
  345. if err != nil {
  346. models.RespondWithError(c, err)
  347. return
  348. }
  349. if app == nil {
  350. models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
  351. return
  352. }
  353. h.entry.WithField("app_id", app.ID).Info("Creating manual preview for app")
  354. go h.createAppPreviewAsync(context.Background(), app.ID) // Use background context for async operation
  355. c.JSON(http.StatusOK, gin.H{"message": "Preview creation started"})
  356. }
  357. // GetAppPreview returns the preview for a specific app
  358. func (h *AppsHandler) GetAppPreview(c *gin.Context) {
  359. ctx := c.Request.Context() // Get context
  360. idStr := c.Param("id")
  361. id, err := strconv.ParseInt(idStr, 10, 64)
  362. if err != nil {
  363. models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
  364. return
  365. }
  366. app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
  367. if err != nil {
  368. models.RespondWithError(c, err)
  369. return
  370. }
  371. if app == nil {
  372. models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
  373. return
  374. }
  375. preview, err := h.store.GetPreviewByAppID(ctx, app.ID) // Pass context
  376. if err != nil {
  377. models.RespondWithError(c, err)
  378. return
  379. }
  380. if preview == nil {
  381. models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("Preview for app ID %d not found", id), nil))
  382. return
  383. }
  384. c.JSON(http.StatusOK, preview)
  385. }
  386. // DeleteAppPreview deletes the preview for a specific app
  387. func (h *AppsHandler) DeleteAppPreview(c *gin.Context) {
  388. ctx := c.Request.Context() // Get context
  389. idStr := c.Param("id")
  390. id, err := strconv.ParseInt(idStr, 10, 64)
  391. if err != nil {
  392. models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
  393. return
  394. }
  395. app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
  396. if err != nil {
  397. models.RespondWithError(c, err)
  398. return
  399. }
  400. if app == nil {
  401. models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
  402. return
  403. }
  404. if err := h.previewService.DeletePreview(ctx, app.ID); err != nil { // Pass context
  405. models.RespondWithError(c, err)
  406. return
  407. }
  408. c.JSON(http.StatusOK, gin.H{"message": "Preview deleted successfully"})
  409. }