From f2ab7099db6d8386299796e6eef8e30f65b21bcc Mon Sep 17 00:00:00 2001
From: Evan Van Dam <evandam92@gmail.com>
Date: Wed, 6 Sep 2023 19:19:24 -0700
Subject: [PATCH] cmd: Prevent overwriting existing env vars with `--envfile`
 (#5803)

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
---
 cmd/commandfuncs.go | 42 +++++++++++++++++++++++++++++++-----------
 cmd/commands.go     | 40 ++++++++++++++++++++--------------------
 cmd/main.go         |  8 ++++++--
 3 files changed, 57 insertions(+), 33 deletions(-)

diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
index f284b89b9..896398861 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -46,7 +46,14 @@ func cmdStart(fl Flags) (int, error) {
 	startCmdConfigAdapterFlag := fl.String("adapter")
 	startCmdPidfileFlag := fl.String("pidfile")
 	startCmdWatchFlag := fl.Bool("watch")
-	startCmdEnvfileFlag := fl.String("envfile")
+
+	var err error
+	var startCmdEnvfileFlag []string
+	startCmdEnvfileFlag, err = fl.GetStringSlice("envfile")
+	if err != nil {
+		return caddy.ExitCodeFailedStartup,
+			fmt.Errorf("reading envfile flag: %v", err)
+	}
 
 	// open a listener to which the child process will connect when
 	// it is ready to confirm that it has successfully started
@@ -70,8 +77,9 @@ func cmdStart(fl Flags) (int, error) {
 	if startCmdConfigFlag != "" {
 		cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
 	}
-	if startCmdEnvfileFlag != "" {
-		cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag)
+
+	for _, envFile := range startCmdEnvfileFlag {
+		cmd.Args = append(cmd.Args, "--envfile", envFile)
 	}
 	if startCmdConfigAdapterFlag != "" {
 		cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
@@ -160,15 +168,22 @@ func cmdRun(fl Flags) (int, error) {
 	runCmdConfigFlag := fl.String("config")
 	runCmdConfigAdapterFlag := fl.String("adapter")
 	runCmdResumeFlag := fl.Bool("resume")
-	runCmdLoadEnvfileFlag := fl.String("envfile")
 	runCmdPrintEnvFlag := fl.Bool("environ")
 	runCmdWatchFlag := fl.Bool("watch")
 	runCmdPidfileFlag := fl.String("pidfile")
 	runCmdPingbackFlag := fl.String("pingback")
 
+	var err error
+	var runCmdLoadEnvfileFlag []string
+	runCmdLoadEnvfileFlag, err = fl.GetStringSlice("envfile")
+	if err != nil {
+		return caddy.ExitCodeFailedStartup,
+			fmt.Errorf("reading envfile flag: %v", err)
+	}
+
 	// load all additional envs as soon as possible
-	if runCmdLoadEnvfileFlag != "" {
-		if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
+	for _, envFile := range runCmdLoadEnvfileFlag {
+		if err := loadEnvFromFile(envFile); err != nil {
 			return caddy.ExitCodeFailedStartup,
 				fmt.Errorf("loading additional environment variables: %v", err)
 		}
@@ -181,7 +196,6 @@ func cmdRun(fl Flags) (int, error) {
 
 	// load the config, depending on flags
 	var config []byte
-	var err error
 	if runCmdResumeFlag {
 		config, err = os.ReadFile(caddy.ConfigAutosavePath)
 		if os.IsNotExist(err) {
@@ -497,18 +511,24 @@ func cmdAdaptConfig(fl Flags) (int, error) {
 func cmdValidateConfig(fl Flags) (int, error) {
 	validateCmdConfigFlag := fl.String("config")
 	validateCmdAdapterFlag := fl.String("adapter")
-	runCmdLoadEnvfileFlag := fl.String("envfile")
+
+	var err error
+	var runCmdLoadEnvfileFlag []string
+	runCmdLoadEnvfileFlag, err = fl.GetStringSlice("envfile")
+	if err != nil {
+		return caddy.ExitCodeFailedStartup,
+			fmt.Errorf("reading envfile flag: %v", err)
+	}
 
 	// load all additional envs as soon as possible
-	if runCmdLoadEnvfileFlag != "" {
-		if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
+	for _, envFile := range runCmdLoadEnvfileFlag {
+		if err := loadEnvFromFile(envFile); err != nil {
 			return caddy.ExitCodeFailedStartup,
 				fmt.Errorf("loading additional environment variables: %v", err)
 		}
 	}
 
 	// use default config and ensure a config file is specified
-	var err error
 	validateCmdConfigFlag, err = configFileWithRespectToDefault(caddy.Log(), validateCmdConfigFlag)
 	if err != nil {
 		return caddy.ExitCodeFailedStartup, err
diff --git a/cmd/commands.go b/cmd/commands.go
index 0885577ea..c64ab7134 100644
--- a/cmd/commands.go
+++ b/cmd/commands.go
@@ -104,7 +104,7 @@ using 'caddy run' instead to keep it in the foreground.
 		CobraFunc: func(cmd *cobra.Command) {
 			cmd.Flags().StringP("config", "c", "", "Configuration file")
 			cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
-			cmd.Flags().StringP("envfile", "", "", "Environment file to load")
+			cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
 			cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically")
 			cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
 			cmd.RunE = WrapCommandFuncForCobra(cmdStart)
@@ -150,7 +150,7 @@ option in a local development environment.
 		CobraFunc: func(cmd *cobra.Command) {
 			cmd.Flags().StringP("config", "c", "", "Configuration file")
 			cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
-			cmd.Flags().StringP("envfile", "", "", "Environment file to load")
+			cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
 			cmd.Flags().BoolP("environ", "e", false, "Print environment")
 			cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)")
 			cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically")
@@ -301,7 +301,7 @@ the KEY=VALUE format will be loaded into the Caddy process.
 		CobraFunc: func(cmd *cobra.Command) {
 			cmd.Flags().StringP("config", "c", "", "Input configuration file")
 			cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
-			cmd.Flags().StringP("envfile", "", "", "Environment file to load")
+			cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
 			cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
 		},
 	})
@@ -402,7 +402,7 @@ latest versions. EXPERIMENTAL: May be changed or removed.
 		Short: "Adds Caddy packages (EXPERIMENTAL)",
 		Long: `
 Downloads an updated Caddy binary with the specified packages (module/plugin)
-added. Retains existing packages. Returns an error if the any of packages are 
+added. Retains existing packages. Returns an error if the any of packages are
 already included. EXPERIMENTAL: May be changed or removed.
 `,
 		CobraFunc: func(cmd *cobra.Command) {
@@ -417,8 +417,8 @@ already included. EXPERIMENTAL: May be changed or removed.
 		Usage: "<packages...>",
 		Short: "Removes Caddy packages (EXPERIMENTAL)",
 		Long: `
-Downloads an updated Caddy binaries without the specified packages (module/plugin). 
-Returns an error if any of the packages are not included. 
+Downloads an updated Caddy binaries without the specified packages (module/plugin).
+Returns an error if any of the packages are not included.
 EXPERIMENTAL: May be changed or removed.
 `,
 		CobraFunc: func(cmd *cobra.Command) {
@@ -464,40 +464,40 @@ argument of --directory. If the directory does not exist, it will be created.
 		Use:   "completion [bash|zsh|fish|powershell]",
 		Short: "Generate completion script",
 		Long: fmt.Sprintf(`To load completions:
-	
+
 	Bash:
-	
+
 	  $ source <(%[1]s completion bash)
-	
+
 	  # To load completions for each session, execute once:
 	  # Linux:
 	  $ %[1]s completion bash > /etc/bash_completion.d/%[1]s
 	  # macOS:
 	  $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
-	
+
 	Zsh:
-	
+
 	  # If shell completion is not already enabled in your environment,
 	  # you will need to enable it.  You can execute the following once:
-	
+
 	  $ echo "autoload -U compinit; compinit" >> ~/.zshrc
-	
+
 	  # To load completions for each session, execute once:
 	  $ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
-	
+
 	  # You will need to start a new shell for this setup to take effect.
-	
+
 	fish:
-	
+
 	  $ %[1]s completion fish | source
-	
+
 	  # To load completions for each session, execute once:
 	  $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
-	
+
 	PowerShell:
-	
+
 	  PS> %[1]s completion powershell | Out-String | Invoke-Expression
-	
+
 	  # To load completions for every new session, run:
 	  PS> %[1]s completion powershell > %[1]s.ps1
 	  # and source this file from your PowerShell profile.
diff --git a/cmd/main.go b/cmd/main.go
index 1d6478a47..b4e3fdc87 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -300,8 +300,12 @@ func loadEnvFromFile(envFile string) error {
 	}
 
 	for k, v := range envMap {
-		if err := os.Setenv(k, v); err != nil {
-			return fmt.Errorf("setting environment variables: %v", err)
+		// do not overwrite existing environment variables
+		_, exists := os.LookupEnv(k)
+		if !exists {
+			if err := os.Setenv(k, v); err != nil {
+				return fmt.Errorf("setting environment variables: %v", err)
+			}
 		}
 	}