compose_parser.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package services
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strconv"
  7. "strings"
  8. "git.linuxforward.com/byop/byop-engine/models"
  9. "github.com/sirupsen/logrus"
  10. "gopkg.in/yaml.v3"
  11. )
  12. // ComposeParser handles parsing and analyzing docker-compose.yml files
  13. type ComposeParser struct {
  14. entry *logrus.Entry
  15. }
  16. // NewComposeParser creates a new ComposeParser instance
  17. func NewComposeParser() *ComposeParser {
  18. return &ComposeParser{
  19. entry: logrus.WithField("service", "ComposeParser"),
  20. }
  21. }
  22. // ComposeFile represents the structure of a docker-compose.yml file
  23. type ComposeFile struct {
  24. Version string `yaml:"version,omitempty"`
  25. Services map[string]ComposeServiceYAML `yaml:"services"`
  26. }
  27. // ComposeService represents a service in docker-compose.yml
  28. type ComposeServiceYAML struct {
  29. Image string `yaml:"image,omitempty"`
  30. Build interface{} `yaml:"build,omitempty"` // Can be string or object
  31. Ports []interface{} `yaml:"ports,omitempty"` // Can be strings or objects
  32. Environment interface{} `yaml:"environment,omitempty"` // Can be array or map
  33. Volumes []interface{} `yaml:"volumes,omitempty"`
  34. Depends_On []string `yaml:"depends_on,omitempty"`
  35. }
  36. // ComposeServiceBuild represents the build configuration
  37. type ComposeServiceBuild struct {
  38. Context string `yaml:"context,omitempty"`
  39. Dockerfile string `yaml:"dockerfile,omitempty"`
  40. }
  41. // ParseComposeFile parses a docker-compose.yml file from a directory path
  42. func (cp *ComposeParser) ParseComposeFile(projectDir, composePath string) (*models.AppImportReview, error) {
  43. fullComposePath := filepath.Join(projectDir, composePath)
  44. cp.entry.Infof("Parsing compose file at: %s", fullComposePath)
  45. // Check if compose file exists
  46. if _, err := os.Stat(fullComposePath); os.IsNotExist(err) {
  47. return &models.AppImportReview{
  48. Valid: false,
  49. Error: fmt.Sprintf("docker-compose.yml file not found at %s", composePath),
  50. }, nil
  51. }
  52. // Read the compose file
  53. content, err := os.ReadFile(fullComposePath)
  54. if err != nil {
  55. return &models.AppImportReview{
  56. Valid: false,
  57. Error: fmt.Sprintf("Failed to read docker-compose.yml: %v", err),
  58. }, nil
  59. }
  60. return cp.ParseComposeContent(string(content), filepath.Base(projectDir))
  61. }
  62. // ParseComposeContent parses docker-compose content directly from string
  63. func (cp *ComposeParser) ParseComposeContent(content, projectName string) (*models.AppImportReview, error) {
  64. cp.entry.Info("Parsing compose content from string")
  65. var composeFile ComposeFile
  66. if err := yaml.Unmarshal([]byte(content), &composeFile); err != nil {
  67. cp.entry.Errorf("Failed to parse YAML: %v", err)
  68. return &models.AppImportReview{
  69. Valid: false,
  70. Error: fmt.Sprintf("Failed to parse docker-compose.yml: %v", err),
  71. }, nil
  72. }
  73. if len(composeFile.Services) == 0 {
  74. return &models.AppImportReview{
  75. Valid: false,
  76. Error: "No services found in docker-compose.yml",
  77. }, nil
  78. }
  79. // Convert compose services to our internal format
  80. services := make([]models.ComposeService, 0, len(composeFile.Services))
  81. for serviceName, serviceConfig := range composeFile.Services {
  82. composeService := models.ComposeService{
  83. Name: serviceName,
  84. }
  85. // Determine if this is a build or image service
  86. if serviceConfig.Build != nil {
  87. composeService.Source = "build"
  88. // Handle build configuration (can be string or object)
  89. switch build := serviceConfig.Build.(type) {
  90. case string:
  91. composeService.BuildContext = build
  92. composeService.Dockerfile = "Dockerfile" // default
  93. case map[string]interface{}:
  94. if context, ok := build["context"].(string); ok {
  95. composeService.BuildContext = context
  96. }
  97. if dockerfile, ok := build["dockerfile"].(string); ok {
  98. composeService.Dockerfile = dockerfile
  99. } else {
  100. composeService.Dockerfile = "Dockerfile" // default
  101. }
  102. }
  103. } else if serviceConfig.Image != "" {
  104. composeService.Source = "image"
  105. composeService.Image = serviceConfig.Image
  106. } else {
  107. cp.entry.Warnf("Service %s has neither build nor image configuration", serviceName)
  108. continue
  109. }
  110. // Extract ports
  111. for _, port := range serviceConfig.Ports {
  112. switch p := port.(type) {
  113. case string:
  114. composeService.Ports = append(composeService.Ports, p)
  115. case int:
  116. composeService.Ports = append(composeService.Ports, strconv.Itoa(p))
  117. case map[string]interface{}:
  118. if target, ok := p["target"].(int); ok {
  119. portStr := strconv.Itoa(target)
  120. if published, ok := p["published"].(int); ok {
  121. portStr = fmt.Sprintf("%d:%d", published, target)
  122. }
  123. composeService.Ports = append(composeService.Ports, portStr)
  124. }
  125. }
  126. }
  127. // Extract environment variables
  128. if serviceConfig.Environment != nil {
  129. composeService.Environment = make(map[string]string)
  130. switch env := serviceConfig.Environment.(type) {
  131. case []interface{}:
  132. for _, item := range env {
  133. if envStr, ok := item.(string); ok {
  134. parts := strings.SplitN(envStr, "=", 2)
  135. if len(parts) == 2 {
  136. composeService.Environment[parts[0]] = parts[1]
  137. }
  138. }
  139. }
  140. case map[string]interface{}:
  141. for key, value := range env {
  142. if valueStr, ok := value.(string); ok {
  143. composeService.Environment[key] = valueStr
  144. }
  145. }
  146. }
  147. }
  148. // Extract volumes
  149. for _, volume := range serviceConfig.Volumes {
  150. if volumeStr, ok := volume.(string); ok {
  151. composeService.Volumes = append(composeService.Volumes, volumeStr)
  152. }
  153. }
  154. services = append(services, composeService)
  155. }
  156. // Use provided project name or default
  157. appName := projectName
  158. if appName == "" {
  159. appName = "imported-app"
  160. }
  161. review := &models.AppImportReview{
  162. AppName: appName,
  163. Services: services,
  164. Valid: true,
  165. }
  166. cp.entry.Infof("Successfully parsed compose file with %d services", len(services))
  167. return review, nil
  168. }