project.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. package compose
  2. import (
  3. "errors"
  4. "fmt"
  5. "io/ioutil"
  6. "os"
  7. "path/filepath"
  8. "regexp"
  9. "strings"
  10. "github.com/compose-spec/compose-go/loader"
  11. "github.com/compose-spec/compose-go/types"
  12. "github.com/sirupsen/logrus"
  13. )
  14. var supportedFilenames = []string{
  15. "compose.yml",
  16. "compose.yaml",
  17. "docker-compose.yml",
  18. "docker-compose.yaml",
  19. }
  20. // ProjectOptions configures a compose project
  21. type ProjectOptions struct {
  22. Name string
  23. WorkDir string
  24. ConfigPaths []string
  25. Environment []string
  26. }
  27. // Project represents a compose project with a name
  28. type Project struct {
  29. types.Config
  30. projectDir string
  31. Name string `yaml:"-" json:"-"`
  32. }
  33. // ProjectFromOptions load a compose project based on given options
  34. func ProjectFromOptions(options *ProjectOptions) (*Project, error) {
  35. configPath, err := getConfigPathFromOptions(options)
  36. if err != nil {
  37. return nil, err
  38. }
  39. configs, err := parseConfigs(configPath)
  40. if err != nil {
  41. return nil, err
  42. }
  43. name := options.Name
  44. if name == "" {
  45. r := regexp.MustCompile(`[^a-z0-9\\-_]+`)
  46. absPath, err := filepath.Abs(options.WorkDir)
  47. if err != nil {
  48. return nil, err
  49. }
  50. name = r.ReplaceAllString(strings.ToLower(filepath.Base(absPath)), "")
  51. }
  52. return newProject(types.ConfigDetails{
  53. WorkingDir: options.WorkDir,
  54. ConfigFiles: configs,
  55. Environment: getAsEqualsMap(options.Environment),
  56. }, name)
  57. }
  58. func newProject(config types.ConfigDetails, name string) (*Project, error) {
  59. model, err := loader.Load(config)
  60. if err != nil {
  61. return nil, err
  62. }
  63. p := Project{
  64. Config: *model,
  65. projectDir: config.WorkingDir,
  66. Name: name,
  67. }
  68. return &p, nil
  69. }
  70. func getConfigPathFromOptions(options *ProjectOptions) ([]string, error) {
  71. var paths []string
  72. pwd := options.WorkDir
  73. if len(options.ConfigPaths) != 0 {
  74. for _, f := range options.ConfigPaths {
  75. if f == "-" {
  76. paths = append(paths, f)
  77. continue
  78. }
  79. if !filepath.IsAbs(f) {
  80. f = filepath.Join(pwd, f)
  81. }
  82. if _, err := os.Stat(f); err != nil {
  83. return nil, err
  84. }
  85. paths = append(paths, f)
  86. }
  87. return paths, nil
  88. }
  89. for {
  90. var candidates []string
  91. for _, n := range supportedFilenames {
  92. f := filepath.Join(pwd, n)
  93. if _, err := os.Stat(f); err == nil {
  94. candidates = append(candidates, f)
  95. }
  96. }
  97. if len(candidates) > 0 {
  98. winner := candidates[0]
  99. if len(candidates) > 1 {
  100. logrus.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", "))
  101. logrus.Warnf("Using %s\n", winner)
  102. }
  103. return []string{winner}, nil
  104. }
  105. parent := filepath.Dir(pwd)
  106. if parent == pwd {
  107. return nil, fmt.Errorf("can't find a suitable configuration file in this directory or any parent. Is %q the right directory?", pwd)
  108. }
  109. pwd = parent
  110. }
  111. }
  112. func parseConfigs(configPaths []string) ([]types.ConfigFile, error) {
  113. var files []types.ConfigFile
  114. for _, f := range configPaths {
  115. var b []byte
  116. var err error
  117. if f == "-" {
  118. return []types.ConfigFile{}, errors.New("reading compose file from stdin is not supported")
  119. }
  120. if _, err := os.Stat(f); err != nil {
  121. return nil, err
  122. }
  123. b, err = ioutil.ReadFile(f)
  124. if err != nil {
  125. return nil, err
  126. }
  127. config, err := loader.ParseYAML(b)
  128. if err != nil {
  129. return nil, err
  130. }
  131. files = append(files, types.ConfigFile{Filename: f, Config: config})
  132. }
  133. return files, nil
  134. }
  135. // getAsEqualsMap split key=value formatted strings into a key : value map
  136. func getAsEqualsMap(em []string) map[string]string {
  137. m := make(map[string]string)
  138. for _, v := range em {
  139. kv := strings.SplitN(v, "=", 2)
  140. m[kv[0]] = kv[1]
  141. }
  142. return m
  143. }