123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- package services
- import (
- "context"
- "encoding/json"
- "fmt"
- "os" // Added import
- "strings"
- "time"
- "git.linuxforward.com/byop/byop-engine/clients"
- "git.linuxforward.com/byop/byop-engine/dbstore"
- "git.linuxforward.com/byop/byop-engine/models"
- "github.com/sirupsen/logrus"
- )
- const (
- defaultBuildQueueSize = 100
- defaultImageTag = "latest"
- defaultDockerfilePath = "Dockerfile"
- defaultBuildContext = "."
- )
- // Builder handles the queuing and processing of application build jobs.
- type Builder struct {
- store *dbstore.SQLiteStore
- buildMachineClient clients.BuildMachineClient
- registryClient clients.RegistryClient
- maxConcurrentBuild int
- buildChan chan uint
- entry *logrus.Entry
- }
- // NewBuilderService creates a new Builder service.
- func NewBuilderService(store *dbstore.SQLiteStore, buildMachineClient clients.BuildMachineClient, registryClient clients.RegistryClient, maxConcurrentBuild int) *Builder {
- svc := &Builder{
- store: store,
- buildMachineClient: buildMachineClient,
- registryClient: registryClient,
- maxConcurrentBuild: maxConcurrentBuild,
- buildChan: make(chan uint, maxConcurrentBuild),
- entry: logrus.WithField("service", "Builder"),
- }
- go svc.startBuildQueueProcessor() // Start a goroutine to process the queue
- svc.entry.Info("Builder service initialized and build queue processor started.")
- return svc
- }
- // QueueBuildJob adds a new build job to the queue.
- // It will first create a BuildJob entry in the database.
- func (s *Builder) QueueBuildJob(ctx context.Context, req models.BuildRequest) (*models.BuildJob, error) {
- s.entry.Infof("Received build request for ComponentID: %d, SourceURL: %s", req.ComponentID, req.SourceURL)
- // Debug: Check if DockerfileContent is present
- if req.DockerfileContent != "" {
- lines := strings.Split(req.DockerfileContent, "\n")
- if len(lines) > 5 {
- lines = lines[:5]
- }
- s.entry.Infof("DockerfileContent received for ComponentID %d (first 5 lines):\n%s", req.ComponentID, strings.Join(lines, "\n"))
- } else {
- s.entry.Warnf("DockerfileContent is EMPTY for ComponentID %d", req.ComponentID)
- }
- // 1. Validate request
- if req.ComponentID == 0 || req.SourceURL == "" || req.ImageName == "" {
- 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)
- s.entry.Error(err)
- return nil, err
- }
- // 2. Create BuildJob model
- imageTag := req.Version
- if imageTag == "" {
- imageTag = defaultImageTag
- }
- fullImageURI := fmt.Sprintf("%s:%s", req.ImageName, imageTag)
- if req.RegistryURL != "" {
- fullImageURI = fmt.Sprintf("%s/%s:%s", req.RegistryURL, req.ImageName, imageTag)
- }
- buildArgsJSON := ""
- if len(req.BuildArgs) > 0 {
- jsonBytes, err := json.Marshal(req.BuildArgs)
- if err != nil {
- s.entry.Errorf("Failed to marshal build args for ComponentID %d: %v", req.ComponentID, err)
- return nil, fmt.Errorf("failed to marshal build args: %w", err)
- }
- buildArgsJSON = string(jsonBytes)
- }
- dockerfilePath := req.Dockerfile
- if dockerfilePath == "" {
- dockerfilePath = defaultDockerfilePath
- }
- buildContext := req.BuildContext
- if buildContext == "" {
- buildContext = defaultBuildContext
- }
- job := models.BuildJob{
- ComponentID: req.ComponentID,
- RequestID: fmt.Sprintf("build-%d-%s", req.ComponentID, time.Now().Format("20060102150405")), // Unique ID for idempotency
- SourceURL: req.SourceURL,
- Status: models.BuildStatusPending, // Corrected: Queued is the initial status set by dbstore.CreateBuildJob
- ImageName: req.ImageName,
- ImageTag: imageTag,
- FullImageURI: fullImageURI,
- RegistryURL: req.RegistryURL,
- RegistryUser: req.RegistryUser, // Added
- RegistryPassword: req.RegistryPassword, // Added
- BuildContext: buildContext,
- Dockerfile: dockerfilePath,
- DockerfileContent: req.DockerfileContent, // NEW: Generated Dockerfile content
- NoCache: req.NoCache,
- BuildArgs: buildArgsJSON,
- RequestedAt: time.Now(),
- }
- // 3. Save BuildJob to database
- if err := s.store.CreateBuildJob(ctx, &job); err != nil {
- s.entry.Errorf("Failed to save build job for ComponentID %d to database: %v", req.ComponentID, err)
- return nil, fmt.Errorf("failed to save build job: %w", err)
- }
- // Debug: Verify the job was saved with DockerfileContent
- if job.DockerfileContent != "" {
- s.entry.Infof("Build job ID %d saved with DockerfileContent (length: %d chars)", job.ID, len(job.DockerfileContent))
- } else {
- s.entry.Warnf("Build job ID %d saved with EMPTY DockerfileContent", job.ID)
- }
- // 4. Send to buildQueue
- select {
- case s.buildChan <- job.ID: // Non-blocking send to channel
- s.entry.Infof("Build job ID %d for ComponentID %d sent to internal queue.", job.ID, job.ComponentID)
- default:
- s.entry.Errorf("Build queue is full. Failed to queue job ID %d for ComponentID %d.", job.ID, job.ComponentID)
- return &job, fmt.Errorf("build queue is full, cannot process job ID %d at this time", job.ID)
- }
- return &job, nil
- }
- // startBuildQueueProcessor runs in a goroutine, picking jobs from buildQueue.
- func (s *Builder) startBuildQueueProcessor() {
- s.entry.Info("Build queue processor started. Waiting for jobs...")
- for jobId := range s.buildChan {
- s.entry.Info("Processing build job from queue.")
- go s.processJob(context.Background(), jobId)
- }
- s.entry.Info("Build queue processor stopped.")
- }
- // processJob handles the lifecycle of a single build job.
- func (s *Builder) processJob(ctx context.Context, jobID uint) {
- // Implementation for processing a job
- job, err := s.store.GetBuildJobByID(ctx, jobID)
- if err != nil {
- s.entry.Errorf("Failed to retrieve build job ID %d from database: %v", jobID, err)
- return
- }
- // Ensure BuildContext is cleaned up after processing, if it's not the default "."
- if job.BuildContext != "" && job.BuildContext != defaultBuildContext {
- defer func() {
- s.entry.Infof("Attempting to clean up build context directory: %s for job ID %d", job.BuildContext, job.ID)
- if err := os.RemoveAll(job.BuildContext); err != nil {
- s.entry.Errorf("Failed to clean up build context directory %s for job ID %d: %v", job.BuildContext, job.ID, err)
- } else {
- s.entry.Infof("Successfully cleaned up build context directory: %s for job ID %d", job.BuildContext, job.ID)
- }
- }()
- }
- s.entry.Infof("Processing build job ID %d for ComponentID %d. BuildContext: %s", job.ID, job.ComponentID, job.BuildContext)
- // Debug: Check if DockerfileContent was retrieved from database
- if job.DockerfileContent != "" {
- s.entry.Infof("Job %d retrieved with DockerfileContent (length: %d chars)", job.ID, len(job.DockerfileContent))
- lines := strings.Split(job.DockerfileContent, "\n")
- if len(lines) > 3 {
- lines = lines[:3]
- }
- s.entry.Infof("Job %d DockerfileContent first 3 lines:\n%s", job.ID, strings.Join(lines, "\n"))
- } else {
- s.entry.Warnf("Job %d retrieved with EMPTY DockerfileContent", job.ID)
- }
- // Update job status to InProgress
- s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusBuilding, "")
- // Componentend log for job start
- s.appendJobLog(ctx, job.ID, fmt.Sprintf("Starting build for ComponentID %d from source %s", job.ComponentID, job.SourceURL))
- // Parse build arguments
- buildArgs, err := s.parseBuildArgs(job.BuildArgs)
- if err != nil {
- s.entry.Errorf("Failed to parse build args for job ID %d: %v", job.ID, err)
- s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Failed to parse build args: %v", err))
- s.appendJobLog(ctx, job.ID, fmt.Sprintf("Build failed: %v", err))
- return
- }
- buildOutput, err := s.buildMachineClient.BuildImage(ctx, *job, job.Dockerfile, job.BuildContext, job.ImageName, job.ImageTag, job.NoCache, buildArgs)
- if err != nil {
- s.entry.Errorf("Build failed for job ID %d: %v", job.ID, err)
- s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Build failed: %v", err))
- s.appendJobLog(ctx, job.ID, fmt.Sprintf("Build failed: %v", err))
- return
- }
- s.entry.Infof("Build completed successfully for job ID %d. Output: %s", job.ID, buildOutput)
- // Debug registry push configuration
- s.entry.Infof("Registry URL configured: %s", job.RegistryURL)
- // Push the image to the registry if configured
- if job.RegistryURL != "" {
- s.entry.Infof("Pushing image %s to registry %s", job.FullImageURI, job.RegistryURL)
- if err := s.registryClient.PushImage(ctx, *job, job.FullImageURI, job.RegistryURL, job.RegistryUser, job.RegistryPassword); err != nil {
- s.entry.Errorf("Failed to push image %s to registry %s: %v", job.FullImageURI, job.RegistryURL, err)
- s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Failed to push image: %v", err))
- s.appendJobLog(ctx, job.ID, fmt.Sprintf("Failed to push image: %v", err))
- return
- }
- s.entry.Infof("Image %s successfully pushed to registry %s", job.FullImageURI, job.RegistryURL)
- }
- // Finalize job with success status
- s.finalizeJob(ctx, job.ID, job.ComponentID, models.BuildStatusSuccess, "")
- s.appendJobLog(ctx, job.ID, "Build job completed successfully and image pushed to registry.")
- s.entry.Infof("Build job ID %d for ComponentID %d completed successfully.", job.ID, job.ComponentID)
- }
- // updateJobStatus updates the job's status in the database.
- func (s *Builder) updateJobStatus(ctx context.Context, jobID uint, componentId uint, status models.BuildStatus, errorMessage string) {
- if err := s.store.UpdateBuildJobStatus(ctx, jobID, status, errorMessage); err != nil {
- s.entry.Errorf("Error updating status for build job ID %d to %s: %v", jobID, status, err)
- } else {
- s.entry.Infof("Updated status for build job ID %d to %s.", jobID, status)
- }
- var componentStatus string
- switch status {
- case models.BuildStatusSuccess:
- componentStatus = "ready"
- case models.BuildStatusFailed:
- componentStatus = "failed"
- default:
- componentStatus = "in_progress"
- }
- if updateErr := s.store.UpdateComponentStatus(ctx, int(componentId), componentStatus, errorMessage); updateErr != nil {
- s.entry.Errorf("Error updating component status for job ID %d: %v", jobID, updateErr)
- } else {
- s.entry.Infof("Updated component status for job ID %d to %s.", jobID, status)
- }
- }
- // appendJobLog appends a log message to the job's logs in the database.
- func (s *Builder) appendJobLog(ctx context.Context, jobID uint, message string) {
- if err := s.store.AppendBuildJobLog(ctx, jobID, message); err != nil {
- s.entry.Errorf("Error appending log for build job ID %d: %v", jobID, err)
- s.entry.Infof("[Job %d Log]: %s", jobID, message)
- }
- }
- // finalizeJob sets the final status of the job (Success or Failed) and records FinishedAt.
- func (s *Builder) finalizeJob(ctx context.Context, jobID uint, componentId uint, status models.BuildStatus, errorMessage string) {
- if err := s.store.UpdateBuildJobStatus(ctx, jobID, status, errorMessage); err != nil {
- s.entry.Errorf("Error finalizing build job ID %d with status %s: %v", jobID, status, err)
- } else {
- s.entry.Infof("Finalized build job ID %d with status %s.", jobID, status)
- }
- var componentStatus string
- switch status {
- case models.BuildStatusSuccess:
- componentStatus = "ready"
- // Update component with image information if build was successful
- job, err := s.store.GetBuildJobByID(ctx, jobID)
- if err != nil {
- s.entry.Errorf("Error retrieving build job ID %d to update component image info: %v", jobID, err)
- } else {
- // Update component with the built image information
- if err := s.store.UpdateComponentImageInfo(ctx, int(componentId), job.ImageTag, job.FullImageURI); err != nil {
- s.entry.Errorf("Error updating component image info for component ID %d after successful build: %v", componentId, err)
- } else {
- s.entry.Infof("Successfully updated component ID %d with image tag %s and URI %s", componentId, job.ImageTag, job.FullImageURI)
- }
- }
- case models.BuildStatusFailed:
- componentStatus = "failed"
- default:
- componentStatus = "in_progress"
- }
- if updateErr := s.store.UpdateComponentStatus(ctx, int(componentId), componentStatus, errorMessage); updateErr != nil {
- s.entry.Errorf("Error updating component status for job ID %d: %v", jobID, updateErr)
- } else {
- s.entry.Infof("Updated component status for job ID %d to %s.", jobID, status)
- }
- }
- // parseBuildArgs converts a JSON string of build arguments into a map.
- func (s *Builder) parseBuildArgs(argsStr string) (map[string]string, error) {
- if argsStr == "" {
- return nil, nil
- }
- var argsMap map[string]string
- err := json.Unmarshal([]byte(argsStr), &argsMap)
- if err != nil {
- return nil, fmt.Errorf("error unmarshalling build args JSON: %w. JSON string was: %s", err, argsStr)
- }
- return argsMap, nil
- }
|