package services import ( "fmt" "os" "path/filepath" "strconv" "strings" "git.linuxforward.com/byop/byop-engine/models" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) // ComposeParser handles parsing and analyzing docker-compose.yml files type ComposeParser struct { entry *logrus.Entry } // NewComposeParser creates a new ComposeParser instance func NewComposeParser() *ComposeParser { return &ComposeParser{ entry: logrus.WithField("service", "ComposeParser"), } } // ComposeFile represents the structure of a docker-compose.yml file type ComposeFile struct { Version string `yaml:"version,omitempty"` Services map[string]ComposeServiceYAML `yaml:"services"` } // ComposeService represents a service in docker-compose.yml type ComposeServiceYAML struct { Image string `yaml:"image,omitempty"` Build interface{} `yaml:"build,omitempty"` // Can be string or object Ports []interface{} `yaml:"ports,omitempty"` // Can be strings or objects Environment interface{} `yaml:"environment,omitempty"` // Can be array or map Volumes []interface{} `yaml:"volumes,omitempty"` Depends_On []string `yaml:"depends_on,omitempty"` } // ComposeServiceBuild represents the build configuration type ComposeServiceBuild struct { Context string `yaml:"context,omitempty"` Dockerfile string `yaml:"dockerfile,omitempty"` } // ParseComposeFile parses a docker-compose.yml file from a directory path func (cp *ComposeParser) ParseComposeFile(projectDir, composePath string) (*models.AppImportReview, error) { fullComposePath := filepath.Join(projectDir, composePath) cp.entry.Infof("Parsing compose file at: %s", fullComposePath) // Check if compose file exists if _, err := os.Stat(fullComposePath); os.IsNotExist(err) { return &models.AppImportReview{ Valid: false, Error: fmt.Sprintf("docker-compose.yml file not found at %s", composePath), }, nil } // Read the compose file content, err := os.ReadFile(fullComposePath) if err != nil { return &models.AppImportReview{ Valid: false, Error: fmt.Sprintf("Failed to read docker-compose.yml: %v", err), }, nil } return cp.ParseComposeContent(string(content), filepath.Base(projectDir)) } // ParseComposeContent parses docker-compose content directly from string func (cp *ComposeParser) ParseComposeContent(content, projectName string) (*models.AppImportReview, error) { cp.entry.Info("Parsing compose content from string") var composeFile ComposeFile if err := yaml.Unmarshal([]byte(content), &composeFile); err != nil { cp.entry.Errorf("Failed to parse YAML: %v", err) return &models.AppImportReview{ Valid: false, Error: fmt.Sprintf("Failed to parse docker-compose.yml: %v", err), }, nil } if len(composeFile.Services) == 0 { return &models.AppImportReview{ Valid: false, Error: "No services found in docker-compose.yml", }, nil } // Convert compose services to our internal format services := make([]models.ComposeService, 0, len(composeFile.Services)) for serviceName, serviceConfig := range composeFile.Services { composeService := models.ComposeService{ Name: serviceName, } // Determine if this is a build or image service if serviceConfig.Build != nil { composeService.Source = "build" // Handle build configuration (can be string or object) switch build := serviceConfig.Build.(type) { case string: composeService.BuildContext = build composeService.Dockerfile = "Dockerfile" // default case map[string]interface{}: if context, ok := build["context"].(string); ok { composeService.BuildContext = context } if dockerfile, ok := build["dockerfile"].(string); ok { composeService.Dockerfile = dockerfile } else { composeService.Dockerfile = "Dockerfile" // default } } } else if serviceConfig.Image != "" { composeService.Source = "image" composeService.Image = serviceConfig.Image } else { cp.entry.Warnf("Service %s has neither build nor image configuration", serviceName) continue } // Extract ports for _, port := range serviceConfig.Ports { switch p := port.(type) { case string: composeService.Ports = append(composeService.Ports, p) case int: composeService.Ports = append(composeService.Ports, strconv.Itoa(p)) case map[string]interface{}: if target, ok := p["target"].(int); ok { portStr := strconv.Itoa(target) if published, ok := p["published"].(int); ok { portStr = fmt.Sprintf("%d:%d", published, target) } composeService.Ports = append(composeService.Ports, portStr) } } } // Extract environment variables if serviceConfig.Environment != nil { composeService.Environment = make(map[string]string) switch env := serviceConfig.Environment.(type) { case []interface{}: for _, item := range env { if envStr, ok := item.(string); ok { parts := strings.SplitN(envStr, "=", 2) if len(parts) == 2 { composeService.Environment[parts[0]] = parts[1] } } } case map[string]interface{}: for key, value := range env { if valueStr, ok := value.(string); ok { composeService.Environment[key] = valueStr } } } } // Extract volumes for _, volume := range serviceConfig.Volumes { if volumeStr, ok := volume.(string); ok { composeService.Volumes = append(composeService.Volumes, volumeStr) } } services = append(services, composeService) } // Use provided project name or default appName := projectName if appName == "" { appName = "imported-app" } review := &models.AppImportReview{ AppName: appName, Services: services, Valid: true, } cp.entry.Infof("Successfully parsed compose file with %d services", len(services)) return review, nil }