compose.go 22 KB

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