package handlers import ( "context" "fmt" "net/http" "os" "path/filepath" "strconv" "strings" "git.linuxforward.com/byop/byop-engine/analyzer" "git.linuxforward.com/byop/byop-engine/dbstore" "git.linuxforward.com/byop/byop-engine/models" "git.linuxforward.com/byop/byop-engine/services" "github.com/gin-gonic/gin" git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/sirupsen/logrus" ) // ComponentHandler handles component-related operations and contains integrated service logic type ComponentHandler struct { store *dbstore.SQLiteStore entry *logrus.Entry buildSvc *services.Builder // Service for building applications registryUrl string // Default registry URL, can be configured } // NewComponentHandler creates a new ComponentHandler func NewComponentHandler(store *dbstore.SQLiteStore, builderSvc *services.Builder, registryUrl string) *ComponentHandler { return &ComponentHandler{ store: store, entry: logrus.WithField("component", "ComponentHandler"), buildSvc: builderSvc, // Initialize the builder service with default values registryUrl: registryUrl, // Set the default registry URL } } // ListComponents returns all components with optional filtering func (h *ComponentHandler) ListComponents(c *gin.Context) { filter := make(map[string]interface{}) ctx := c.Request.Context() // Attempt to bind query parameters, but allow empty filters if err := c.ShouldBindQuery(&filter); err != nil && len(filter) > 0 { appErr := models.NewErrValidation("invalid_query_params", nil, err) models.RespondWithError(c, appErr) return } components, err := h.store.GetAllComponents(ctx) if err != nil { appErr := models.NewErrInternalServer("failed_list_components", fmt.Errorf("Failed to list components: %w", err)) models.RespondWithError(c, appErr) return } // If empty, return an empty list if len(components) == 0 { c.JSON(http.StatusOK, []models.Component{}) return } c.JSON(http.StatusOK, components) } // CreateComponent creates a new component func (h *ComponentHandler) CreateComponent(c *gin.Context) { var component models.Component ctx := c.Request.Context() if err := c.ShouldBindJSON(&component); err != nil { appErr := models.NewErrValidation("invalid_request_body", nil, err) models.RespondWithError(c, appErr) return } // Get the user ID from the context (set by auth middleware) userIDInterface, exists := c.Get("user_id") if !exists { appErr := models.NewErrUnauthorized("user_id_not_found", fmt.Errorf("User ID not found in context")) models.RespondWithError(c, appErr) return } // Convert user_id to int - it might be a string from JWT var userID int switch v := userIDInterface.(type) { case string: if parsedID, err := strconv.Atoi(v); err == nil { userID = parsedID } else { h.entry.Warnf("Failed to parse user_id string '%s' to int, defaulting. Error: %v", v, err) userID = 1 } case int: userID = v case int64: userID = int(v) default: h.entry.Warnf("User_id in context is of unexpected type %T, defaulting.", v) userID = 1 } // Set the user ID on the component component.UserID = userID // Validate component data if validationErrors := h.validateComponentRequest(&component); len(validationErrors) > 0 { appErr := models.NewErrValidation("invalid_component_data", validationErrors, nil) models.RespondWithError(c, appErr) return } // Set initial status to validating component.Status = "validating" // Create the component id, err := h.store.CreateComponent(ctx, &component) if err != nil { appErr := models.NewErrInternalServer("failed_create_component", fmt.Errorf("Failed to create component: %w", err)) models.RespondWithError(c, appErr) return } // Set the generated ID component.ID = id // Start async validation h.entry.WithField("component_id", component.ID).Info("Starting async validation for component") go h.validateComponent(context.Background(), &component) c.JSON(http.StatusCreated, component) } // validateComponent asynchronously validates the component's repository and Dockerfile // validateComponent checks if repository URL is valid, branch exists, and Dockerfile is present // if Dockerfile is not present, it starts an async generation process func (h *ComponentHandler) validateComponent(ctx context.Context, component *models.Component) { h.entry.WithField("component_id", component.ID).Info("Starting validation for component") // Create a temporary directory for cloning and Dockerfile generation tempDir, err := os.MkdirTemp("", fmt.Sprintf("byop-validate-%d-*", component.ID)) if err != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to create temp directory: %v", err) // Update component status to invalid - use background context to avoid cancellation if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Failed to create temp dir: %v", err)); updateErr != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr) } return } // Change permissions of tempDir to allow access by other users (e.g., buildkitd) if err := os.Chmod(tempDir, 0755); err != nil { h.entry.Errorf("Failed to chmod tempDir %s for component %d: %v", tempDir, component.ID, err) if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Failed to set permissions on temp dir: %v", err)); updateErr != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr) } // Attempt to clean up tempDir if chmod fails and we are returning early, // as no build job will be queued for it. if errRemove := os.RemoveAll(tempDir); errRemove != nil { h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after chmod failure: %v", tempDir, errRemove) } return } h.entry.Debugf("Set permissions to 0755 for tempDir: %s", tempDir) // Log the start of validation h.entry.WithField("component_id", component.ID).Info("Validating component repository and Dockerfile") if err := h.validateRepoAndBranch(ctx, *component, tempDir); err != nil { h.entry.WithField("component_id", component.ID).Errorf("Validation failed: %v", err) // Update component status to invalid - use background context to avoid cancellation if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", err.Error()); updateErr != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr) } // Attempt to clean up tempDir if validation fails and we are returning early. if errRemove := os.RemoveAll(tempDir); errRemove != nil { h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after validation failure: %v", tempDir, errRemove) } return } // If Dockerfile does not exist, start generation h.entry.WithField("component_id", component.ID).Info("Dockerfile not found, starting generation") if err := h.store.UpdateComponentStatus(context.Background(), component.ID, "generating", "Dockerfile not found, generating..."); err != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status to generating: %v", err) return } // Guess the type of Dockerfile to generate based on component type stack, err := analyzer.AnalyzeCode(tempDir) if err != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to analyze code for Dockerfile generation: %v", err) // Update component status to invalid - use background context to avoid cancellation if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Code analysis failed: %v", err)); updateErr != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr) } // Attempt to clean up tempDir if analysis fails. if errRemove := os.RemoveAll(tempDir); errRemove != nil { h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after code analysis failure: %v", tempDir, errRemove) } return } dockerfileContent, err := stack.GenerateDockerfile(tempDir) if err != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to generate Dockerfile: %v", err) // Update component status to invalid - use background context to avoid cancellation if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Dockerfile generation failed: %v", err)); updateErr != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr) } // Attempt to clean up tempDir if Dockerfile generation fails. if errRemove := os.RemoveAll(tempDir); errRemove != nil { h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after Dockerfile generation failure: %v", tempDir, errRemove) } return } // Write the generated Dockerfile to the temp directory dockerfilePath := filepath.Join(tempDir, "Dockerfile") if err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644); err != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to write generated Dockerfile: %v", err) // Update component status to invalid - use background context to avoid cancellation if updateErr := h.store.UpdateComponentStatus(context.Background(), component.ID, "invalid", fmt.Sprintf("Failed to write Dockerfile: %v", err)); updateErr != nil { h.entry.WithField("component_id", component.ID).Errorf("Failed to update component status: %v", updateErr) } // Attempt to clean up tempDir if writing fails. if errRemove := os.RemoveAll(tempDir); errRemove != nil { h.entry.WithField("component_id", component.ID).Errorf("Error removing temp dir %s after Dockerfile write failure: %v", tempDir, errRemove) } return } h.entry.WithField("component_id", component.ID).Info("Dockerfile generated, queueing build job.") // Debug: Log the first few lines of generated Dockerfile content lines := strings.Split(dockerfileContent, "\n") if len(lines) > 5 { lines = lines[:5] } h.entry.WithField("component_id", component.ID).Infof("Generated Dockerfile first 5 lines:\n%s", strings.Join(lines, "\n")) // Queue the build job with the generated Dockerfile h.buildSvc.QueueBuildJob(context.Background(), models.BuildRequest{ ComponentID: uint(component.ID), SourceURL: component.Repository, ImageName: fmt.Sprintf("byop-component-%d", component.ID), Version: "latest", // Default version, can be changed later BuildContext: tempDir, // Use the temp directory as the build context DockerfileContent: dockerfileContent, // Pass the generated Dockerfile content Dockerfile: "Dockerfile", // Standard Dockerfile name RegistryURL: h.registryUrl, // Use the configured registry URL }) // Do not remove tempDir here; buildSvc is responsible for it. } // validateRepository validates the Git repository func (h *ComponentHandler) validateRepoAndBranch(ctx context.Context, component models.Component, tempDir string) error { // Clone the repository h.entry.WithField("component_id", component.ID).Infof("Cloning repository %s on branch %s to %s", component.Repository, component.Branch, tempDir) if err := h.cloneRepository(component.Repository, component.Branch, tempDir); err != nil { return fmt.Errorf("failed to clone repository: %w", err) } h.entry.WithField("component_id", component.ID).Info("Repository and Dockerfile validation successful") return nil } // cloneRepository clones a Git repository to the specified directory func (h *ComponentHandler) cloneRepository(repoURL, branch, targetDir string) error { // Create target directory if err := os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("failed to create target directory: %w", err) } // Default branch if not specified if branch == "" { branch = "main" } // Try to clone with the specified branch _, err := git.PlainClone(targetDir, false, &git.CloneOptions{ URL: repoURL, ReferenceName: plumbing.ReferenceName("refs/heads/" + branch), SingleBranch: true, Depth: 1, }) if err == nil { h.entry.WithField("repo_url", repoURL).WithField("branch", branch).Infof("Successfully cloned repository to %s", targetDir) return nil } // If the specified branch fails and it's "main", try "master" if branch == "main" { h.entry.WithField("repo_url", repoURL).WithField("branch", branch).Warnf("Failed to clone with 'main' branch, trying 'master': %v", err) // Clean up the failed clone attempt os.RemoveAll(targetDir) if err := os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("failed to recreate target directory: %w", err) } _, err := git.PlainClone(targetDir, false, &git.CloneOptions{ URL: repoURL, ReferenceName: plumbing.ReferenceName("refs/heads/master"), SingleBranch: true, Depth: 1, }) if err == nil { h.entry.WithField("repo_url", repoURL).WithField("branch", "master").Infof("Successfully cloned repository to %s using 'master' branch", targetDir) return nil } h.entry.WithField("repo_url", repoURL).Errorf("Failed to clone with both 'main' and 'master' branches") return fmt.Errorf("failed to clone repository %s: tried both 'main' and 'master' branches, last error: %w", repoURL, err) } h.entry.WithField("repo_url", repoURL).WithField("branch", branch).Warnf("Failed to clone repository to %s: %v", targetDir, err) return fmt.Errorf("failed to clone repository %s (branch %s): %w", repoURL, branch, err) } // GetComponent returns a specific component func (h *ComponentHandler) GetComponent(c *gin.Context) { idStr := c.Param("id") ctx := c.Request.Context() id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err) models.RespondWithError(c, appErr) return } component, err := h.store.GetComponentByID(ctx, int(id)) if err != nil { models.RespondWithError(c, err) return } if component.ID == 0 { appErr := models.NewErrNotFound("component_not_found_explicit_check", fmt.Errorf("Component with ID %d not found after store call", id)) models.RespondWithError(c, appErr) return } c.JSON(http.StatusOK, component) } // UpdateComponent updates a component func (h *ComponentHandler) UpdateComponent(c *gin.Context) { idStr := c.Param("id") ctx := c.Request.Context() id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err) models.RespondWithError(c, appErr) return } var updatedComponent models.Component if err := c.ShouldBindJSON(&updatedComponent); err != nil { appErr := models.NewErrValidation("invalid_request_body", nil, err) models.RespondWithError(c, appErr) return } // Ensure the ID matches the URL parameter updatedComponent.ID = int(id) // Validate component data if validationErrors := h.validateComponentRequest(&updatedComponent); len(validationErrors) > 0 { appErr := models.NewErrValidation("invalid_component_data_update", validationErrors, nil) models.RespondWithError(c, appErr) return } if err := h.store.UpdateComponent(ctx, updatedComponent); err != nil { models.RespondWithError(c, err) return } c.JSON(http.StatusOK, updatedComponent) } // DeleteComponent deletes a component func (h *ComponentHandler) DeleteComponent(c *gin.Context) { idStr := c.Param("id") ctx := c.Request.Context() id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err) models.RespondWithError(c, appErr) return } if err := h.store.DeleteComponent(ctx, int(id)); err != nil { models.RespondWithError(c, err) return } c.JSON(http.StatusOK, gin.H{"message": "Component deleted successfully"}) } // GetComponentDeployments returns all deployments for a component func (h *ComponentHandler) GetComponentDeployments(c *gin.Context) { idStr := c.Param("id") ctx := c.Request.Context() id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err) models.RespondWithError(c, appErr) return } // Check if component exists component, err := h.store.GetComponentByID(ctx, int(id)) if err != nil { models.RespondWithError(c, err) return } if component.ID == 0 { appErr := models.NewErrNotFound("component_not_found_for_deployments", fmt.Errorf("Component with ID %d not found when listing deployments", id)) models.RespondWithError(c, appErr) return } // TODO: Retrieve deployments - this likely requires a separate repository or service deployments := []models.Deployment{} c.JSON(http.StatusOK, deployments) } // validateComponentRequest checks if the component data is valid and returns a map of validation errors func (h *ComponentHandler) validateComponentRequest(component *models.Component) map[string]string { errors := make(map[string]string) if component.Name == "" { errors["name"] = "Component name is required" } if component.Type == "" { errors["type"] = "Component type is required" } if component.Repository == "" { errors["repository"] = "Component Git URL is required" } return errors }