compose.go 19 KB

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