From b09ec14216d77bb3c843694c84a9b57f4b719fc4 Mon Sep 17 00:00:00 2001 From: apoorvam Date: Wed, 15 May 2019 14:07:58 -0400 Subject: [PATCH] Add validations to check that host directory exists in mount option --- internal/util/util.go | 22 ++++++++++++ internal/util/util_test.go | 51 ++++++++++++++++++++++++++++ pkg/config/config.go | 51 ++++++++++++++++------------ pkg/config/config_test.go | 68 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 168 insertions(+), 24 deletions(-) create mode 100644 internal/util/util.go create mode 100644 internal/util/util_test.go diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..a414a41 --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,22 @@ +package util + +import ( + "os" + "path" + "strings" +) + +// HomeDir is the environment variable HOME +var HomeDir = os.Getenv("HOME") + +// DirExists returns true if the given param is a valid existing directory +func DirExists(dir string) bool { + if dir[0] == '~' { + dir = path.Join(HomeDir, strings.Trim(dir, "~")) + } + src, err := os.Stat(dir) + if err != nil { + return false + } + return src.IsDir() +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go new file mode 100644 index 0000000..ce7993a --- /dev/null +++ b/internal/util/util_test.go @@ -0,0 +1,51 @@ +package util + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestDirExistsSuccess(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "TestDir") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + exists := DirExists(tmpdir) + + if !exists { + t.Fatalf("Directory exists; but got false") + } +} + +func TestDirExistsFail(t *testing.T) { + exists := DirExists("this path is invalid") + + if exists { + t.Fatalf("Directory invalid; but got as exists") + } +} + +func TestDirExistsFailForFile(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "TestFileExists") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpfile.Name()) + + exists := DirExists(tmpfile.Name()) + + if exists { + t.Fatalf("Not a directory; but got as true") + } +} + +func TestDirExistsIfNotAbsPath(t *testing.T) { + exists := DirExists("~/invalidpathfortesting") + + if exists { + t.Fatalf("Not a directory; but got as true") + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 2bfe181..d53812d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -15,6 +15,7 @@ import ( ut "github.com/go-playground/universal-translator" "github.com/joho/godotenv" "github.com/leopardslab/Dunner/internal/logger" + "github.com/leopardslab/Dunner/internal/util" "github.com/leopardslab/Dunner/pkg/docker" "github.com/spf13/viper" "gopkg.in/go-playground/validator.v9" @@ -25,19 +26,23 @@ import ( var log = logger.Log var ( - uni *ut.UniversalTranslator - govalidator *validator.Validate - trans ut.Translator + uni *ut.UniversalTranslator + govalidator *validator.Validate + trans ut.Translator + defaultPermissionMode = "r" + validDirPermissionModes = []string{defaultPermissionMode, "wr", "rw", "w"} ) -var customValidations = []struct { +type customValidation struct { tag string translation string validationFn func(fl validator.FieldLevel) bool -}{ +} + +var customValidations = []customValidation{ { tag: "mountdir", - translation: "mount directory '{0}' is invalid. Use '::'", + translation: "mount directory '{0}' is invalid. Check format is '::' and has right permission level", validationFn: ValidateMountDir, }, } @@ -66,7 +71,7 @@ type Configs struct { // Validate validates config and returns errors. func (configs *Configs) Validate() []error { - err := initValidator() + err := initValidator(customValidations) if err != nil { return []error{err} } @@ -98,7 +103,7 @@ func formatErrors(valErrs error, taskName string) []error { return errs } -func initValidator() error { +func initValidator(customValidations []customValidation) error { govalidator = validator.New() govalidator.RegisterTagNameFunc(func(fld reflect.StructField) string { name := strings.SplitN(fld.Tag.Get("yaml"), ",", 2)[0] @@ -132,15 +137,28 @@ func initValidator() error { return nil } -// ValidateMountDir verifies that mount values are in proper format +// ValidateMountDir verifies that mount values are in proper format :: +// Format should match, is optional which is `readOnly` by default and `src` directory exists in host machine func ValidateMountDir(fl validator.FieldLevel) bool { value := fl.Field().String() f := func(c rune) bool { return c == ':' } mountValues := strings.FieldsFunc(value, f) + if len(mountValues) != 3 { + mountValues = append(mountValues, defaultPermissionMode) + } if len(mountValues) != 3 { return false } - return true + validPerm := false + for _, perm := range validDirPermissionModes { + if mountValues[2] == perm { + validPerm = true + } + } + if !validPerm { + return false + } + return util.DirExists(mountValues[0]) } // GetConfigs reads and parses tasks from the dunner file @@ -228,21 +246,10 @@ func DecodeMount(mounts []string, step *docker.Step) error { strings.Trim(strings.Trim(m, `'`), `"`), ":", ) - if len(arr) != 3 && len(arr) != 2 { - return fmt.Errorf( - `config: invalid format for mount %s`, - m, - ) - } var readOnly = true if len(arr) == 3 { if arr[2] == "wr" || arr[2] == "w" { readOnly = false - } else if arr[2] != "r" { - return fmt.Errorf( - `config: invalid format of read-write mode for mount '%s'`, - m, - ) } } src, err := filepath.Abs(joinPathRelToHome(arr[0])) @@ -263,7 +270,7 @@ func DecodeMount(mounts []string, step *docker.Step) error { func joinPathRelToHome(p string) string { if p[0] == '~' { - return path.Join(os.Getenv("HOME"), strings.Trim(p, "~")) + return path.Join(util.HomeDir, strings.Trim(p, "~")) } return p } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 67d0b4e..f005e9e 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -105,7 +105,7 @@ func TestConfigs_ValidateWithParseErrors(t *testing.T) { } } -func TestConfigs_ValidateWithInvalidMountDirectory(t *testing.T) { +func TestConfigs_ValidateWithInvalidMountFormat(t *testing.T) { tasks := make(map[string][]Task, 0) task := getSampleTask() task.Mounts = []string{"invalid_dir"} @@ -118,7 +118,7 @@ func TestConfigs_ValidateWithInvalidMountDirectory(t *testing.T) { t.Fatalf("expected 1 error, got %d : %s", len(errs), errs) } - expected := "task 'stats': mount directory 'invalid_dir' is invalid. Use '::'" + expected := "task 'stats': mount directory 'invalid_dir' is invalid. Check format is '::' and has right permission level" if errs[0].Error() != expected { t.Fatalf("expected: %s, got: %s", expected, errs[0].Error()) } @@ -139,6 +139,70 @@ func TestConfigs_ValidateWithValidMountDirectory(t *testing.T) { } } +func TestConfigs_ValidateWithNoModeGiven(t *testing.T) { + tasks := make(map[string][]Task, 0) + task := getSampleTask() + wd, _ := os.Getwd() + task.Mounts = []string{fmt.Sprintf("%s:%s", wd, wd)} + tasks["stats"] = []Task{task} + configs := &Configs{Tasks: tasks} + + errs := configs.Validate() + + if errs != nil { + t.Fatalf("expected no errors, got %s", errs) + } +} + +func TestConfigs_ValidateWithInvalidMode(t *testing.T) { + tasks := make(map[string][]Task, 0) + task := getSampleTask() + wd, _ := os.Getwd() + task.Mounts = []string{fmt.Sprintf("%s:%s:ab", wd, wd)} + tasks["stats"] = []Task{task} + configs := &Configs{Tasks: tasks} + + errs := configs.Validate() + + expected := fmt.Sprintf("task 'stats': mount directory '%s' is invalid. Check format is '::' and has right permission level", task.Mounts[0]) + if errs[0].Error() != expected { + t.Fatalf("expected: %s, got: %s", expected, errs[0].Error()) + } +} + +func TestConfigs_ValidateWithInvalidMountDirectory(t *testing.T) { + tasks := make(map[string][]Task, 0) + task := getSampleTask() + task.Mounts = []string{"blah:foo:w"} + tasks["stats"] = []Task{task} + configs := &Configs{Tasks: tasks} + + errs := configs.Validate() + + if len(errs) != 1 { + t.Fatalf("expected 1 error, got %d : %s", len(errs), errs) + } + + expected := "task 'stats': mount directory 'blah:foo:w' is invalid. Check format is '::' and has right permission level" + if errs[0].Error() != expected { + t.Fatalf("expected: %s, got: %s", expected, errs[0].Error()) + } +} + func getSampleTask() Task { return Task{Image: "image_name", Command: []string{"node", "--version"}} } + +func TestInitValidatorForNilTranslation(t *testing.T) { + vals := []customValidation{{tag: "foo", translation: "", validationFn: nil}} + + err := initValidator(vals) + + expected := "failed to register validation: Function cannot be empty" + if err == nil { + t.Fatalf("expected %s, got %s", expected, err) + } + if err.Error() != expected { + t.Fatalf("expected %s, got %s", expected, err.Error()) + } +}