components.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. package handlers
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "os"
  7. "path/filepath"
  8. "strconv"
  9. "strings"
  10. "git.linuxforward.com/byop/byop-engine/analyzer"
  11. "git.linuxforward.com/byop/byop-engine/dbstore"
  12. "git.linuxforward.com/byop/byop-engine/models"
  13. "git.linuxforward.com/byop/byop-engine/services"
  14. "github.com/gin-gonic/gin"
  15. git "github.com/go-git/go-git/v5"
  16. "github.com/go-git/go-git/v5/plumbing"
  17. "github.com/sirupsen/logrus"
  18. )
  19. // ComponentHandler handles component-related operations and contains integrated service logic
  20. type ComponentHandler struct {
  21. store *dbstore.SQLiteStore
  22. entry *logrus.Entry
  23. buildSvc *services.Builder // Service for building applications
  24. registryUrl string // Default registry URL, can be configured
  25. }
  26. // NewComponentHandler creates a new ComponentHandler
  27. func NewComponentHandler(store *dbstore.SQLiteStore, builderSvc *services.Builder, registryUrl string) *ComponentHandler {
  28. return &ComponentHandler{
  29. store: store,
  30. entry: logrus.WithField("component", "ComponentHandler"),
  31. buildSvc: builderSvc, // Initialize the builder service with default values
  32. registryUrl: registryUrl, // Set the default registry URL
  33. }
  34. }
  35. // ListComponents returns all components with optional filtering
  36. func (h *ComponentHandler) ListComponents(c *gin.Context) {
  37. filter := make(map[string]interface{})
  38. ctx := c.Request.Context()
  39. // Attempt to bind query parameters, but allow empty filters
  40. if err := c.ShouldBindQuery(&filter); err != nil && len(filter) > 0 {
  41. appErr := models.NewErrValidation("invalid_query_params", nil, err)
  42. models.RespondWithError(c, appErr)
  43. return
  44. }
  45. components, err := h.store.GetAllComponents(ctx)
  46. if err != nil {
  47. appErr := models.NewErrInternalServer("failed_list_components", fmt.Errorf("Failed to list components: %w", err))
  48. models.RespondWithError(c, appErr)
  49. return
  50. }
  51. // If empty, return an empty list
  52. if len(components) == 0 {
  53. c.JSON(http.StatusOK, []models.Component{})
  54. return
  55. }
  56. c.JSON(http.StatusOK, components)
  57. }
  58. // CreateComponent creates a new component
  59. func (h *ComponentHandler) CreateComponent(c *gin.Context) {
  60. var component models.Component
  61. ctx := c.Request.Context()
  62. if err := c.ShouldBindJSON(&component); err != nil {
  63. appErr := models.NewErrValidation("invalid_request_body", nil, err)
  64. models.RespondWithError(c, appErr)
  65. return
  66. }
  67. // Get the user ID from the context (set by auth middleware)
  68. userIDInterface, exists := c.Get("user_id")
  69. if !exists {
  70. appErr := models.NewErrUnauthorized("user_id_not_found", fmt.Errorf("User ID not found in context"))
  71. models.RespondWithError(c, appErr)
  72. return
  73. }
  74. // Convert user_id to int - it might be a string from JWT
  75. var userID int
  76. switch v := userIDInterface.(type) {
  77. case string:
  78. if parsedID, err := strconv.Atoi(v); err == nil {
  79. userID = parsedID
  80. } else {
  81. h.entry.Warnf("Failed to parse user_id string '%s' to int, defaulting. Error: %v", v, err)
  82. userID = 1
  83. }
  84. case int:
  85. userID = v
  86. case int64:
  87. userID = int(v)
  88. default:
  89. h.entry.Warnf("User_id in context is of unexpected type %T, defaulting.", v)
  90. userID = 1
  91. }
  92. // Set the user ID on the component
  93. component.UserID = userID
  94. // Validate component data
  95. if validationErrors := h.validateComponentRequest(&component); len(validationErrors) > 0 {
  96. appErr := models.NewErrValidation("invalid_component_data", validationErrors, nil)
  97. models.RespondWithError(c, appErr)
  98. return
  99. }
  100. // Set initial status to validating
  101. component.Status = "validating"
  102. // Create the component
  103. id, err := h.store.CreateComponent(ctx, &component)
  104. if err != nil {
  105. appErr := models.NewErrInternalServer("failed_create_component", fmt.Errorf("Failed to create component: %w", err))
  106. models.RespondWithError(c, appErr)
  107. return
  108. }
  109. // Set the generated ID
  110. component.ID = id
  111. // Start async validation
  112. h.entry.WithField("component_id", component.ID).Info("Starting async validation for component")
  113. go h.validateComponent(context.Background(), &component)
  114. c.JSON(http.StatusCreated, component)
  115. }
  116. // validateComponent asynchronously validates the component's repository and Dockerfile
  117. // validateComponent checks if repository URL is valid, branch exists, and Dockerfile is present
  118. // if Dockerfile is not present, it starts an async generation process
  119. func (h *ComponentHandler) validateComponent(ctx context.Context, component *models.Component) {
  120. h.entry.WithField("component_id", component.ID).Info("Starting validation for component")
  121. // Create a temporary directory for cloning and Dockerfile generation
  122. tempDir, err := os.MkdirTemp("", fmt.Sprintf("byop-validate-%d-*", component.ID))
  123. if err != nil {
  124. h.entry.WithField("component_id", component.ID).Errorf("Failed to create temp directory: %v", err)
  125. // Update component status to invalid - use background context to avoid cancellation
  126. if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Failed to create temp dir: %v", err)); updateErr != nil {
  127. h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr)
  128. }
  129. return
  130. }
  131. // Change permissions of tempDir to allow access by other users (e.g., buildkitd)
  132. if err := os.Chmod(tempDir, 0755); err != nil {
  133. h.entry.Errorf("Failed to chmod tempDir %s for component %d: %v", tempDir, component.ID, err)
  134. if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Failed to set permissions on temp dir: %v", err)); updateErr != nil {
  135. h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr)
  136. }
  137. // Attempt to clean up tempDir if chmod fails and we are returning early,
  138. // as no build job will be queued for it.
  139. if errRemove := os.RemoveAll(tempDir); errRemove != nil {
  140. h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after chmod failure: %v", tempDir, errRemove)
  141. }
  142. return
  143. }
  144. h.entry.Debugf("Set permissions to 0755 for tempDir: %s", tempDir)
  145. // Log the start of validation
  146. h.entry.WithField("component_id", component.ID).Info("Validating component repository and Dockerfile")
  147. if err := h.validateRepoAndBranch(ctx, *component, tempDir); err != nil {
  148. h.entry.WithField("component_id", component.ID).Errorf("Validation failed: %v", err)
  149. // Update component status to invalid - use background context to avoid cancellation
  150. if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", err.Error()); updateErr != nil {
  151. h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr)
  152. }
  153. // Attempt to clean up tempDir if validation fails and we are returning early.
  154. if errRemove := os.RemoveAll(tempDir); errRemove != nil {
  155. h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after validation failure: %v", tempDir, errRemove)
  156. }
  157. return
  158. }
  159. // If Dockerfile does not exist, start generation
  160. h.entry.WithField("component_id", component.ID).Info("Dockerfile not found, starting generation")
  161. if err := h.store.UpdateComponentStatus(context.Background(), component.ID, "generating", "Dockerfile not found, generating..."); err != nil {
  162. h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status to generating: %v", err)
  163. return
  164. }
  165. // Guess the type of Dockerfile to generate based on component type
  166. stack, err := analyzer.AnalyzeCode(tempDir)
  167. if err != nil {
  168. h.entry.WithField("component_id", component.ID).Errorf("Failed to analyze code for Dockerfile generation: %v", err)
  169. // Update component status to invalid - use background context to avoid cancellation
  170. if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Code analysis failed: %v", err)); updateErr != nil {
  171. h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr)
  172. }
  173. // Attempt to clean up tempDir if analysis fails.
  174. if errRemove := os.RemoveAll(tempDir); errRemove != nil {
  175. h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after code analysis failure: %v", tempDir, errRemove)
  176. }
  177. return
  178. }
  179. dockerfileContent, err := stack.GenerateDockerfile(tempDir)
  180. if err != nil {
  181. h.entry.WithField("component_id", component.ID).Errorf("Failed to generate Dockerfile: %v", err)
  182. // Update component status to invalid - use background context to avoid cancellation
  183. if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Dockerfile generation failed: %v", err)); updateErr != nil {
  184. h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr)
  185. }
  186. // Attempt to clean up tempDir if Dockerfile generation fails.
  187. if errRemove := os.RemoveAll(tempDir); errRemove != nil {
  188. h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after Dockerfile generation failure: %v", tempDir, errRemove)
  189. }
  190. return
  191. }
  192. // Write the generated Dockerfile to the temp directory
  193. dockerfilePath := filepath.Join(tempDir, "Dockerfile")
  194. if err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644); err != nil {
  195. h.entry.WithField("component_id", component.ID).Errorf("Failed to write generated Dockerfile: %v", err)
  196. // Update component status to invalid - use background context to avoid cancellation
  197. if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Failed to write Dockerfile: %v", err)); updateErr != nil {
  198. h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr)
  199. }
  200. // Attempt to clean up tempDir if writing fails.
  201. if errRemove := os.RemoveAll(tempDir); errRemove != nil {
  202. h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after Dockerfile write failure: %v", tempDir, errRemove)
  203. }
  204. return
  205. }
  206. h.entry.WithField("component_id", component.ID).Info("Dockerfile generated, queueing build job.")
  207. // Debug: Log the first few lines of generated Dockerfile content
  208. lines := strings.Split(dockerfileContent, "\n")
  209. if len(lines) > 5 {
  210. lines = lines[:5]
  211. }
  212. h.entry.WithField("component_id", component.ID).Infof("Generated Dockerfile first 5 lines:\n%s", strings.Join(lines, "\n"))
  213. // Queue the build job with the generated Dockerfile
  214. h.buildSvc.QueueBuildJob(context.Background(), models.BuildRequest{
  215. ComponentID: uint(component.ID),
  216. SourceURL: component.Repository,
  217. ImageName: fmt.Sprintf("byop-component-%d", component.ID),
  218. Version: "latest", // Default version, can be changed later
  219. BuildContext: tempDir, // Use the temp directory as the build context
  220. DockerfileContent: dockerfileContent, // Pass the generated Dockerfile content
  221. Dockerfile: "Dockerfile", // Standard Dockerfile name
  222. RegistryURL: h.registryUrl, // Use the configured registry URL
  223. })
  224. // Do not remove tempDir here; buildSvc is responsible for it.
  225. }
  226. // validateRepository validates the Git repository
  227. func (h *ComponentHandler) validateRepoAndBranch(ctx context.Context, component models.Component, tempDir string) error {
  228. // Clone the repository
  229. h.entry.WithField("component_id", component.ID).Infof("Cloning repository %s on branch %s to %s", component.Repository, component.Branch, tempDir)
  230. if err := h.cloneRepository(component.Repository, component.Branch, tempDir); err != nil {
  231. return fmt.Errorf("failed to clone repository: %w", err)
  232. }
  233. h.entry.WithField("component_id", component.ID).Info("Repository and Dockerfile validation successful")
  234. return nil
  235. }
  236. // cloneRepository clones a Git repository to the specified directory
  237. func (h *ComponentHandler) cloneRepository(repoURL, branch, targetDir string) error {
  238. // Create target directory
  239. if err := os.MkdirAll(targetDir, 0755); err != nil {
  240. return fmt.Errorf("failed to create target directory: %w", err)
  241. }
  242. // Default branch if not specified
  243. if branch == "" {
  244. branch = "main"
  245. }
  246. // Try to clone with the specified branch
  247. _, err := git.PlainClone(targetDir, false, &git.CloneOptions{
  248. URL: repoURL,
  249. ReferenceName: plumbing.ReferenceName("refs/heads/" + branch),
  250. SingleBranch: true,
  251. Depth: 1,
  252. })
  253. if err == nil {
  254. h.entry.WithField("repo_url", repoURL).WithField("branch", branch).Infof("Successfully cloned repository to %s", targetDir)
  255. return nil
  256. }
  257. // If the specified branch fails and it's "main", try "master"
  258. if branch == "main" {
  259. h.entry.WithField("repo_url", repoURL).WithField("branch", branch).Warnf("Failed to clone with 'main' branch, trying 'master': %v", err)
  260. // Clean up the failed clone attempt
  261. os.RemoveAll(targetDir)
  262. if err := os.MkdirAll(targetDir, 0755); err != nil {
  263. return fmt.Errorf("failed to recreate target directory: %w", err)
  264. }
  265. _, err := git.PlainClone(targetDir, false, &git.CloneOptions{
  266. URL: repoURL,
  267. ReferenceName: plumbing.ReferenceName("refs/heads/master"),
  268. SingleBranch: true,
  269. Depth: 1,
  270. })
  271. if err == nil {
  272. h.entry.WithField("repo_url", repoURL).WithField("branch", "master").Infof("Successfully cloned repository to %s using 'master' branch", targetDir)
  273. return nil
  274. }
  275. h.entry.WithField("repo_url", repoURL).Errorf("Failed to clone with both 'main' and 'master' branches")
  276. return fmt.Errorf("failed to clone repository %s: tried both 'main' and 'master' branches, last error: %w", repoURL, err)
  277. }
  278. h.entry.WithField("repo_url", repoURL).WithField("branch", branch).Warnf("Failed to clone repository to %s: %v", targetDir, err)
  279. return fmt.Errorf("failed to clone repository %s (branch %s): %w", repoURL, branch, err)
  280. }
  281. // GetComponent returns a specific component
  282. func (h *ComponentHandler) GetComponent(c *gin.Context) {
  283. idStr := c.Param("id")
  284. ctx := c.Request.Context()
  285. id, err := strconv.ParseInt(idStr, 10, 64)
  286. if err != nil {
  287. appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err)
  288. models.RespondWithError(c, appErr)
  289. return
  290. }
  291. component, err := h.store.GetComponentByID(ctx, int(id))
  292. if err != nil {
  293. models.RespondWithError(c, err)
  294. return
  295. }
  296. if component.ID == 0 {
  297. appErr := models.NewErrNotFound("component_not_found_explicit_check", fmt.Errorf("Component with ID %d not found after store call", id))
  298. models.RespondWithError(c, appErr)
  299. return
  300. }
  301. c.JSON(http.StatusOK, component)
  302. }
  303. // UpdateComponent updates a component
  304. func (h *ComponentHandler) UpdateComponent(c *gin.Context) {
  305. idStr := c.Param("id")
  306. ctx := c.Request.Context()
  307. id, err := strconv.ParseInt(idStr, 10, 64)
  308. if err != nil {
  309. appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err)
  310. models.RespondWithError(c, appErr)
  311. return
  312. }
  313. var updatedComponent models.Component
  314. if err := c.ShouldBindJSON(&updatedComponent); err != nil {
  315. appErr := models.NewErrValidation("invalid_request_body", nil, err)
  316. models.RespondWithError(c, appErr)
  317. return
  318. }
  319. // Ensure the ID matches the URL parameter
  320. updatedComponent.ID = int(id)
  321. // Validate component data
  322. if validationErrors := h.validateComponentRequest(&updatedComponent); len(validationErrors) > 0 {
  323. appErr := models.NewErrValidation("invalid_component_data_update", validationErrors, nil)
  324. models.RespondWithError(c, appErr)
  325. return
  326. }
  327. if err := h.store.UpdateComponent(ctx, updatedComponent); err != nil {
  328. models.RespondWithError(c, err)
  329. return
  330. }
  331. c.JSON(http.StatusOK, updatedComponent)
  332. }
  333. // DeleteComponent deletes a component
  334. func (h *ComponentHandler) DeleteComponent(c *gin.Context) {
  335. idStr := c.Param("id")
  336. ctx := c.Request.Context()
  337. id, err := strconv.ParseInt(idStr, 10, 64)
  338. if err != nil {
  339. appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err)
  340. models.RespondWithError(c, appErr)
  341. return
  342. }
  343. if err := h.store.DeleteComponent(ctx, int(id)); err != nil {
  344. models.RespondWithError(c, err)
  345. return
  346. }
  347. c.JSON(http.StatusOK, gin.H{"message": "Component deleted successfully"})
  348. }
  349. // GetComponentDeployments returns all deployments for a component
  350. func (h *ComponentHandler) GetComponentDeployments(c *gin.Context) {
  351. idStr := c.Param("id")
  352. ctx := c.Request.Context()
  353. id, err := strconv.ParseInt(idStr, 10, 64)
  354. if err != nil {
  355. appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err)
  356. models.RespondWithError(c, appErr)
  357. return
  358. }
  359. // Check if component exists
  360. component, err := h.store.GetComponentByID(ctx, int(id))
  361. if err != nil {
  362. models.RespondWithError(c, err)
  363. return
  364. }
  365. if component.ID == 0 {
  366. appErr := models.NewErrNotFound("component_not_found_for_deployments", fmt.Errorf("Component with ID %d not found when listing deployments", id))
  367. models.RespondWithError(c, appErr)
  368. return
  369. }
  370. // TODO: Retrieve deployments - this likely requires a separate repository or service
  371. deployments := []models.Deployment{}
  372. c.JSON(http.StatusOK, deployments)
  373. }
  374. // validateComponentRequest checks if the component data is valid and returns a map of validation errors
  375. func (h *ComponentHandler) validateComponentRequest(component *models.Component) map[string]string {
  376. errors := make(map[string]string)
  377. if component.Name == "" {
  378. errors["name"] = "Component name is required"
  379. }
  380. if component.Type == "" {
  381. errors["type"] = "Component type is required"
  382. }
  383. if component.Repository == "" {
  384. errors["repository"] = "Component Git URL is required"
  385. }
  386. return errors
  387. }