From c663ddc94b32d3786758931af33c033b954a3b42 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Tue, 8 Aug 2023 17:37:32 -0700 Subject: [PATCH] [env] Add --env and --env-file flags (#1353) ## Summary Adds `--env` and `--env-file` flags that can be used with `run` `shell` and `services` If both flags are specified they are merged with `--env` taking precedence. environment variables passed in with flags take the highest level of precedence, replacing any env variables set by nix, devbox.json, or plugins. TODO: The flag code is a bit brittle and repetitive, I think we can consolidate the common flag registration. ## How was it tested? ``` devbox run -e FOO=BAR echo \$FOO devbox services up -e HTTPD_PORT=8083 ``` --- examples/servers/apache/devbox.lock | 3 +- go.mod | 2 ++ go.sum | 2 ++ internal/boxcli/env.go | 48 +++++++++++++++++++++++++++++ internal/boxcli/run.go | 7 +++++ internal/boxcli/services.go | 22 +++++++++++++ internal/boxcli/shell.go | 7 +++++ internal/boxcli/shellenv.go | 7 +++++ internal/impl/devbox.go | 6 ++++ internal/impl/devopt/devboxopts.go | 1 + 10 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 internal/boxcli/env.go diff --git a/examples/servers/apache/devbox.lock b/examples/servers/apache/devbox.lock index cad26c5f056..bd674c899ba 100644 --- a/examples/servers/apache/devbox.lock +++ b/examples/servers/apache/devbox.lock @@ -3,8 +3,9 @@ "packages": { "apacheHttpd@latest": { "last_modified": "2023-05-01T16:53:22Z", + "plugin_version": "0.0.2", "resolved": "github:NixOS/nixpkgs/8670e496ffd093b60e74e7fa53526aa5920d09eb#apacheHttpd", "version": "2.4.57" } } -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 4ab1aab3b2b..fb61d684a76 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require github.com/joho/godotenv v1.5.1 + require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect diff --git a/go.sum b/go.sum index 16e4a557e1b..cd64a2a7b32 100644 --- a/go.sum +++ b/go.sum @@ -129,6 +129,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= diff --git a/internal/boxcli/env.go b/internal/boxcli/env.go new file mode 100644 index 00000000000..f43896cc4f7 --- /dev/null +++ b/internal/boxcli/env.go @@ -0,0 +1,48 @@ +// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved. +// Use of this source code is governed by the license in the LICENSE file. + +package boxcli + +import ( + "path/filepath" + + "github.com/joho/godotenv" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// to be composed into xyzCmdFlags structs +type envFlag struct { + env map[string]string + envFile string +} + +func (f *envFlag) register(cmd *cobra.Command) { + cmd.PersistentFlags().StringToStringVarP( + &f.env, "env", "e", nil, "environment variables to set in the devbox environment", + ) + cmd.PersistentFlags().StringVar( + &f.envFile, "env-file", "", "path to a file containing environment variables to set in the devbox environment", + ) +} + +func (f *envFlag) Env(path string) (map[string]string, error) { + envs := map[string]string{} + var err error + if f.envFile != "" { + envPath := f.envFile + if !filepath.IsAbs(envPath) { + envPath = filepath.Join(path, envPath) + } + envs, err = godotenv.Read(envPath) + if err != nil { + return nil, errors.WithStack(err) + } + } + + for k, v := range f.env { + envs[k] = v + } + + return envs, nil +} diff --git a/internal/boxcli/run.go b/internal/boxcli/run.go index 14d7480c284..ad74836a761 100644 --- a/internal/boxcli/run.go +++ b/internal/boxcli/run.go @@ -20,6 +20,7 @@ import ( ) type runCmdFlags struct { + envFlag config configFlags pure bool listScripts bool @@ -43,6 +44,7 @@ func runCmd() *cobra.Command { }, } + flags.envFlag.register(command) flags.config.register(command) command.Flags().BoolVar( &flags.pure, "pure", false, "If this flag is specified, devbox runs the script in an isolated environment inheriting almost no variables from the current environment. A few variables, in particular HOME, USER and DISPLAY, are retained.") @@ -90,11 +92,16 @@ func runScriptCmd(cmd *cobra.Command, args []string, flags runCmdFlags) error { debug.Log("script: %s", script) debug.Log("script args: %v", scriptArgs) + env, err := flags.Env(path) + if err != nil { + return err + } // Check the directory exists. box, err := devbox.Open(&devopt.Opts{ Dir: path, Writer: cmd.ErrOrStderr(), Pure: flags.pure, + Env: env, }) if err != nil { return redact.Errorf("error reading devbox.json: %w", err) diff --git a/internal/boxcli/services.go b/internal/boxcli/services.go index e0b33b2f897..22e15a24862 100644 --- a/internal/boxcli/services.go +++ b/internal/boxcli/services.go @@ -11,6 +11,7 @@ import ( ) type servicesCmdFlags struct { + envFlag config configFlags } @@ -92,6 +93,7 @@ func servicesCmd() *cobra.Command { }, } + flags.envFlag.register(servicesCommand) flags.config.registerPersistent(servicesCommand) serviceUpFlags.register(upCommand) serviceStopFlags.register(stopCommand) @@ -116,8 +118,13 @@ func listServices(cmd *cobra.Command, flags servicesCmdFlags) error { } func startServices(cmd *cobra.Command, services []string, flags servicesCmdFlags) error { + env, err := flags.Env(flags.config.path) + if err != nil { + return err + } box, err := devbox.Open(&devopt.Opts{ Dir: flags.config.path, + Env: env, Writer: cmd.ErrOrStderr(), }) if err != nil { @@ -133,8 +140,13 @@ func stopServices( servicesFlags servicesCmdFlags, flags serviceStopFlags, ) error { + env, err := servicesFlags.Env(servicesFlags.config.path) + if err != nil { + return err + } box, err := devbox.Open(&devopt.Opts{ Dir: servicesFlags.config.path, + Env: env, Writer: cmd.ErrOrStderr(), }) if err != nil { @@ -151,8 +163,13 @@ func restartServices( services []string, flags servicesCmdFlags, ) error { + env, err := flags.Env(flags.config.path) + if err != nil { + return err + } box, err := devbox.Open(&devopt.Opts{ Dir: flags.config.path, + Env: env, Writer: cmd.ErrOrStderr(), }) if err != nil { @@ -168,8 +185,13 @@ func startProcessManager( servicesFlags servicesCmdFlags, flags serviceUpFlags, ) error { + env, err := servicesFlags.Env(servicesFlags.config.path) + if err != nil { + return err + } box, err := devbox.Open(&devopt.Opts{ Dir: servicesFlags.config.path, + Env: env, CustomProcessComposeFile: flags.processComposeFile, Writer: cmd.ErrOrStderr(), }) diff --git a/internal/boxcli/shell.go b/internal/boxcli/shell.go index c830ab99023..6406d9bd41a 100644 --- a/internal/boxcli/shell.go +++ b/internal/boxcli/shell.go @@ -16,6 +16,7 @@ import ( ) type shellCmdFlags struct { + envFlag config configFlags printEnv bool pure bool @@ -42,13 +43,19 @@ func shellCmd() *cobra.Command { &flags.pure, "pure", false, "If this flag is specified, devbox creates an isolated shell inheriting almost no variables from the current environment. A few variables, in particular HOME, USER and DISPLAY, are retained.") flags.config.register(command) + flags.envFlag.register(command) return command } func runShellCmd(cmd *cobra.Command, flags shellCmdFlags) error { + env, err := flags.Env(flags.config.path) + if err != nil { + return err + } // Check the directory exists. box, err := devbox.Open(&devopt.Opts{ Dir: flags.config.path, + Env: env, Pure: flags.pure, Writer: cmd.ErrOrStderr(), }) diff --git a/internal/boxcli/shellenv.go b/internal/boxcli/shellenv.go index 752bc27bd6b..5baad06c35e 100644 --- a/internal/boxcli/shellenv.go +++ b/internal/boxcli/shellenv.go @@ -12,6 +12,7 @@ import ( ) type shellEnvCmdFlags struct { + envFlag config configFlags runInitHook bool install bool @@ -45,6 +46,7 @@ func shellEnvCmd() *cobra.Command { &flags.pure, "pure", false, "If this flag is specified, devbox creates an isolated environment inheriting almost no variables from the current environment. A few variables, in particular HOME, USER and DISPLAY, are retained.") flags.config.register(command) + flags.envFlag.register(command) command.AddCommand(shellEnvOnlyPathWithoutWrappersCmd()) @@ -52,10 +54,15 @@ func shellEnvCmd() *cobra.Command { } func shellEnvFunc(cmd *cobra.Command, flags shellEnvCmdFlags) (string, error) { + env, err := flags.Env(flags.config.path) + if err != nil { + return "", err + } box, err := devbox.Open(&devopt.Opts{ Dir: flags.config.path, Writer: cmd.ErrOrStderr(), Pure: flags.pure, + Env: env, }) if err != nil { return "", err diff --git a/internal/impl/devbox.go b/internal/impl/devbox.go index 2b4ecc5fa58..3e8f16a9226 100644 --- a/internal/impl/devbox.go +++ b/internal/impl/devbox.go @@ -53,6 +53,7 @@ const ( type Devbox struct { cfg *devconfig.Config + env map[string]string lockfile *lock.File nix nix.Nixer projectDir string @@ -84,6 +85,7 @@ func Open(opts *devopt.Opts) (*Devbox, error) { box := &Devbox{ cfg: cfg, + env: opts.Env, nix: &nix.Nix{}, projectDir: projectDir, pluginManager: plugin.NewManager(), @@ -881,6 +883,10 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m env["XDG_DATA_DIRS"] = JoinPathLists(env["XDG_DATA_DIRS"], os.Getenv("XDG_DATA_DIRS")) } + for k, v := range d.env { + env[k] = v + } + return env, d.addHashToEnv(env) } diff --git a/internal/impl/devopt/devboxopts.go b/internal/impl/devopt/devboxopts.go index bde6ad36e9f..ede56cfcd36 100644 --- a/internal/impl/devopt/devboxopts.go +++ b/internal/impl/devopt/devboxopts.go @@ -7,6 +7,7 @@ import ( type Opts struct { AllowInsecureAdds bool Dir string + Env map[string]string Pure bool IgnoreWarnings bool CustomProcessComposeFile string