project.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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. name = r.ReplaceAllString(strings.ToLower(filepath.Base(options.WorkDir)), "")
  47. }
  48. return newProject(types.ConfigDetails{
  49. WorkingDir: options.WorkDir,
  50. ConfigFiles: configs,
  51. Environment: getAsEqualsMap(options.Environment),
  52. }, name)
  53. }
  54. func newProject(config types.ConfigDetails, name string) (*Project, error) {
  55. model, err := loader.Load(config)
  56. if err != nil {
  57. return nil, err
  58. }
  59. p := Project{
  60. Config: *model,
  61. projectDir: config.WorkingDir,
  62. Name: name,
  63. }
  64. return &p, nil
  65. }
  66. func getConfigPathFromOptions(options *ProjectOptions) ([]string, error) {
  67. var paths []string
  68. pwd := options.WorkDir
  69. if len(options.ConfigPaths) != 0 {
  70. for _, f := range options.ConfigPaths {
  71. if f == "-" {
  72. paths = append(paths, f)
  73. continue
  74. }
  75. if !filepath.IsAbs(f) {
  76. f = filepath.Join(pwd, f)
  77. }
  78. if _, err := os.Stat(f); err != nil {
  79. return nil, err
  80. }
  81. paths = append(paths, f)
  82. }
  83. return paths, nil
  84. }
  85. for {
  86. var candidates []string
  87. for _, n := range supportedFilenames {
  88. f := filepath.Join(pwd, n)
  89. if _, err := os.Stat(f); err == nil {
  90. candidates = append(candidates, f)
  91. }
  92. }
  93. if len(candidates) > 0 {
  94. winner := candidates[0]
  95. if len(candidates) > 1 {
  96. logrus.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", "))
  97. logrus.Warnf("Using %s\n", winner)
  98. }
  99. return []string{winner}, nil
  100. }
  101. parent := filepath.Dir(pwd)
  102. if parent == pwd {
  103. return nil, fmt.Errorf("can't find a suitable configuration file in this directory or any parent. Is %q the right directory?", pwd)
  104. }
  105. pwd = parent
  106. }
  107. }
  108. func parseConfigs(configPaths []string) ([]types.ConfigFile, error) {
  109. var files []types.ConfigFile
  110. for _, f := range configPaths {
  111. var b []byte
  112. var err error
  113. if f == "-" {
  114. return []types.ConfigFile{}, errors.New("reading compose file from stdin is not supported")
  115. }
  116. if _, err := os.Stat(f); err != nil {
  117. return nil, err
  118. }
  119. b, err = ioutil.ReadFile(f)
  120. if err != nil {
  121. return nil, err
  122. }
  123. config, err := loader.ParseYAML(b)
  124. if err != nil {
  125. return nil, err
  126. }
  127. files = append(files, types.ConfigFile{Filename: f, Config: config})
  128. }
  129. return files, nil
  130. }
  131. // getAsEqualsMap split key=value formatted strings into a key : value map
  132. func getAsEqualsMap(em []string) map[string]string {
  133. m := make(map[string]string)
  134. for _, v := range em {
  135. kv := strings.SplitN(v, "=", 2)
  136. m[kv[0]] = kv[1]
  137. }
  138. return m
  139. }