config.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  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. "encoding/json"
  18. "fmt"
  19. "io"
  20. "os"
  21. "slices"
  22. "sort"
  23. "strings"
  24. "github.com/compose-spec/compose-go/v2/cli"
  25. "github.com/compose-spec/compose-go/v2/template"
  26. "github.com/compose-spec/compose-go/v2/types"
  27. "github.com/docker/cli/cli/command"
  28. "github.com/spf13/cobra"
  29. "go.yaml.in/yaml/v4"
  30. "github.com/docker/compose/v5/cmd/formatter"
  31. "github.com/docker/compose/v5/pkg/api"
  32. "github.com/docker/compose/v5/pkg/compose"
  33. )
  34. type configOptions struct {
  35. *ProjectOptions
  36. Format string
  37. Output string
  38. quiet bool
  39. resolveImageDigests bool
  40. noInterpolate bool
  41. noNormalize bool
  42. noResolvePath bool
  43. noResolveEnv bool
  44. services bool
  45. volumes bool
  46. networks bool
  47. models bool
  48. profiles bool
  49. images bool
  50. hash string
  51. noConsistency bool
  52. variables bool
  53. environment bool
  54. lockImageDigests bool
  55. }
  56. func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string) (*types.Project, error) {
  57. project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, backend, services, o.toProjectOptionsFns()...)
  58. return project, err
  59. }
  60. func (o *configOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
  61. po = append(po, o.toProjectOptionsFns()...)
  62. return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...)
  63. }
  64. // toProjectOptionsFns converts config options to cli.ProjectOptionsFn
  65. func (o *configOptions) toProjectOptionsFns() []cli.ProjectOptionsFn {
  66. fns := []cli.ProjectOptionsFn{
  67. cli.WithInterpolation(!o.noInterpolate),
  68. cli.WithResolvedPaths(!o.noResolvePath),
  69. cli.WithNormalization(!o.noNormalize),
  70. cli.WithConsistency(!o.noConsistency),
  71. cli.WithDefaultProfiles(o.Profiles...),
  72. cli.WithDiscardEnvFile,
  73. }
  74. if o.noResolveEnv {
  75. fns = append(fns, cli.WithoutEnvironmentResolution)
  76. }
  77. return fns
  78. }
  79. func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
  80. opts := configOptions{
  81. ProjectOptions: p,
  82. }
  83. cmd := &cobra.Command{
  84. Use: "config [OPTIONS] [SERVICE...]",
  85. Short: "Parse, resolve and render compose file in canonical format",
  86. PreRunE: Adapt(func(ctx context.Context, args []string) error {
  87. if opts.quiet {
  88. devnull, err := os.Open(os.DevNull)
  89. if err != nil {
  90. return err
  91. }
  92. os.Stdout = devnull
  93. }
  94. if p.Compatibility {
  95. opts.noNormalize = true
  96. }
  97. if opts.lockImageDigests {
  98. opts.resolveImageDigests = true
  99. }
  100. return nil
  101. }),
  102. RunE: Adapt(func(ctx context.Context, args []string) error {
  103. if opts.services {
  104. return runServices(ctx, dockerCli, opts)
  105. }
  106. if opts.volumes {
  107. return runVolumes(ctx, dockerCli, opts)
  108. }
  109. if opts.networks {
  110. return runNetworks(ctx, dockerCli, opts)
  111. }
  112. if opts.models {
  113. return runModels(ctx, dockerCli, opts)
  114. }
  115. if opts.hash != "" {
  116. return runHash(ctx, dockerCli, opts)
  117. }
  118. if opts.profiles {
  119. return runProfiles(ctx, dockerCli, opts, args)
  120. }
  121. if opts.images {
  122. return runConfigImages(ctx, dockerCli, opts, args)
  123. }
  124. if opts.variables {
  125. return runVariables(ctx, dockerCli, opts, args)
  126. }
  127. if opts.environment {
  128. return runEnvironment(ctx, dockerCli, opts, args)
  129. }
  130. if opts.Format == "" {
  131. opts.Format = "yaml"
  132. }
  133. return runConfig(ctx, dockerCli, opts, args)
  134. }),
  135. ValidArgsFunction: completeServiceNames(dockerCli, p),
  136. }
  137. flags := cmd.Flags()
  138. flags.StringVar(&opts.Format, "format", "", "Format the output. Values: [yaml | json]")
  139. flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
  140. flags.BoolVar(&opts.lockImageDigests, "lock-image-digests", false, "Produces an override file with image digests")
  141. flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything")
  142. flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables")
  143. flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model")
  144. flags.BoolVar(&opts.noResolvePath, "no-path-resolution", false, "Don't resolve file paths")
  145. flags.BoolVar(&opts.noConsistency, "no-consistency", false, "Don't check model consistency - warning: may produce invalid Compose output")
  146. flags.BoolVar(&opts.noResolveEnv, "no-env-resolution", false, "Don't resolve service env files")
  147. flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.")
  148. flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.")
  149. flags.BoolVar(&opts.networks, "networks", false, "Print the network names, one per line.")
  150. flags.BoolVar(&opts.models, "models", false, "Print the model names, one per line.")
  151. flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line.")
  152. flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line.")
  153. flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line.")
  154. flags.BoolVar(&opts.variables, "variables", false, "Print model variables and default values.")
  155. flags.BoolVar(&opts.environment, "environment", false, "Print environment used for interpolation.")
  156. flags.StringVarP(&opts.Output, "output", "o", "", "Save to file (default to stdout)")
  157. return cmd
  158. }
  159. func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) (err error) {
  160. var content []byte
  161. if opts.noInterpolate {
  162. content, err = runConfigNoInterpolate(ctx, dockerCli, opts, services)
  163. if err != nil {
  164. return err
  165. }
  166. } else {
  167. content, err = runConfigInterpolate(ctx, dockerCli, opts, services)
  168. if err != nil {
  169. return err
  170. }
  171. }
  172. if !opts.noInterpolate {
  173. content = escapeDollarSign(content)
  174. }
  175. if opts.quiet {
  176. return nil
  177. }
  178. if opts.Output != "" && len(content) > 0 {
  179. return os.WriteFile(opts.Output, content, 0o666)
  180. }
  181. _, err = fmt.Fprint(dockerCli.Out(), string(content))
  182. return err
  183. }
  184. func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
  185. backend, err := compose.NewComposeService(dockerCli)
  186. if err != nil {
  187. return nil, err
  188. }
  189. project, err := opts.ToProject(ctx, dockerCli, backend, services)
  190. if err != nil {
  191. return nil, err
  192. }
  193. if opts.resolveImageDigests {
  194. project, err = project.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client()))
  195. if err != nil {
  196. return nil, err
  197. }
  198. }
  199. if !opts.noResolveEnv {
  200. project, err = project.WithServicesEnvironmentResolved(true)
  201. if err != nil {
  202. return nil, err
  203. }
  204. }
  205. if !opts.noConsistency {
  206. err := project.CheckContainerNameUnicity()
  207. if err != nil {
  208. return nil, err
  209. }
  210. }
  211. if opts.lockImageDigests {
  212. project = imagesOnly(project)
  213. }
  214. var content []byte
  215. switch opts.Format {
  216. case "json":
  217. content, err = project.MarshalJSON()
  218. case "yaml":
  219. content, err = project.MarshalYAML()
  220. default:
  221. return nil, fmt.Errorf("unsupported format %q", opts.Format)
  222. }
  223. if err != nil {
  224. return nil, err
  225. }
  226. return content, nil
  227. }
  228. // imagesOnly return project with all attributes removed but service.images
  229. func imagesOnly(project *types.Project) *types.Project {
  230. digests := types.Services{}
  231. for name, config := range project.Services {
  232. digests[name] = types.ServiceConfig{
  233. Image: config.Image,
  234. }
  235. }
  236. project = &types.Project{Services: digests}
  237. return project
  238. }
  239. func runConfigNoInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
  240. // we can't use ToProject, so the model we render here is only partially resolved
  241. model, err := opts.ToModel(ctx, dockerCli, services)
  242. if err != nil {
  243. return nil, err
  244. }
  245. if opts.resolveImageDigests {
  246. err = resolveImageDigests(ctx, dockerCli, model)
  247. if err != nil {
  248. return nil, err
  249. }
  250. }
  251. if opts.lockImageDigests {
  252. for key, e := range model {
  253. if key != "services" {
  254. delete(model, key)
  255. } else {
  256. for _, s := range e.(map[string]any) {
  257. service := s.(map[string]any)
  258. for key := range service {
  259. if key != "image" {
  260. delete(service, key)
  261. }
  262. }
  263. }
  264. }
  265. }
  266. }
  267. return formatModel(model, opts.Format)
  268. }
  269. func resolveImageDigests(ctx context.Context, dockerCli command.Cli, model map[string]any) (err error) {
  270. // create a pseudo-project so we can rely on WithImagesResolved to resolve images
  271. p := &types.Project{
  272. Services: types.Services{},
  273. }
  274. services := model["services"].(map[string]any)
  275. for name, s := range services {
  276. service := s.(map[string]any)
  277. if image, ok := service["image"]; ok {
  278. p.Services[name] = types.ServiceConfig{
  279. Image: image.(string),
  280. }
  281. }
  282. }
  283. p, err = p.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client()))
  284. if err != nil {
  285. return err
  286. }
  287. // Collect image resolved with digest and update model accordingly
  288. for name, s := range services {
  289. service := s.(map[string]any)
  290. config := p.Services[name]
  291. if config.Image != "" {
  292. service["image"] = config.Image
  293. }
  294. services[name] = service
  295. }
  296. model["services"] = services
  297. return nil
  298. }
  299. func formatModel(model map[string]any, format string) (content []byte, err error) {
  300. switch format {
  301. case "json":
  302. return json.MarshalIndent(model, "", " ")
  303. case "yaml":
  304. buf := bytes.NewBuffer([]byte{})
  305. encoder := yaml.NewEncoder(buf)
  306. encoder.SetIndent(2)
  307. err = encoder.Encode(model)
  308. return buf.Bytes(), err
  309. default:
  310. return nil, fmt.Errorf("unsupported format %q", format)
  311. }
  312. }
  313. func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
  314. if opts.noInterpolate {
  315. // we can't use ToProject, so the model we render here is only partially resolved
  316. data, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
  317. if err != nil {
  318. return err
  319. }
  320. if _, ok := data["services"]; ok {
  321. for serviceName := range data["services"].(map[string]any) {
  322. _, _ = fmt.Fprintln(dockerCli.Out(), serviceName)
  323. }
  324. }
  325. return nil
  326. }
  327. backend, err := compose.NewComposeService(dockerCli)
  328. if err != nil {
  329. return err
  330. }
  331. project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
  332. if err != nil {
  333. return err
  334. }
  335. err = project.ForEachService(project.ServiceNames(), func(serviceName string, _ *types.ServiceConfig) error {
  336. _, _ = fmt.Fprintln(dockerCli.Out(), serviceName)
  337. return nil
  338. })
  339. return err
  340. }
  341. func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
  342. backend, err := compose.NewComposeService(dockerCli)
  343. if err != nil {
  344. return err
  345. }
  346. project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
  347. if err != nil {
  348. return err
  349. }
  350. for n := range project.Volumes {
  351. _, _ = fmt.Fprintln(dockerCli.Out(), n)
  352. }
  353. return nil
  354. }
  355. func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
  356. backend, err := compose.NewComposeService(dockerCli)
  357. if err != nil {
  358. return err
  359. }
  360. project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
  361. if err != nil {
  362. return err
  363. }
  364. for n := range project.Networks {
  365. _, _ = fmt.Fprintln(dockerCli.Out(), n)
  366. }
  367. return nil
  368. }
  369. func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
  370. backend, err := compose.NewComposeService(dockerCli)
  371. if err != nil {
  372. return err
  373. }
  374. project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
  375. if err != nil {
  376. return err
  377. }
  378. for _, model := range project.Models {
  379. if model.Model != "" {
  380. _, _ = fmt.Fprintln(dockerCli.Out(), model.Model)
  381. }
  382. }
  383. return nil
  384. }
  385. func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
  386. var services []string
  387. if opts.hash != "*" {
  388. services = append(services, strings.Split(opts.hash, ",")...)
  389. }
  390. backend, err := compose.NewComposeService(dockerCli)
  391. if err != nil {
  392. return err
  393. }
  394. project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
  395. if err != nil {
  396. return err
  397. }
  398. if err := applyPlatforms(project, true); err != nil {
  399. return err
  400. }
  401. if len(services) == 0 {
  402. services = project.ServiceNames()
  403. }
  404. sorted := services
  405. slices.Sort(sorted)
  406. for _, name := range sorted {
  407. s, err := project.GetService(name)
  408. if err != nil {
  409. return err
  410. }
  411. hash, err := compose.ServiceHash(s)
  412. if err != nil {
  413. return err
  414. }
  415. _, _ = fmt.Fprintf(dockerCli.Out(), "%s %s\n", name, hash)
  416. }
  417. return nil
  418. }
  419. func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
  420. set := map[string]struct{}{}
  421. backend, err := compose.NewComposeService(dockerCli)
  422. if err != nil {
  423. return err
  424. }
  425. project, err := opts.ToProject(ctx, dockerCli, backend, services)
  426. if err != nil {
  427. return err
  428. }
  429. for _, s := range project.AllServices() {
  430. for _, p := range s.Profiles {
  431. set[p] = struct{}{}
  432. }
  433. }
  434. profiles := make([]string, 0, len(set))
  435. for p := range set {
  436. profiles = append(profiles, p)
  437. }
  438. sort.Strings(profiles)
  439. for _, p := range profiles {
  440. _, _ = fmt.Fprintln(dockerCli.Out(), p)
  441. }
  442. return nil
  443. }
  444. func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
  445. backend, err := compose.NewComposeService(dockerCli)
  446. if err != nil {
  447. return err
  448. }
  449. project, err := opts.ToProject(ctx, dockerCli, backend, services)
  450. if err != nil {
  451. return err
  452. }
  453. for _, s := range project.Services {
  454. _, _ = fmt.Fprintln(dockerCli.Out(), api.GetImageNameOrDefault(s, project.Name))
  455. }
  456. return nil
  457. }
  458. func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
  459. opts.noInterpolate = true
  460. model, err := opts.ToModel(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
  461. if err != nil {
  462. return err
  463. }
  464. variables := template.ExtractVariables(model, template.DefaultPattern)
  465. if opts.Format == "yaml" {
  466. result, err := yaml.Marshal(variables)
  467. if err != nil {
  468. return err
  469. }
  470. fmt.Print(string(result))
  471. return nil
  472. }
  473. return formatter.Print(variables, opts.Format, dockerCli.Out(), func(w io.Writer) {
  474. for name, variable := range variables {
  475. _, _ = fmt.Fprintf(w, "%s\t%t\t%s\t%s\n", name, variable.Required, variable.DefaultValue, variable.PresenceValue)
  476. }
  477. }, "NAME", "REQUIRED", "DEFAULT VALUE", "ALTERNATE VALUE")
  478. }
  479. func runEnvironment(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
  480. backend, err := compose.NewComposeService(dockerCli)
  481. if err != nil {
  482. return err
  483. }
  484. project, err := opts.ToProject(ctx, dockerCli, backend, services)
  485. if err != nil {
  486. return err
  487. }
  488. for _, v := range project.Environment.Values() {
  489. fmt.Println(v)
  490. }
  491. return nil
  492. }
  493. func escapeDollarSign(marshal []byte) []byte {
  494. dollar := []byte{'$'}
  495. escDollar := []byte{'$', '$'}
  496. return bytes.ReplaceAll(marshal, dollar, escDollar)
  497. }