project.go 4.0 KB

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