-
Notifications
You must be signed in to change notification settings - Fork 228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[bug] plugin env PATH fix #2418
Conversation
internal/devconfig/config.go
Outdated
rootEnv := mergePATHsFromTwoEnvs(env, c.Root.Env) | ||
maps.Copy(env, rootEnv) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use OSExpandEnvMap
instead?
Basically take the environment provided by the plugins and use it to expand the environment of the config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeland73 after your suggestion, I tried OSExpandEnvMap
and it it doesn't work.
The issue with OSExpandEnvMap
is it merges the two PATHs correctly but it applies the (non-nix) system PATH to the :$PATH
at the end. So with OSExpandEnvMap
PATH becomes: /my/config:/my/plugin:/opt/homebrew/sbin:/usr/local/bin:...
With mergePATHsFromTwoEnvs
PATH becomes: /my/config:/my/plugin:/Users/mohsenansari/code/go.jetpack.io/devbox/examples/stacks/rails/.devbox/nix/profile/default/bin:...:/opt/homebrew/sbin:/usr/local/bin:...
So OSExpandEnvMap can't work here because I don't want to expand $PATH env yet, I just want to merge them correctly without losing :$PATH
suffix or $PATH:
prefix to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Essentially, OSExpandEnvMap
expands PATH with non-devboxified PATH value, but I don't want that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would something like
func OSExpandIfPossible(env, existingEnv map[string]string) map[string]string {
mapping := func(value string) string {
// If the value is not set in existingEnv, return the value wrapped in ${...}
if existingEnv == nil || existingEnv[value] == "" {
return fmt.Sprintf("${%s}", value)
}
return existingEnv[value]
}
res := map[string]string{}
for k, v := range env {
res[k] = os.Expand(v, mapping)
}
return res
}
work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And some unit tests if you decide to go this route:
package conf
import "testing"
func TestOSExpandIfPossible(t *testing.T) {
tests := []struct {
name string
env map[string]string
existingEnv map[string]string
want map[string]string
}{
{
name: "basic expansion",
env: map[string]string{
"FOO": "$BAR",
"BAZ": "${QUX}",
},
existingEnv: map[string]string{
"BAR": "bar_value",
"QUX": "qux_value",
},
want: map[string]string{
"FOO": "bar_value",
"BAZ": "qux_value",
},
},
{
name: "missing values remain as template",
env: map[string]string{
"FOO": "$BAR",
"BAZ": "${QUX}",
},
existingEnv: map[string]string{
"BAR": "bar_value",
// QUX is missing
},
want: map[string]string{
"FOO": "bar_value",
"BAZ": "${QUX}",
},
},
{
name: "nil existing env",
env: map[string]string{
"FOO": "$BAR",
"BAZ": "${QUX}",
},
existingEnv: nil,
want: map[string]string{
"FOO": "${BAR}",
"BAZ": "${QUX}",
},
},
{
name: "empty existing env",
env: map[string]string{
"FOO": "$BAR",
},
existingEnv: map[string]string{},
want: map[string]string{
"FOO": "${BAR}",
},
},
{
name: "mixed literal and variable",
env: map[string]string{
"FOO": "prefix_${BAR}_suffix",
},
existingEnv: map[string]string{
"BAR": "bar_value",
},
want: map[string]string{
"FOO": "prefix_bar_value_suffix",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := OSExpandIfPossible(tt.env, tt.existingEnv)
if len(got) != len(tt.want) {
t.Errorf("OSExpandIfPossible() got %v entries, want %v entries", len(got), len(tt.want))
}
for k, v := range tt.want {
if got[k] != v {
t.Errorf("OSExpandIfPossible() for key %q = %q, want %q", k, got[k], v)
}
}
})
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OSExpandIfPossible
works. I included this suggestion along with the unit test and added a case for path to the unit test as well.
internal/devconfig/config.go
Outdated
pluginEnv := mergePATHsFromTwoEnvs(env, i.Env()) | ||
maps.Copy(env, pluginEnv) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you use OSExpandEnvMap
instead of mergePATHsFromTwoEnvs
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should treat $PATH different from other env vars. Also, a minor nit is that you are not really "merging" paths, you are expanding, but only if $PATH already exists in the new environment you are creating.
I think what you want is something like OSExpandIfPossible
(I added a possible implementation).
Also, for stuff like this, adding some unit tests and/or some integration tests in example dir is really useful, since reasoning about what the final environment will look like is hard. See https://github.com/jetify-com/devbox/blob/163ae999a46e6ff4b946437b22a782557e708eeb/examples/plugins/v2-local/devbox.json#L1C1-L36C1 for an example of an integration test for plugins.
internal/devconfig/config.go
Outdated
rootEnv := mergePATHsFromTwoEnvs(env, c.Root.Env) | ||
maps.Copy(env, rootEnv) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would something like
func OSExpandIfPossible(env, existingEnv map[string]string) map[string]string {
mapping := func(value string) string {
// If the value is not set in existingEnv, return the value wrapped in ${...}
if existingEnv == nil || existingEnv[value] == "" {
return fmt.Sprintf("${%s}", value)
}
return existingEnv[value]
}
res := map[string]string{}
for k, v := range env {
res[k] = os.Expand(v, mapping)
}
return res
}
work?
internal/devconfig/config.go
Outdated
rootEnv := mergePATHsFromTwoEnvs(env, c.Root.Env) | ||
maps.Copy(env, rootEnv) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And some unit tests if you decide to go this route:
package conf
import "testing"
func TestOSExpandIfPossible(t *testing.T) {
tests := []struct {
name string
env map[string]string
existingEnv map[string]string
want map[string]string
}{
{
name: "basic expansion",
env: map[string]string{
"FOO": "$BAR",
"BAZ": "${QUX}",
},
existingEnv: map[string]string{
"BAR": "bar_value",
"QUX": "qux_value",
},
want: map[string]string{
"FOO": "bar_value",
"BAZ": "qux_value",
},
},
{
name: "missing values remain as template",
env: map[string]string{
"FOO": "$BAR",
"BAZ": "${QUX}",
},
existingEnv: map[string]string{
"BAR": "bar_value",
// QUX is missing
},
want: map[string]string{
"FOO": "bar_value",
"BAZ": "${QUX}",
},
},
{
name: "nil existing env",
env: map[string]string{
"FOO": "$BAR",
"BAZ": "${QUX}",
},
existingEnv: nil,
want: map[string]string{
"FOO": "${BAR}",
"BAZ": "${QUX}",
},
},
{
name: "empty existing env",
env: map[string]string{
"FOO": "$BAR",
},
existingEnv: map[string]string{},
want: map[string]string{
"FOO": "${BAR}",
},
},
{
name: "mixed literal and variable",
env: map[string]string{
"FOO": "prefix_${BAR}_suffix",
},
existingEnv: map[string]string{
"BAR": "bar_value",
},
want: map[string]string{
"FOO": "prefix_bar_value_suffix",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := OSExpandIfPossible(tt.env, tt.existingEnv)
if len(got) != len(tt.want) {
t.Errorf("OSExpandIfPossible() got %v entries, want %v entries", len(got), len(tt.want))
}
for k, v := range tt.want {
if got[k] != v {
t.Errorf("OSExpandIfPossible() for key %q = %q, want %q", k, got[k], v)
}
}
})
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RC to send back to your queue
Summary
When a plugin modifies PATH and also config in devbox.json modifies PATH via
env:{}
, the env overwrites the plugin and so PATH modification from the plugin is lost. This is because we domaps.Copy()
to merge the env from plugin to the one from config.The copy is fine for all other env variables but for PATH we need to handle merging the two rather than overwriting.
Fixes #2138
How was it tested?
bug recreate:
"env": {"PATH": "/my/config/bin:$PATH"}
"/my/config/bin"
is prepended.fix recreate: