components.go 17 KB

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