123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- 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
- }
|