Skip to content

Commit f24cbf3

Browse files
committed
fix(config): validate volume mount paths syntactically instead of filepath
1 parent 4ca67d0 commit f24cbf3

File tree

3 files changed

+102
-2
lines changed

3 files changed

+102
-2
lines changed

internal/config/types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ type ResourceConfig struct {
9494
// VolumeMount represents a volume mount configuration
9595
type VolumeMount struct {
9696
Name string `yaml:"name" validate:"required,min=1,max=63,alphanum"`
97-
LocalPath string `yaml:"localPath" validate:"required,min=1,filepath"`
98-
ContainerPath string `yaml:"containerPath" validate:"required,min=1,filepath"`
97+
LocalPath string `yaml:"localPath" validate:"required,mount_path"`
98+
ContainerPath string `yaml:"containerPath" validate:"required,mount_path"`
9999
}
100100

101101
// RefreshConfig represents auto-refresh settings

internal/config/validation.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ func init() {
5757
if err := validate.RegisterValidation("k8s_memory", validateKubernetesMemory); err != nil {
5858
panic(fmt.Errorf("register validator k8s_memory: %w", err))
5959
}
60+
if err := validate.RegisterValidation("mount_path", validateMountPath); err != nil {
61+
panic(fmt.Errorf("register validator mount_path: %w", err))
62+
}
6063
validate.RegisterStructValidation(validateGitRepo, GitRepo{})
6164
}
6265

@@ -188,6 +191,33 @@ func validateKubernetesMemory(fl validator.FieldLevel) bool {
188191
}
189192
}
190193

194+
// validateMountPath implements the "mount_path" tag.
195+
// It validates mount paths using syntax only (no filesystem checks).
196+
func validateMountPath(fl validator.FieldLevel) bool {
197+
p, ok := fl.Field().Interface().(string)
198+
if !ok {
199+
return false
200+
}
201+
202+
p = strings.TrimSpace(p)
203+
if p == "" {
204+
return false
205+
}
206+
207+
// Kubernetes-style mount paths are absolute, slash-separated paths.
208+
if !path.IsAbs(p) {
209+
return false
210+
}
211+
212+
// Reject NUL bytes; otherwise rely on lexical cleaning only.
213+
if strings.ContainsRune(p, '\x00') {
214+
return false
215+
}
216+
217+
clean := path.Clean(p)
218+
return clean != "" && clean != "."
219+
}
220+
191221
// ValidateDevEnvConfig runs tag-based validation and then applies
192222
// additional semantic checks that are easier to express in code.
193223
func ValidateDevEnvConfig(config *DevEnvConfig) error {
@@ -282,6 +312,8 @@ func formatFieldError(fieldError validator.FieldError) string {
282312
return fmt.Sprintf("'%s' must be a valid URL, got '%v'", fieldName, value)
283313
case "filepath":
284314
return fmt.Sprintf("'%s' must be a valid file path, got '%v'", fieldName, value)
315+
case "mount_path":
316+
return fmt.Sprintf("'%s' must be a valid absolute mount path, got '%v'", fieldName, value)
285317
case "cron":
286318
return fmt.Sprintf("'%s' must be a valid cron expression, got '%v'", fieldName, value)
287319

internal/config/validation_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,71 @@ func TestValidateDevEnvConfig_PythonBinPathMustBeAbsolute(t *testing.T) {
279279
assert.Contains(t, err.Error(), "pythonBinPath")
280280
assert.Contains(t, err.Error(), "absolute path")
281281
}
282+
283+
func TestValidator_MountPath(t *testing.T) {
284+
type S struct {
285+
Path string `validate:"mount_path"`
286+
}
287+
288+
cases := []struct {
289+
name string
290+
val string
291+
ok bool
292+
}{
293+
{name: "root mount dir", val: "/mnt", ok: true},
294+
{name: "mount subpath", val: "/mnt/data", ok: true},
295+
{name: "root", val: "/", ok: true},
296+
{name: "empty", val: "", ok: false},
297+
{name: "whitespace", val: " ", ok: false},
298+
{name: "relative path", val: "mnt/data", ok: false},
299+
}
300+
301+
for _, tc := range cases {
302+
t.Run(tc.name, func(t *testing.T) {
303+
err := validate.Struct(&S{Path: tc.val})
304+
if tc.ok {
305+
require.NoError(t, err)
306+
} else {
307+
require.Error(t, err)
308+
}
309+
})
310+
}
311+
}
312+
313+
func TestValidateDevEnvConfig_VolumeMountPaths(t *testing.T) {
314+
newCfg := func(localPath, containerPath string) *DevEnvConfig {
315+
return &DevEnvConfig{
316+
Name: "alice",
317+
BaseConfig: BaseConfig{
318+
SSHPublicKey: "ssh-ed25519 AAAAB3NzaC1lZDI1NTE5AAAA user@host",
319+
Volumes: []VolumeMount{
320+
{
321+
Name: "mnt",
322+
LocalPath: localPath,
323+
ContainerPath: containerPath,
324+
},
325+
},
326+
},
327+
}
328+
}
329+
330+
t.Run("accepts root directory mounts", func(t *testing.T) {
331+
require.NoError(t, ValidateDevEnvConfig(newCfg("/mnt", "/mnt")))
332+
})
333+
334+
t.Run("accepts mount subpaths", func(t *testing.T) {
335+
require.NoError(t, ValidateDevEnvConfig(newCfg("/mnt/data", "/mnt/data")))
336+
})
337+
338+
t.Run("rejects empty localPath", func(t *testing.T) {
339+
err := ValidateDevEnvConfig(newCfg("", "/mnt"))
340+
require.Error(t, err)
341+
assert.Contains(t, err.Error(), "LocalPath")
342+
})
343+
344+
t.Run("rejects empty containerPath", func(t *testing.T) {
345+
err := ValidateDevEnvConfig(newCfg("/mnt", ""))
346+
require.Error(t, err)
347+
assert.Contains(t, err.Error(), "ContainerPath")
348+
})
349+
}

0 commit comments

Comments
 (0)