Skip to content

Commit 3905a98

Browse files
committed
improve flexibility of limactl shell cmd
Signed-off-by: olalekan odukoya <[email protected]>
1 parent 7dcdf6a commit 3905a98

File tree

3 files changed

+130
-40
lines changed

3 files changed

+130
-40
lines changed

hack/bats/tests/preserve-env.bats

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ local_setup() {
9393
assert_line BAR=bar
9494
}
9595

96-
@test 'wildcard does only work at the end of the pattern' {
96+
@test 'wildcard works at the start of the pattern' {
9797
export LIMA_SHELLENV_BLOCK="*FOO"
9898
export FOO=foo
9999
export BARFOO=barfoo
100100
run -0 limactl shell --preserve-env "$NAME" printenv
101-
assert_line FOO=foo
102-
assert_line BARFOO=barfoo
101+
refute_line --regexp '^BARFOO='
102+
refute_line --regexp '^FOO='
103103
}
104104

105105
@test 'block list can use a , separated list with whitespace ignored' {
@@ -138,15 +138,6 @@ local_setup() {
138138
refute_line --regexp '^BARBAZ='
139139
}
140140

141-
@test 'setting both allow list and block list generates a warning' {
142-
export LIMA_SHELLENV_ALLOW=FOO
143-
export LIMA_SHELLENV_BLOCK=BAR
144-
export FOO=foo
145-
run -0 --separate-stderr limactl shell --preserve-env "$NAME" printenv FOO
146-
assert_output foo
147-
assert_stderr --regexp 'level=warning msg="Both LIMA_SHELLENV_BLOCK and LIMA_SHELLENV_ALLOW are set'
148-
}
149-
150141
@test 'limactl info includes the default block list' {
151142
run -0 limactl info
152143
run -0 limactl yq '.shellEnvBlock[]' <<<"$output"

pkg/envutil/envutil.go

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
package envutil
55

66
import (
7+
"fmt"
78
"os"
9+
"regexp"
810
"slices"
911
"strings"
1012

@@ -42,27 +44,55 @@ var defaultBlockList = []string{
4244
"_*", // Variables starting with underscore are typically internal
4345
}
4446

47+
func validatePattern(pattern string) error {
48+
validPattern := regexp.MustCompile(`^[a-zA-Z0-9_*]*$`)
49+
if !validPattern.MatchString(pattern) {
50+
return fmt.Errorf("pattern %q contains invalid characters. Only alphanumeric characters and underscores are allowed", pattern)
51+
}
52+
return nil
53+
}
54+
4555
// getBlockList returns the list of environment variable patterns to be blocked.
4656
// The second return value indicates whether the list was explicitly set via LIMA_SHELLENV_BLOCK.
47-
func getBlockList() ([]string, bool) {
57+
func getBlockList() []string {
4858
blockEnv := os.Getenv("LIMA_SHELLENV_BLOCK")
4959
if blockEnv == "" {
50-
return defaultBlockList, false
60+
return defaultBlockList
5161
}
5262
after, found := strings.CutPrefix(blockEnv, "+")
63+
var patterns []string
64+
if !found {
65+
patterns = parseEnvList(blockEnv)
66+
} else {
67+
patterns = parseEnvList(after)
68+
}
69+
70+
for _, pattern := range patterns {
71+
if err := validatePattern(pattern); err != nil {
72+
logrus.Fatalf("Invalid LIMA_SHELLENV_BLOCK pattern: %v", err)
73+
}
74+
}
75+
5376
if !found {
54-
return parseEnvList(blockEnv), true
77+
return patterns
5578
}
56-
return slices.Concat(defaultBlockList, parseEnvList(after)), true
79+
return slices.Concat(defaultBlockList, patterns)
5780
}
5881

5982
// getAllowList returns the list of environment variable patterns to be allowed.
6083
// The second return value indicates whether the list was explicitly set via LIMA_SHELLENV_ALLOW.
61-
func getAllowList() ([]string, bool) {
84+
func getAllowList() []string {
6285
if allowEnv := os.Getenv("LIMA_SHELLENV_ALLOW"); allowEnv != "" {
63-
return parseEnvList(allowEnv), true
86+
patterns := parseEnvList(allowEnv)
87+
88+
for _, pattern := range patterns {
89+
if err := validatePattern(pattern); err != nil {
90+
logrus.Fatalf("Invalid LIMA_SHELLENV_ALLOW pattern: %v", err)
91+
}
92+
}
93+
return patterns
6494
}
65-
return nil, false
95+
return nil
6696
}
6797

6898
func parseEnvList(envList string) []string {
@@ -82,8 +112,39 @@ func matchesPattern(name, pattern string) bool {
82112
return true
83113
}
84114

85-
prefix, found := strings.CutSuffix(pattern, "*")
86-
return found && strings.HasPrefix(name, prefix)
115+
parts := strings.Split(pattern, "*")
116+
117+
currentNameIndex := 0
118+
119+
for i, part := range parts {
120+
if part == "" {
121+
continue
122+
}
123+
124+
foundIndex := strings.Index(name[currentNameIndex:], part)
125+
126+
if foundIndex == -1 {
127+
return false
128+
}
129+
130+
foundSegmentStart := currentNameIndex + foundIndex
131+
132+
if i == 0 {
133+
if !strings.HasPrefix(pattern, "*") && foundSegmentStart != 0 {
134+
return false
135+
}
136+
}
137+
138+
if i == len(parts)-1 {
139+
if !strings.HasSuffix(pattern, "*") && !strings.HasSuffix(name, part) {
140+
return false
141+
}
142+
}
143+
144+
currentNameIndex = foundSegmentStart + len(part)
145+
}
146+
147+
return true
87148
}
88149

89150
func matchesAnyPattern(name string, patterns []string) bool {
@@ -96,17 +157,10 @@ func matchesAnyPattern(name string, patterns []string) bool {
96157
// It returns a slice of environment variables that are not blocked by the current configuration.
97158
// The filtering is controlled by LIMA_SHELLENV_BLOCK and LIMA_SHELLENV_ALLOW environment variables.
98159
func FilterEnvironment() []string {
99-
allowList, isAllowListSet := getAllowList()
100-
blockList, isBlockListSet := getBlockList()
101-
102-
if isBlockListSet && isAllowListSet {
103-
logrus.Warn("Both LIMA_SHELLENV_BLOCK and LIMA_SHELLENV_ALLOW are set. Block list will be ignored.")
104-
blockList = nil
105-
}
106160
return filterEnvironmentWithLists(
107161
os.Environ(),
108-
allowList,
109-
blockList,
162+
getAllowList(),
163+
getBlockList(),
110164
)
111165
}
112166

pkg/envutil/envutil_test.go

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,9 @@ func TestGetBlockAndAllowLists(t *testing.T) {
8888
t.Setenv("LIMA_SHELLENV_BLOCK", "")
8989
t.Setenv("LIMA_SHELLENV_ALLOW", "")
9090

91-
blockList, isBlockListSet := getBlockList()
92-
allowList, isAllowListSet := getAllowList()
91+
blockList := getBlockList()
92+
allowList := getAllowList()
9393

94-
assert.Assert(t, !isBlockListSet)
95-
assert.Assert(t, !isAllowListSet)
9694
assert.Assert(t, isUsingDefaultBlockList())
9795
assert.DeepEqual(t, blockList, defaultBlockList)
9896
assert.Equal(t, len(allowList), 0)
@@ -101,8 +99,7 @@ func TestGetBlockAndAllowLists(t *testing.T) {
10199
t.Run("custom blocklist", func(t *testing.T) {
102100
t.Setenv("LIMA_SHELLENV_BLOCK", "PATH,HOME")
103101

104-
blockList, isSet := getBlockList()
105-
assert.Assert(t, isSet)
102+
blockList := getBlockList()
106103
assert.Assert(t, !isUsingDefaultBlockList())
107104
expected := []string{"PATH", "HOME"}
108105
assert.DeepEqual(t, blockList, expected)
@@ -111,8 +108,7 @@ func TestGetBlockAndAllowLists(t *testing.T) {
111108
t.Run("additive blocklist", func(t *testing.T) {
112109
t.Setenv("LIMA_SHELLENV_BLOCK", "+CUSTOM_VAR")
113110

114-
blockList, isSet := getBlockList()
115-
assert.Assert(t, isSet)
111+
blockList := getBlockList()
116112
assert.Assert(t, isUsingDefaultBlockList())
117113
expected := slices.Concat(GetDefaultBlockList(), []string{"CUSTOM_VAR"})
118114
assert.DeepEqual(t, blockList, expected)
@@ -121,8 +117,7 @@ func TestGetBlockAndAllowLists(t *testing.T) {
121117
t.Run("allowlist", func(t *testing.T) {
122118
t.Setenv("LIMA_SHELLENV_ALLOW", "FOO,BAR")
123119

124-
allowList, isSet := getAllowList()
125-
assert.Assert(t, isSet)
120+
allowList := getAllowList()
126121
expected := []string{"FOO", "BAR"}
127122
assert.DeepEqual(t, allowList, expected)
128123
})
@@ -213,3 +208,53 @@ func TestGetDefaultBlockList(t *testing.T) {
213208
assert.Assert(t, found, "Expected builtin blocklist to contain %q", item)
214209
}
215210
}
211+
212+
func TestValidatePattern(t *testing.T) {
213+
tests := []struct {
214+
name string
215+
pattern string
216+
valid bool
217+
}{
218+
{"simple alphanumeric", "FOO", true},
219+
{"with underscore", "FOO_BAR", true},
220+
{"with numbers", "VAR123", true},
221+
{"with trailing asterisk", "FOO*", true},
222+
{"with multiple asterisks", "FOO*BAR*", true},
223+
{"asterisk at beginning", "*FOO", true},
224+
{"asterisk in middle", "FOO*BAR", true},
225+
{"only asterisk", "*", true},
226+
{"with dash", "FOO-BAR", false},
227+
{"with dot", "FOO.BAR", false},
228+
{"with space", "FOO BAR", false},
229+
{"with slash", "FOO/BAR", false},
230+
{"with at symbol", "FOO@BAR", false},
231+
{"with dollar", "$FOO", false},
232+
{"with percent", "FOO%", false},
233+
{"with hash", "#FOO", false},
234+
{"with exclamation", "FOO!", false},
235+
{"with colon", "FOO:BAR", false},
236+
{"with semicolon", "FOO;BAR", false},
237+
{"with parentheses", "FOO(BAR)", false},
238+
{"with brackets", "FOO[BAR]", false},
239+
{"with braces", "FOO{BAR}", false},
240+
{"with plus", "FOO+BAR", false},
241+
{"with equals", "FOO=BAR", false},
242+
{"with pipe", "FOO|BAR", false},
243+
{"with backslash", "FOO/BAR", false},
244+
{"with question mark", "FOO?", false},
245+
{"with less than", "FOO<BAR", false},
246+
{"with greater than", "FOO>BAR", false},
247+
{"with comma", "FOO,BAR", false},
248+
}
249+
250+
for _, tt := range tests {
251+
t.Run(tt.name, func(t *testing.T) {
252+
err := validatePattern(tt.pattern)
253+
if tt.valid {
254+
assert.NilError(t, err, "Expected pattern %q to be valid", tt.pattern)
255+
} else {
256+
assert.Error(t, err, "pattern \""+tt.pattern+"\" contains invalid characters. Only alphanumeric characters and underscores are allowed")
257+
}
258+
})
259+
}
260+
}

0 commit comments

Comments
 (0)