diff --git a/internal/devbox/devbox.go b/internal/devbox/devbox.go index d15dd0f5ba3..c7fe1c78723 100644 --- a/internal/devbox/devbox.go +++ b/internal/devbox/devbox.go @@ -954,6 +954,21 @@ func (d *Devbox) configEnvs( } } } + } else if d.cfg.Root.IsdotEnvEnabled() { + // if env_from points to a .env file, parse and add it + parsedEnvs, err := d.cfg.Root.ParseEnvsFromDotEnv() + if err != nil { + // it's fine to include the error ParseEnvsFromDotEnv here because + // the error message is relevant to the user + return nil, usererr.New( + "failed parsing %s file. Error: %v", + d.cfg.Root.EnvFrom, + err, + ) + } + for k, v := range parsedEnvs { + env[k] = v + } } else if d.cfg.Root.EnvFrom != "" { return nil, usererr.New( "unknown from_env value: %s. Supported value is: %q.", diff --git a/internal/devconfig/configfile/env.go b/internal/devconfig/configfile/env.go index fc62d1e1284..82252922c8b 100644 --- a/internal/devconfig/configfile/env.go +++ b/internal/devconfig/configfile/env.go @@ -1,6 +1,63 @@ package configfile +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" +) + func (c *ConfigFile) IsEnvsecEnabled() bool { - // envsec for legacy. - return c.EnvFrom == "envsec" || c.EnvFrom == "jetpack-cloud" + // envsec for legacy. jetpack-cloud for legacy + return c.EnvFrom == "envsec" || c.EnvFrom == "jetpack-cloud" || c.EnvFrom == "jetify-cloud" +} + +func (c *ConfigFile) IsdotEnvEnabled() bool { + // filename has to end with .env + return filepath.Ext(c.EnvFrom) == ".env" +} + +func (c *ConfigFile) ParseEnvsFromDotEnv() (map[string]string, error) { + // This check should never happen because we call IsdotEnvEnabled + // before calling this method. But having it makes it more robust + // in case if anyone uses this method without the IsdotEnvEnabled + if !c.IsdotEnvEnabled() { + return nil, fmt.Errorf("env file does not have a .env extension") + } + + file, err := os.Open(c.EnvFrom) + if err != nil { + return nil, fmt.Errorf("failed to open file: %s", c.EnvFrom) + } + defer file.Close() + + envMap := map[string]string{} + + // Read the file line by line + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // Ideally .env file shouldn't have empty lines and comments but + // this check makes it allowed. + if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid line in .env file: %s", line) + } + // Also ideally, .env files should not have space in their `key=value` format + // but this allows `key = value` to pass through as well + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + // Add the parsed key-value pair to the map + envMap[key] = value + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to read env file: %v", err) + } + return envMap, nil } diff --git a/testscripts/run/envfrom.test.txt b/testscripts/run/envfrom.test.txt new file mode 100644 index 00000000000..68a8cdc2895 --- /dev/null +++ b/testscripts/run/envfrom.test.txt @@ -0,0 +1,36 @@ +# Tests related to setting the env_from for devbox run. + +exec devbox run test +stdout 'BAR' + +exec devbox run test2 +stdout 'BAZ' + +exec devbox run test3 +stdout 'BAS' + +exec devbox run test4 +stdout '' + +-- test.env -- +FOO=BAR +FOO2 = BAZ +FOO3=ToBeOverwrittenByDevboxJSON +# FOO4=comment shouldn't be processed + +-- devbox.json -- +{ + "packages": [], + "env": { + "FOO3": "BAS" + }, + "shell": { + "scripts": { + "test": "echo $FOO", + "test2": "echo $FOO2", + "test3": "echo $FOO3", + "test4": "echo $FOO4" + } + }, + "env_from": "test.env" +}