builder.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. package services
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os" // Added import
  7. "strings"
  8. "time"
  9. "git.linuxforward.com/byop/byop-engine/clients"
  10. "git.linuxforward.com/byop/byop-engine/dbstore"
  11. "git.linuxforward.com/byop/byop-engine/models"
  12. "github.com/sirupsen/logrus"
  13. )
  14. const (
  15. defaultBuildQueueSize = 100
  16. defaultImageTag = "latest"
  17. defaultDockerfilePath = "Dockerfile"
  18. defaultBuildContext = "."
  19. )
  20. // Builder handles the queuing and processing of application build jobs.
  21. type Builder struct {
  22. store *dbstore.SQLiteStore
  23. buildMachineClient clients.BuildMachineClient
  24. registryClient clients.RegistryClient
  25. maxConcurrentBuild int
  26. buildChan chan uint
  27. entry *logrus.Entry
  28. }
  29. // NewBuilderService creates a new Builder service.
  30. func NewBuilderService(store *dbstore.SQLiteStore, buildMachineClient clients.BuildMachineClient, registryClient clients.RegistryClient, maxConcurrentBuild int) *Builder {
  31. svc := &Builder{
  32. store: store,
  33. buildMachineClient: buildMachineClient,
  34. registryClient: registryClient,
  35. maxConcurrentBuild: maxConcurrentBuild,
  36. buildChan: make(chan uint, maxConcurrentBuild),
  37. entry: logrus.WithField("service", "Builder"),
  38. }
  39. go svc.startBuildQueueProcessor() // Start a goroutine to process the queue
  40. svc.entry.Info("Builder service initialized and build queue processor started.")
  41. return svc
  42. }
  43. // QueueBuildJob adds a new build job to the queue.
  44. // It will first create a BuildJob entry in the database.
  45. func (s *Builder) QueueBuildJob(ctx context.Context, req models.BuildRequest) (*models.BuildJob, error) {
  46. s.entry.Infof("Received build request for ComponentID: %d, SourceURL: %s", req.ComponentID, req.SourceURL)
  47. // Debug: Check if DockerfileContent is present
  48. if req.DockerfileContent != "" {
  49. lines := strings.Split(req.DockerfileContent, "\n")
  50. if len(lines) > 5 {
  51. lines = lines[:5]
  52. }
  53. s.entry.Infof("DockerfileContent received for ComponentID %d (first 5 lines):\n%s", req.ComponentID, strings.Join(lines, "\n"))
  54. } else {
  55. s.entry.Warnf("DockerfileContent is EMPTY for ComponentID %d", req.ComponentID)
  56. }
  57. // 1. Validate request
  58. if req.ComponentID == 0 || req.SourceURL == "" || req.ImageName == "" {
  59. err := fmt.Errorf("invalid build request: ComponentID, SourceURL, and ImageName are required. Got ComponentID: %d, SourceURL: '%s', ImageName: '%s'", req.ComponentID, req.SourceURL, req.ImageName)
  60. s.entry.Error(err)
  61. return nil, err
  62. }
  63. // 2. Create BuildJob model
  64. imageTag := req.Version
  65. if imageTag == "" {
  66. imageTag = defaultImageTag
  67. }
  68. fullImageURI := fmt.Sprintf("%s:%s", req.ImageName, imageTag)
  69. if req.RegistryURL != "" {
  70. fullImageURI = fmt.Sprintf("%s/%s:%s", req.RegistryURL, req.ImageName, imageTag)
  71. }
  72. buildArgsJSON := ""
  73. if len(req.BuildArgs) > 0 {
  74. jsonBytes, err := json.Marshal(req.BuildArgs)
  75. if err != nil {
  76. s.entry.Errorf("Failed to marshal build args for ComponentID %d: %v", req.ComponentID, err)
  77. return nil, fmt.Errorf("failed to marshal build args: %w", err)
  78. }
  79. buildArgsJSON = string(jsonBytes)
  80. }
  81. dockerfilePath := req.Dockerfile
  82. if dockerfilePath == "" {
  83. dockerfilePath = defaultDockerfilePath
  84. }
  85. buildContext := req.BuildContext
  86. if buildContext == "" {
  87. buildContext = defaultBuildContext
  88. }
  89. job := models.BuildJob{
  90. ComponentID: req.ComponentID,
  91. RequestID: fmt.Sprintf("build-%d-%s", req.ComponentID, time.Now().Format("20060102150405")), // Unique ID for idempotency
  92. SourceURL: req.SourceURL,
  93. Status: models.BuildStatusPending, // Corrected: Queued is the initial status set by dbstore.CreateBuildJob
  94. ImageName: req.ImageName,
  95. ImageTag: imageTag,
  96. FullImageURI: fullImageURI,
  97. RegistryURL: req.RegistryURL,
  98. RegistryUser: req.RegistryUser, // Added
  99. RegistryPassword: req.RegistryPassword, // Added
  100. BuildContext: buildContext,
  101. Dockerfile: dockerfilePath,
  102. DockerfileContent: req.DockerfileContent, // NEW: Generated Dockerfile content
  103. NoCache: req.NoCache,
  104. BuildArgs: buildArgsJSON,
  105. RequestedAt: time.Now(),
  106. }
  107. // 3. Save BuildJob to database
  108. if err := s.store.CreateBuildJob(ctx, &job); err != nil {
  109. s.entry.Errorf("Failed to save build job for ComponentID %d to database: %v", req.ComponentID, err)
  110. return nil, fmt.Errorf("failed to save build job: %w", err)
  111. }
  112. // Debug: Verify the job was saved with DockerfileContent
  113. if job.DockerfileContent != "" {
  114. s.entry.Infof("Build job ID %d saved with DockerfileContent (length: %d chars)", job.ID, len(job.DockerfileContent))
  115. } else {
  116. s.entry.Warnf("Build job ID %d saved with EMPTY DockerfileContent", job.ID)
  117. }
  118. // 4. Send to buildQueue
  119. select {
  120. case s.buildChan <- job.ID: // Non-blocking send to channel
  121. s.entry.Infof("Build job ID %d for ComponentID %d sent to internal queue.", job.ID, job.ComponentID)
  122. default:
  123. s.entry.Errorf("Build queue is full. Failed to queue job ID %d for ComponentID %d.", job.ID, job.ComponentID)
  124. return &job, fmt.Errorf("build queue is full, cannot process job ID %d at this time", job.ID)
  125. }
  126. return &job, nil
  127. }
  128. // startBuildQueueProcessor runs in a goroutine, picking jobs from buildQueue.
  129. func (s *Builder) startBuildQueueProcessor() {
  130. s.entry.Info("Build queue processor started. Waiting for jobs...")
  131. for jobId := range s.buildChan {
  132. s.entry.Info("Processing build job from queue.")
  133. go s.processJob(context.Background(), jobId)
  134. }
  135. s.entry.Info("Build queue processor stopped.")
  136. }
  137. // processJob handles the lifecycle of a single build job.
  138. func (s *Builder) processJob(ctx context.Context, jobID uint) {
  139. // Implementation for processing a job
  140. job, err := s.store.GetBuildJobByID(ctx, jobID)
  141. if err != nil {
  142. s.entry.Errorf("Failed to retrieve build job ID %d from database: %v", jobID, err)
  143. return
  144. }
  145. // Ensure BuildContext is cleaned up after processing, if it's not the default "."
  146. if job.BuildContext != "" && job.BuildContext != defaultBuildContext {
  147. defer func() {
  148. s.entry.Infof("Attempting to clean up build context directory: %s for job ID %d", job.BuildContext, job.ID)
  149. if err := os.RemoveAll(job.BuildContext); err != nil {
  150. s.entry.Errorf("Failed to clean up build context directory %s for job ID %d: %v", job.BuildContext, job.ID, err)
  151. } else {
  152. s.entry.Infof("Successfully cleaned up build context directory: %s for job ID %d", job.BuildContext, job.ID)
  153. }
  154. }()
  155. }
  156. s.entry.Infof("Processing build job ID %d for ComponentID %d. BuildContext: %s", job.ID, job.ComponentID, job.BuildContext)
  157. // Debug: Check if DockerfileContent was retrieved from database
  158. if job.DockerfileContent != "" {
  159. s.entry.Infof("Job %d retrieved with DockerfileContent (length: %d chars)", job.ID, len(job.DockerfileContent))
  160. lines := strings.Split(job.DockerfileContent, "\n")
  161. if len(lines) > 3 {
  162. lines = lines[:3]
  163. }
  164. s.entry.Infof("Job %d DockerfileContent first 3 lines:\n%s", job.ID, strings.Join(lines, "\n"))
  165. } else {
  166. s.entry.Warnf("Job %d retrieved with EMPTY DockerfileContent", job.ID)
  167. }
  168. // Update job status to InProgress
  169. s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusBuilding, "")
  170. // Componentend log for job start
  171. s.appendJobLog(ctx, job.ID, fmt.Sprintf("Starting build for ComponentID %d from source %s", job.ComponentID, job.SourceURL))
  172. // Parse build arguments
  173. buildArgs, err := s.parseBuildArgs(job.BuildArgs)
  174. if err != nil {
  175. s.entry.Errorf("Failed to parse build args for job ID %d: %v", job.ID, err)
  176. s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Failed to parse build args: %v", err))
  177. s.appendJobLog(ctx, job.ID, fmt.Sprintf("Build failed: %v", err))
  178. return
  179. }
  180. buildOutput, err := s.buildMachineClient.BuildImage(ctx, *job, job.Dockerfile, job.BuildContext, job.ImageName, job.ImageTag, job.NoCache, buildArgs)
  181. if err != nil {
  182. s.entry.Errorf("Build failed for job ID %d: %v", job.ID, err)
  183. s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Build failed: %v", err))
  184. s.appendJobLog(ctx, job.ID, fmt.Sprintf("Build failed: %v", err))
  185. return
  186. }
  187. s.entry.Infof("Build completed successfully for job ID %d. Output: %s", job.ID, buildOutput)
  188. // Debug registry push configuration
  189. s.entry.Infof("Registry URL configured: %s", job.RegistryURL)
  190. // Push the image to the registry if configured
  191. if job.RegistryURL != "" {
  192. s.entry.Infof("Pushing image %s to registry %s", job.FullImageURI, job.RegistryURL)
  193. if err := s.registryClient.PushImage(ctx, *job, job.FullImageURI, job.RegistryURL, job.RegistryUser, job.RegistryPassword); err != nil {
  194. s.entry.Errorf("Failed to push image %s to registry %s: %v", job.FullImageURI, job.RegistryURL, err)
  195. s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Failed to push image: %v", err))
  196. s.appendJobLog(ctx, job.ID, fmt.Sprintf("Failed to push image: %v", err))
  197. return
  198. }
  199. s.entry.Infof("Image %s successfully pushed to registry %s", job.FullImageURI, job.RegistryURL)
  200. }
  201. // Finalize job with success status
  202. s.finalizeJob(ctx, job.ID, job.ComponentID, models.BuildStatusSuccess, "")
  203. s.appendJobLog(ctx, job.ID, "Build job completed successfully and image pushed to registry.")
  204. s.entry.Infof("Build job ID %d for ComponentID %d completed successfully.", job.ID, job.ComponentID)
  205. }
  206. // updateJobStatus updates the job's status in the database.
  207. func (s *Builder) updateJobStatus(ctx context.Context, jobID uint, componentId uint, status models.BuildStatus, errorMessage string) {
  208. if err := s.store.UpdateBuildJobStatus(ctx, jobID, status, errorMessage); err != nil {
  209. s.entry.Errorf("Error updating status for build job ID %d to %s: %v", jobID, status, err)
  210. } else {
  211. s.entry.Infof("Updated status for build job ID %d to %s.", jobID, status)
  212. }
  213. var componentStatus string
  214. switch status {
  215. case models.BuildStatusSuccess:
  216. componentStatus = "ready"
  217. case models.BuildStatusFailed:
  218. componentStatus = "failed"
  219. default:
  220. componentStatus = "in_progress"
  221. }
  222. if updateErr := s.store.UpdateComponentStatus(ctx, int(componentId), componentStatus, errorMessage); updateErr != nil {
  223. s.entry.Errorf("Error updating component status for job ID %d: %v", jobID, updateErr)
  224. } else {
  225. s.entry.Infof("Updated component status for job ID %d to %s.", jobID, status)
  226. }
  227. }
  228. // appendJobLog appends a log message to the job's logs in the database.
  229. func (s *Builder) appendJobLog(ctx context.Context, jobID uint, message string) {
  230. if err := s.store.AppendBuildJobLog(ctx, jobID, message); err != nil {
  231. s.entry.Errorf("Error appending log for build job ID %d: %v", jobID, err)
  232. s.entry.Infof("[Job %d Log]: %s", jobID, message)
  233. }
  234. }
  235. // finalizeJob sets the final status of the job (Success or Failed) and records FinishedAt.
  236. func (s *Builder) finalizeJob(ctx context.Context, jobID uint, componentId uint, status models.BuildStatus, errorMessage string) {
  237. if err := s.store.UpdateBuildJobStatus(ctx, jobID, status, errorMessage); err != nil {
  238. s.entry.Errorf("Error finalizing build job ID %d with status %s: %v", jobID, status, err)
  239. } else {
  240. s.entry.Infof("Finalized build job ID %d with status %s.", jobID, status)
  241. }
  242. var componentStatus string
  243. switch status {
  244. case models.BuildStatusSuccess:
  245. componentStatus = "ready"
  246. // Update component with image information if build was successful
  247. job, err := s.store.GetBuildJobByID(ctx, jobID)
  248. if err != nil {
  249. s.entry.Errorf("Error retrieving build job ID %d to update component image info: %v", jobID, err)
  250. } else {
  251. // Update component with the built image information
  252. if err := s.store.UpdateComponentImageInfo(ctx, int(componentId), job.ImageTag, job.FullImageURI); err != nil {
  253. s.entry.Errorf("Error updating component image info for component ID %d after successful build: %v", componentId, err)
  254. } else {
  255. s.entry.Infof("Successfully updated component ID %d with image tag %s and URI %s", componentId, job.ImageTag, job.FullImageURI)
  256. }
  257. }
  258. case models.BuildStatusFailed:
  259. componentStatus = "failed"
  260. default:
  261. componentStatus = "in_progress"
  262. }
  263. if updateErr := s.store.UpdateComponentStatus(ctx, int(componentId), componentStatus, errorMessage); updateErr != nil {
  264. s.entry.Errorf("Error updating component status for job ID %d: %v", jobID, updateErr)
  265. } else {
  266. s.entry.Infof("Updated component status for job ID %d to %s.", jobID, status)
  267. }
  268. }
  269. // parseBuildArgs converts a JSON string of build arguments into a map.
  270. func (s *Builder) parseBuildArgs(argsStr string) (map[string]string, error) {
  271. if argsStr == "" {
  272. return nil, nil
  273. }
  274. var argsMap map[string]string
  275. err := json.Unmarshal([]byte(argsStr), &argsMap)
  276. if err != nil {
  277. return nil, fmt.Errorf("error unmarshalling build args JSON: %w. JSON string was: %s", err, argsStr)
  278. }
  279. return argsMap, nil
  280. }