config.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  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. "bytes"
  16. "context"
  17. "fmt"
  18. "os"
  19. "sort"
  20. "strings"
  21. "github.com/compose-spec/compose-go/cli"
  22. "github.com/compose-spec/compose-go/types"
  23. "github.com/spf13/cobra"
  24. "github.com/docker/compose/v2/pkg/api"
  25. "github.com/docker/compose/v2/pkg/compose"
  26. )
  27. type configOptions struct {
  28. *ProjectOptions
  29. Format string
  30. Output string
  31. quiet bool
  32. resolveImageDigests bool
  33. noInterpolate bool
  34. noNormalize bool
  35. services bool
  36. volumes bool
  37. profiles bool
  38. images bool
  39. hash string
  40. noConsistency bool
  41. }
  42. func (o *configOptions) ToProject(services []string) (*types.Project, error) {
  43. return o.ProjectOptions.ToProject(services,
  44. cli.WithInterpolation(!o.noInterpolate),
  45. cli.WithResolvedPaths(true),
  46. cli.WithNormalization(!o.noNormalize),
  47. cli.WithConsistency(!o.noConsistency),
  48. cli.WithDiscardEnvFile)
  49. }
  50. func convertCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
  51. opts := configOptions{
  52. ProjectOptions: p,
  53. }
  54. cmd := &cobra.Command{
  55. Aliases: []string{"convert"}, // for backward compatibility with Cloud integrations
  56. Use: "config [OPTIONS] [SERVICE...]",
  57. Short: "Parse, resolve and render compose file in canonical format",
  58. PreRunE: Adapt(func(ctx context.Context, args []string) error {
  59. if opts.quiet {
  60. devnull, err := os.Open(os.DevNull)
  61. if err != nil {
  62. return err
  63. }
  64. os.Stdout = devnull
  65. }
  66. if p.Compatibility {
  67. opts.noNormalize = true
  68. }
  69. return nil
  70. }),
  71. RunE: Adapt(func(ctx context.Context, args []string) error {
  72. if opts.services {
  73. return runServices(streams, opts)
  74. }
  75. if opts.volumes {
  76. return runVolumes(streams, opts)
  77. }
  78. if opts.hash != "" {
  79. return runHash(streams, opts)
  80. }
  81. if opts.profiles {
  82. return runProfiles(streams, opts, args)
  83. }
  84. if opts.images {
  85. return runConfigImages(streams, opts, args)
  86. }
  87. return runConfig(ctx, streams, backend, opts, args)
  88. }),
  89. ValidArgsFunction: completeServiceNames(p),
  90. }
  91. flags := cmd.Flags()
  92. flags.StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
  93. flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests.")
  94. flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything.")
  95. flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables.")
  96. flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model.")
  97. flags.BoolVar(&opts.noConsistency, "no-consistency", false, "Don't check model consistency - warning: may produce invalid Compose output")
  98. flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.")
  99. flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.")
  100. flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line.")
  101. flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line.")
  102. flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line.")
  103. flags.StringVarP(&opts.Output, "output", "o", "", "Save to file (default to stdout)")
  104. return cmd
  105. }
  106. func runConfig(ctx context.Context, streams api.Streams, backend api.Service, opts configOptions, services []string) error {
  107. var content []byte
  108. project, err := opts.ToProject(services)
  109. if err != nil {
  110. return err
  111. }
  112. content, err = backend.Config(ctx, project, api.ConfigOptions{
  113. Format: opts.Format,
  114. Output: opts.Output,
  115. ResolveImageDigests: opts.resolveImageDigests,
  116. })
  117. if err != nil {
  118. return err
  119. }
  120. if !opts.noInterpolate {
  121. content = escapeDollarSign(content)
  122. }
  123. if opts.quiet {
  124. return nil
  125. }
  126. if opts.Output != "" && len(content) > 0 {
  127. return os.WriteFile(opts.Output, content, 0o666)
  128. }
  129. _, err = fmt.Fprint(streams.Out(), string(content))
  130. return err
  131. }
  132. func runServices(streams api.Streams, opts configOptions) error {
  133. project, err := opts.ToProject(nil)
  134. if err != nil {
  135. return err
  136. }
  137. return project.WithServices(project.ServiceNames(), func(s types.ServiceConfig) error {
  138. fmt.Fprintln(streams.Out(), s.Name)
  139. return nil
  140. })
  141. }
  142. func runVolumes(streams api.Streams, opts configOptions) error {
  143. project, err := opts.ToProject(nil)
  144. if err != nil {
  145. return err
  146. }
  147. for n := range project.Volumes {
  148. fmt.Fprintln(streams.Out(), n)
  149. }
  150. return nil
  151. }
  152. func runHash(streams api.Streams, opts configOptions) error {
  153. var services []string
  154. if opts.hash != "*" {
  155. services = append(services, strings.Split(opts.hash, ",")...)
  156. }
  157. project, err := opts.ToProject(nil)
  158. if err != nil {
  159. return err
  160. }
  161. if len(services) > 0 {
  162. err = withSelectedServicesOnly(project, services)
  163. if err != nil {
  164. return err
  165. }
  166. }
  167. sorted := project.Services
  168. sort.Slice(sorted, func(i, j int) bool {
  169. return sorted[i].Name < sorted[j].Name
  170. })
  171. for _, s := range sorted {
  172. hash, err := compose.ServiceHash(s)
  173. if err != nil {
  174. return err
  175. }
  176. fmt.Fprintf(streams.Out(), "%s %s\n", s.Name, hash)
  177. }
  178. return nil
  179. }
  180. func runProfiles(streams api.Streams, opts configOptions, services []string) error {
  181. set := map[string]struct{}{}
  182. project, err := opts.ToProject(services)
  183. if err != nil {
  184. return err
  185. }
  186. for _, s := range project.AllServices() {
  187. for _, p := range s.Profiles {
  188. set[p] = struct{}{}
  189. }
  190. }
  191. profiles := make([]string, 0, len(set))
  192. for p := range set {
  193. profiles = append(profiles, p)
  194. }
  195. sort.Strings(profiles)
  196. for _, p := range profiles {
  197. fmt.Fprintln(streams.Out(), p)
  198. }
  199. return nil
  200. }
  201. func runConfigImages(streams api.Streams, opts configOptions, services []string) error {
  202. project, err := opts.ToProject(services)
  203. if err != nil {
  204. return err
  205. }
  206. for _, s := range project.Services {
  207. if s.Image != "" {
  208. fmt.Fprintln(streams.Out(), s.Image)
  209. } else {
  210. fmt.Fprintf(streams.Out(), "%s%s%s\n", project.Name, api.Separator, s.Name)
  211. }
  212. }
  213. return nil
  214. }
  215. func escapeDollarSign(marshal []byte) []byte {
  216. dollar := []byte{'$'}
  217. escDollar := []byte{'$', '$'}
  218. return bytes.ReplaceAll(marshal, dollar, escDollar)
  219. }