compose.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  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. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "io"
  20. "os"
  21. "os/signal"
  22. "path/filepath"
  23. "strconv"
  24. "strings"
  25. "syscall"
  26. "github.com/compose-spec/compose-go/v2/cli"
  27. "github.com/compose-spec/compose-go/v2/dotenv"
  28. "github.com/compose-spec/compose-go/v2/loader"
  29. "github.com/compose-spec/compose-go/v2/types"
  30. composegoutils "github.com/compose-spec/compose-go/v2/utils"
  31. "github.com/docker/buildx/util/logutil"
  32. dockercli "github.com/docker/cli/cli"
  33. "github.com/docker/cli/cli-plugins/metadata"
  34. "github.com/docker/cli/cli/command"
  35. "github.com/docker/cli/pkg/kvfile"
  36. "github.com/docker/compose/v2/cmd/formatter"
  37. "github.com/docker/compose/v2/internal/tracing"
  38. "github.com/docker/compose/v2/pkg/api"
  39. "github.com/docker/compose/v2/pkg/compose"
  40. ui "github.com/docker/compose/v2/pkg/progress"
  41. "github.com/docker/compose/v2/pkg/remote"
  42. "github.com/docker/compose/v2/pkg/utils"
  43. "github.com/morikuni/aec"
  44. "github.com/sirupsen/logrus"
  45. "github.com/spf13/cobra"
  46. "github.com/spf13/pflag"
  47. )
  48. const (
  49. // ComposeParallelLimit set the limit running concurrent operation on docker engine
  50. ComposeParallelLimit = "COMPOSE_PARALLEL_LIMIT"
  51. // ComposeProjectName define the project name to be used, instead of guessing from parent directory
  52. ComposeProjectName = "COMPOSE_PROJECT_NAME"
  53. // ComposeCompatibility try to mimic compose v1 as much as possible
  54. ComposeCompatibility = "COMPOSE_COMPATIBILITY"
  55. // ComposeRemoveOrphans remove "orphaned" containers, i.e. containers tagged for current project but not declared as service
  56. ComposeRemoveOrphans = "COMPOSE_REMOVE_ORPHANS"
  57. // ComposeIgnoreOrphans ignore "orphaned" containers
  58. ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS"
  59. // ComposeEnvFiles defines the env files to use if --env-file isn't used
  60. ComposeEnvFiles = "COMPOSE_ENV_FILES"
  61. // ComposeMenu defines if the navigation menu should be rendered. Can be also set via --menu
  62. ComposeMenu = "COMPOSE_MENU"
  63. // ComposeProgress defines type of progress output, if --progress isn't used
  64. ComposeProgress = "COMPOSE_PROGRESS"
  65. )
  66. // rawEnv load a dot env file using docker/cli key=value parser, without attempt to interpolate or evaluate values
  67. func rawEnv(r io.Reader, filename string, vars map[string]string, lookup func(key string) (string, bool)) error {
  68. lines, err := kvfile.ParseFromReader(r, lookup)
  69. if err != nil {
  70. return fmt.Errorf("failed to parse env_file %s: %w", filename, err)
  71. }
  72. for _, line := range lines {
  73. key, value, _ := strings.Cut(line, "=")
  74. vars[key] = value
  75. }
  76. return nil
  77. }
  78. func init() {
  79. // compose evaluates env file values for interpolation
  80. // `raw` format allows to load env_file with the same parser used by docker run --env-file
  81. dotenv.RegisterFormat("raw", rawEnv)
  82. }
  83. // Command defines a compose CLI command as a func with args
  84. type Command func(context.Context, []string) error
  85. // CobraCommand defines a cobra command function
  86. type CobraCommand func(context.Context, *cobra.Command, []string) error
  87. // AdaptCmd adapt a CobraCommand func to cobra library
  88. func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error {
  89. return func(cmd *cobra.Command, args []string) error {
  90. ctx, cancel := context.WithCancel(cmd.Context())
  91. s := make(chan os.Signal, 1)
  92. signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
  93. go func() {
  94. <-s
  95. cancel()
  96. signal.Stop(s)
  97. close(s)
  98. }()
  99. err := fn(ctx, cmd, args)
  100. if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
  101. err = dockercli.StatusError{
  102. StatusCode: 130,
  103. }
  104. }
  105. if ui.Mode == ui.ModeJSON {
  106. err = makeJSONError(err)
  107. }
  108. return err
  109. }
  110. }
  111. // Adapt a Command func to cobra library
  112. func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
  113. return AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
  114. return fn(ctx, args)
  115. })
  116. }
  117. type ProjectOptions struct {
  118. ProjectName string
  119. Profiles []string
  120. ConfigPaths []string
  121. WorkDir string
  122. ProjectDir string
  123. EnvFiles []string
  124. Compatibility bool
  125. Progress string
  126. Offline bool
  127. All bool
  128. }
  129. // ProjectFunc does stuff within a types.Project
  130. type ProjectFunc func(ctx context.Context, project *types.Project) error
  131. // ProjectServicesFunc does stuff within a types.Project and a selection of services
  132. type ProjectServicesFunc func(ctx context.Context, project *types.Project, services []string) error
  133. // WithProject creates a cobra run command from a ProjectFunc based on configured project options and selected services
  134. func (o *ProjectOptions) WithProject(fn ProjectFunc, dockerCli command.Cli) func(cmd *cobra.Command, args []string) error {
  135. return o.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
  136. return fn(ctx, project)
  137. })
  138. }
  139. // WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
  140. func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
  141. return Adapt(func(ctx context.Context, args []string) error {
  142. options := []cli.ProjectOptionsFn{
  143. cli.WithResolvedPaths(true),
  144. cli.WithoutEnvironmentResolution,
  145. }
  146. project, metrics, err := o.ToProject(ctx, dockerCli, args, options...)
  147. if err != nil {
  148. return err
  149. }
  150. ctx = context.WithValue(ctx, tracing.MetricsKey{}, metrics)
  151. project, err = project.WithServicesEnvironmentResolved(true)
  152. if err != nil {
  153. return err
  154. }
  155. return fn(ctx, project, args)
  156. })
  157. }
  158. type jsonErrorData struct {
  159. Error bool `json:"error,omitempty"`
  160. Message string `json:"message,omitempty"`
  161. }
  162. func errorAsJSON(message string) string {
  163. errorMessage := &jsonErrorData{
  164. Error: true,
  165. Message: message,
  166. }
  167. marshal, err := json.Marshal(errorMessage)
  168. if err == nil {
  169. return string(marshal)
  170. } else {
  171. return message
  172. }
  173. }
  174. func makeJSONError(err error) error {
  175. if err == nil {
  176. return nil
  177. }
  178. var statusErr dockercli.StatusError
  179. if errors.As(err, &statusErr) {
  180. return dockercli.StatusError{
  181. StatusCode: statusErr.StatusCode,
  182. Status: errorAsJSON(statusErr.Status),
  183. }
  184. }
  185. return fmt.Errorf("%s", errorAsJSON(err.Error()))
  186. }
  187. func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
  188. f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable")
  189. f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
  190. f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
  191. f.StringArrayVar(&o.EnvFiles, "env-file", defaultStringArrayVar(ComposeEnvFiles), "Specify an alternate environment file")
  192. f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
  193. f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
  194. f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode")
  195. f.StringVar(&o.Progress, "progress", os.Getenv(ComposeProgress), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
  196. f.BoolVar(&o.All, "all-resources", false, "Include all resources, even those not used by services")
  197. _ = f.MarkHidden("workdir")
  198. }
  199. // get default value for a command line flag that is set by a coma-separated value in environment variable
  200. func defaultStringArrayVar(env string) []string {
  201. return strings.FieldsFunc(os.Getenv(env), func(c rune) bool {
  202. return c == ','
  203. })
  204. }
  205. func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cli, services ...string) (*types.Project, string, error) {
  206. name := o.ProjectName
  207. var project *types.Project
  208. if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
  209. p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
  210. if err != nil {
  211. envProjectName := os.Getenv(ComposeProjectName)
  212. if envProjectName != "" {
  213. return nil, envProjectName, nil
  214. }
  215. return nil, "", err
  216. }
  217. project = p
  218. name = p.Name
  219. }
  220. return project, name, nil
  221. }
  222. func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cli) (string, error) {
  223. if o.ProjectName != "" {
  224. return o.ProjectName, nil
  225. }
  226. envProjectName := os.Getenv(ComposeProjectName)
  227. if envProjectName != "" {
  228. return envProjectName, nil
  229. }
  230. project, _, err := o.ToProject(ctx, dockerCli, nil)
  231. if err != nil {
  232. return "", err
  233. }
  234. return project.Name, nil
  235. }
  236. func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
  237. remotes := o.remoteLoaders(dockerCli)
  238. for _, r := range remotes {
  239. po = append(po, cli.WithResourceLoader(r))
  240. }
  241. options, err := o.toProjectOptions(po...)
  242. if err != nil {
  243. return nil, err
  244. }
  245. if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
  246. api.Separator = "_"
  247. }
  248. return options.LoadModel(ctx)
  249. }
  250. func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { //nolint:gocyclo
  251. var metrics tracing.Metrics
  252. remotes := o.remoteLoaders(dockerCli)
  253. for _, r := range remotes {
  254. po = append(po, cli.WithResourceLoader(r))
  255. }
  256. options, err := o.toProjectOptions(po...)
  257. if err != nil {
  258. return nil, metrics, err
  259. }
  260. options.WithListeners(func(event string, metadata map[string]any) {
  261. switch event {
  262. case "extends":
  263. metrics.CountExtends++
  264. case "include":
  265. paths := metadata["path"].(types.StringList)
  266. for _, path := range paths {
  267. var isRemote bool
  268. for _, r := range remotes {
  269. if r.Accept(path) {
  270. isRemote = true
  271. break
  272. }
  273. }
  274. if isRemote {
  275. metrics.CountIncludesRemote++
  276. } else {
  277. metrics.CountIncludesLocal++
  278. }
  279. }
  280. }
  281. })
  282. if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
  283. api.Separator = "_"
  284. }
  285. project, err := options.LoadProject(ctx)
  286. if err != nil {
  287. return nil, metrics, err
  288. }
  289. if project.Name == "" {
  290. return nil, metrics, errors.New("project name can't be empty. Use `--project-name` to set a valid name")
  291. }
  292. project, err = project.WithServicesEnabled(services...)
  293. if err != nil {
  294. return nil, metrics, err
  295. }
  296. for name, s := range project.Services {
  297. s.CustomLabels = map[string]string{
  298. api.ProjectLabel: project.Name,
  299. api.ServiceLabel: name,
  300. api.VersionLabel: api.ComposeVersion,
  301. api.WorkingDirLabel: project.WorkingDir,
  302. api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
  303. api.OneoffLabel: "False", // default, will be overridden by `run` command
  304. }
  305. if len(o.EnvFiles) != 0 {
  306. s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(o.EnvFiles, ",")
  307. }
  308. project.Services[name] = s
  309. }
  310. project, err = project.WithSelectedServices(services)
  311. if err != nil {
  312. return nil, tracing.Metrics{}, err
  313. }
  314. if !o.All {
  315. project = project.WithoutUnnecessaryResources()
  316. }
  317. return project, metrics, err
  318. }
  319. func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader {
  320. if o.Offline {
  321. return nil
  322. }
  323. git := remote.NewGitRemoteLoader(dockerCli, o.Offline)
  324. oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline)
  325. return []loader.ResourceLoader{git, oci}
  326. }
  327. func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) {
  328. opts := []cli.ProjectOptionsFn{
  329. cli.WithWorkingDirectory(o.ProjectDir),
  330. // First apply os.Environment, always win
  331. cli.WithOsEnv,
  332. }
  333. if _, present := os.LookupEnv("PWD"); !present {
  334. if pwd, err := os.Getwd(); err != nil {
  335. return nil, err
  336. } else {
  337. opts = append(opts, cli.WithEnv([]string{"PWD=" + pwd}))
  338. }
  339. }
  340. opts = append(opts,
  341. // Load PWD/.env if present and no explicit --env-file has been set
  342. cli.WithEnvFiles(o.EnvFiles...),
  343. // read dot env file to populate project environment
  344. cli.WithDotEnv,
  345. // get compose file path set by COMPOSE_FILE
  346. cli.WithConfigFileEnv,
  347. // if none was selected, get default compose.yaml file from current dir or parent folder
  348. cli.WithDefaultConfigPath,
  349. // .. and then, a project directory != PWD maybe has been set so let's load .env file
  350. cli.WithEnvFiles(o.EnvFiles...),
  351. cli.WithDotEnv,
  352. // eventually COMPOSE_PROFILES should have been set
  353. cli.WithDefaultProfiles(o.Profiles...),
  354. cli.WithName(o.ProjectName),
  355. )
  356. return cli.NewProjectOptions(o.ConfigPaths, append(po, opts...)...)
  357. }
  358. // PluginName is the name of the plugin
  359. const PluginName = "compose"
  360. // RunningAsStandalone detects when running as a standalone program
  361. func RunningAsStandalone() bool {
  362. return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName && os.Args[1] != PluginName
  363. }
  364. type BackendOptions struct {
  365. Options []compose.Option
  366. }
  367. func (o *BackendOptions) Add(option compose.Option) {
  368. o.Options = append(o.Options, option)
  369. }
  370. // RootCommand returns the compose command with its child commands
  371. func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command { //nolint:gocyclo
  372. // filter out useless commandConn.CloseWrite warning message that can occur
  373. // when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
  374. // https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
  375. logrus.AddHook(logutil.NewFilter([]logrus.Level{
  376. logrus.WarnLevel,
  377. },
  378. "commandConn.CloseWrite:",
  379. "commandConn.CloseRead:",
  380. ))
  381. opts := ProjectOptions{}
  382. var (
  383. ansi string
  384. noAnsi bool
  385. verbose bool
  386. version bool
  387. parallel int
  388. dryRun bool
  389. )
  390. c := &cobra.Command{
  391. Short: "Docker Compose",
  392. Long: "Define and run multi-container applications with Docker",
  393. Use: PluginName,
  394. TraverseChildren: true,
  395. // By default (no Run/RunE in parent c) for typos in subcommands, cobra displays the help of parent c but exit(0) !
  396. RunE: func(cmd *cobra.Command, args []string) error {
  397. if len(args) == 0 {
  398. return cmd.Help()
  399. }
  400. if version {
  401. return versionCommand(dockerCli).Execute()
  402. }
  403. _ = cmd.Help()
  404. return dockercli.StatusError{
  405. StatusCode: 1,
  406. Status: fmt.Sprintf("unknown docker command: %q", "compose "+args[0]),
  407. }
  408. },
  409. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  410. parent := cmd.Root()
  411. if parent != nil {
  412. parentPrerun := parent.PersistentPreRunE
  413. if parentPrerun != nil {
  414. err := parentPrerun(cmd, args)
  415. if err != nil {
  416. return err
  417. }
  418. }
  419. }
  420. if verbose {
  421. logrus.SetLevel(logrus.TraceLevel)
  422. }
  423. err := setEnvWithDotEnv(opts)
  424. if err != nil {
  425. return err
  426. }
  427. if noAnsi {
  428. if ansi != "auto" {
  429. return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
  430. }
  431. ansi = "never"
  432. fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n")
  433. }
  434. if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") {
  435. ansi = v
  436. }
  437. formatter.SetANSIMode(dockerCli, ansi)
  438. if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
  439. ui.NoColor()
  440. formatter.SetANSIMode(dockerCli, formatter.Never)
  441. }
  442. switch ansi {
  443. case "never":
  444. ui.Mode = ui.ModePlain
  445. case "always":
  446. ui.Mode = ui.ModeTTY
  447. }
  448. var ep ui.EventProcessor
  449. switch opts.Progress {
  450. case "", ui.ModeAuto:
  451. switch {
  452. case ansi == "never":
  453. ui.Mode = ui.ModePlain
  454. ep = ui.NewPlainWriter(dockerCli.Err())
  455. case dockerCli.Out().IsTerminal():
  456. ep = ui.NewTTYWriter(dockerCli.Err())
  457. default:
  458. ep = ui.NewPlainWriter(dockerCli.Err())
  459. }
  460. case ui.ModeTTY:
  461. if ansi == "never" {
  462. return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
  463. }
  464. ui.Mode = ui.ModeTTY
  465. ep = ui.NewTTYWriter(dockerCli.Err())
  466. case ui.ModePlain:
  467. if ansi == "always" {
  468. return fmt.Errorf("can't use --progress plain while ANSI support is forced")
  469. }
  470. ui.Mode = ui.ModePlain
  471. ep = ui.NewPlainWriter(dockerCli.Err())
  472. case ui.ModeQuiet, "none":
  473. ui.Mode = ui.ModeQuiet
  474. ep = ui.NewQuiedWriter()
  475. case ui.ModeJSON:
  476. ui.Mode = ui.ModeJSON
  477. logrus.SetFormatter(&logrus.JSONFormatter{})
  478. ep = ui.NewJSONWriter(dockerCli.Err())
  479. default:
  480. return fmt.Errorf("unsupported --progress value %q", opts.Progress)
  481. }
  482. backendOptions.Add(compose.WithEventProcessor(ep))
  483. // (4) options validation / normalization
  484. if opts.WorkDir != "" {
  485. if opts.ProjectDir != "" {
  486. return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
  487. }
  488. opts.ProjectDir = opts.WorkDir
  489. fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF))
  490. }
  491. for i, file := range opts.EnvFiles {
  492. if !filepath.IsAbs(file) {
  493. file, err := filepath.Abs(file)
  494. if err != nil {
  495. return err
  496. }
  497. opts.EnvFiles[i] = file
  498. }
  499. }
  500. composeCmd := cmd
  501. for composeCmd.Name() != PluginName {
  502. if !composeCmd.HasParent() {
  503. return fmt.Errorf("error parsing command line, expected %q", PluginName)
  504. }
  505. composeCmd = composeCmd.Parent()
  506. }
  507. if v, ok := os.LookupEnv(ComposeParallelLimit); ok && !composeCmd.Flags().Changed("parallel") {
  508. i, err := strconv.Atoi(v)
  509. if err != nil {
  510. return fmt.Errorf("%s must be an integer (found: %q)", ComposeParallelLimit, v)
  511. }
  512. parallel = i
  513. }
  514. if parallel > 0 {
  515. logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
  516. backendOptions.Add(compose.WithMaxConcurrency(parallel))
  517. }
  518. // dry run detection
  519. if dryRun {
  520. backendOptions.Add(compose.WithDryRun)
  521. }
  522. return nil
  523. },
  524. }
  525. c.AddCommand(
  526. upCommand(&opts, dockerCli, backendOptions),
  527. downCommand(&opts, dockerCli, backendOptions),
  528. startCommand(&opts, dockerCli, backendOptions),
  529. restartCommand(&opts, dockerCli, backendOptions),
  530. stopCommand(&opts, dockerCli, backendOptions),
  531. psCommand(&opts, dockerCli, backendOptions),
  532. listCommand(dockerCli, backendOptions),
  533. logsCommand(&opts, dockerCli, backendOptions),
  534. configCommand(&opts, dockerCli),
  535. killCommand(&opts, dockerCli, backendOptions),
  536. runCommand(&opts, dockerCli, backendOptions),
  537. removeCommand(&opts, dockerCli, backendOptions),
  538. execCommand(&opts, dockerCli, backendOptions),
  539. attachCommand(&opts, dockerCli, backendOptions),
  540. exportCommand(&opts, dockerCli, backendOptions),
  541. commitCommand(&opts, dockerCli, backendOptions),
  542. pauseCommand(&opts, dockerCli, backendOptions),
  543. unpauseCommand(&opts, dockerCli, backendOptions),
  544. topCommand(&opts, dockerCli, backendOptions),
  545. eventsCommand(&opts, dockerCli, backendOptions),
  546. portCommand(&opts, dockerCli, backendOptions),
  547. imagesCommand(&opts, dockerCli, backendOptions),
  548. versionCommand(dockerCli),
  549. buildCommand(&opts, dockerCli, backendOptions),
  550. pushCommand(&opts, dockerCli, backendOptions),
  551. pullCommand(&opts, dockerCli, backendOptions),
  552. createCommand(&opts, dockerCli, backendOptions),
  553. copyCommand(&opts, dockerCli, backendOptions),
  554. waitCommand(&opts, dockerCli, backendOptions),
  555. scaleCommand(&opts, dockerCli, backendOptions),
  556. statsCommand(&opts, dockerCli),
  557. watchCommand(&opts, dockerCli, backendOptions),
  558. publishCommand(&opts, dockerCli, backendOptions),
  559. alphaCommand(&opts, dockerCli, backendOptions),
  560. bridgeCommand(&opts, dockerCli),
  561. volumesCommand(&opts, dockerCli, backendOptions),
  562. )
  563. c.Flags().SetInterspersed(false)
  564. opts.addProjectFlags(c.Flags())
  565. c.RegisterFlagCompletionFunc( //nolint:errcheck
  566. "project-name",
  567. completeProjectNames(dockerCli, backendOptions),
  568. )
  569. c.RegisterFlagCompletionFunc( //nolint:errcheck
  570. "project-directory",
  571. func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  572. return []string{}, cobra.ShellCompDirectiveFilterDirs
  573. },
  574. )
  575. c.RegisterFlagCompletionFunc( //nolint:errcheck
  576. "file",
  577. func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  578. return []string{"yaml", "yml"}, cobra.ShellCompDirectiveFilterFileExt
  579. },
  580. )
  581. c.RegisterFlagCompletionFunc( //nolint:errcheck
  582. "profile",
  583. completeProfileNames(dockerCli, &opts),
  584. )
  585. c.RegisterFlagCompletionFunc( //nolint:errcheck
  586. "progress",
  587. cobra.FixedCompletions(printerModes, cobra.ShellCompDirectiveNoFileComp),
  588. )
  589. c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
  590. c.Flags().IntVar(&parallel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
  591. c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
  592. c.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Execute command in dry run mode")
  593. c.Flags().MarkHidden("version") //nolint:errcheck
  594. c.Flags().BoolVar(&noAnsi, "no-ansi", false, `Do not print ANSI control characters (DEPRECATED)`)
  595. c.Flags().MarkHidden("no-ansi") //nolint:errcheck
  596. c.Flags().BoolVar(&verbose, "verbose", false, "Show more output")
  597. c.Flags().MarkHidden("verbose") //nolint:errcheck
  598. return c
  599. }
  600. func setEnvWithDotEnv(opts ProjectOptions) error {
  601. options, err := cli.NewProjectOptions(opts.ConfigPaths,
  602. cli.WithWorkingDirectory(opts.ProjectDir),
  603. cli.WithOsEnv,
  604. cli.WithEnvFiles(opts.EnvFiles...),
  605. cli.WithDotEnv,
  606. )
  607. if err != nil {
  608. return nil
  609. }
  610. envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), options.EnvFiles)
  611. if err != nil {
  612. return nil
  613. }
  614. for k, v := range envFromFile {
  615. if _, ok := os.LookupEnv(k); !ok && strings.HasPrefix(k, "COMPOSE_") {
  616. if err = os.Setenv(k, v); err != nil {
  617. return nil
  618. }
  619. }
  620. }
  621. return err
  622. }
  623. var printerModes = []string{
  624. ui.ModeAuto,
  625. ui.ModeTTY,
  626. ui.ModePlain,
  627. ui.ModeJSON,
  628. ui.ModeQuiet,
  629. }