Skip to content

Commit

Permalink
Add validation using goplayground/validator
Browse files Browse the repository at this point in the history
  • Loading branch information
apoorvam committed May 14, 2019
1 parent 72e3aba commit 3e727d3
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 63 deletions.
51 changes: 51 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@
[prune]
go-tests = true
unused-packages = true

[[constraint]]
name = "github.com/go-playground/validator"
version = "9.28.0"

[[constraint]]
name = "github.com/go-playground/locales"
version = "0.12.1"

[[constraint]]
name = "gopkg.in/go-playground/validator.v9"
version = "9.28.0"
23 changes: 22 additions & 1 deletion cmd/validate.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package cmd

import (
"os"

"github.com/leopardslab/Dunner/pkg/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func init() {
Expand All @@ -13,6 +16,24 @@ var validateCmd = &cobra.Command{
Use: "validate",
Short: "Validate the dunner task file `.dunner.yaml`",
Long: "You can validate task file `.dunner.yaml` with this command to see if there are any parse errors",
Run: config.Validate,
Run: Validate,
Args: cobra.MinimumNArgs(0),
}

// Validate command invoked from command line, validates the dunner task file. If there are errors, it fails with non-zero exit code.
func Validate(_ *cobra.Command, args []string) {
var dunnerFile = viper.GetString("DunnerTaskFile")

configs, err := config.GetConfigs(dunnerFile)
if err != nil {
log.Fatal(err)
}

errs := configs.Validate()
if len(errs) != 0 {
for _, err := range errs {
log.Error(err)
}
os.Exit(1)
}
}
80 changes: 61 additions & 19 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,30 @@ import (
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"strings"

"github.com/docker/docker/api/types/mount"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
"github.com/joho/godotenv"
"github.com/leopardslab/Dunner/internal/logger"
"github.com/leopardslab/Dunner/pkg/docker"
"github.com/spf13/viper"
"gopkg.in/go-playground/validator.v9"
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
yaml "gopkg.in/yaml.v2"
)

var log = logger.Log

var (
uni *ut.UniversalTranslator
govalidator *validator.Validate
trans ut.Translator
)

type DirMount struct {
Src string `yaml:"src"`
Dest string `yaml:"dest"`
Expand All @@ -28,42 +39,73 @@ type DirMount struct {
// Task describes a single task to be run in a docker container
type Task struct {
Name string `yaml:"name"`
Image string `yaml:"image"`
Image string `yaml:"image" validate:"required"`
SubDir string `yaml:"dir"`
Command []string `yaml:"command"`
Command []string `yaml:"command" validate:"required,min=1,dive,required"`
Envs []string `yaml:"envs"`
Mounts []string `yaml:"mounts"`
Args []string `yaml:"args"`
}

// Configs describes the parsed information from the dunner file
type Configs struct {
Tasks map[string][]Task
Tasks map[string][]Task `validate:"required,min=1,dive,keys,required,endkeys,required,min=1,required"`
}

// Validates config and returns a list of errors and warnings. If errors are not critical/only warnings, it returns param `ok` as true, else false
func (configs *Configs) Validate() ([]error, bool) {
var errs []error
var warnings []error
if len(configs.Tasks) == 0 {
warnings = append(warnings, fmt.Errorf("dunner: No tasks defined"))
// Validate validates config and returns errors.
func (configs *Configs) Validate() []error {
err := initValidator()
if err != nil {
return []error{err}
}
valErrs := govalidator.Struct(configs)
errs := formatErrors(valErrs, "")

for taskName, tasks := range configs.Tasks {
for _, task := range tasks {
if task.Image == "" {
errs = append(errs, fmt.Errorf(`dunner: [%s] Image repository name cannot be empty`, taskName))
}
if len(task.Command) == 0 {
errs = append(errs, fmt.Errorf("dunner: [%s] Commands not defined for task with image %s", taskName, task.Image))
taskValErrs := govalidator.Var(tasks, "dive")
errs = append(errs, formatErrors(taskValErrs, taskName)...)
}
return errs
}

func formatErrors(valErrs error, taskName string) []error {
var errs []error
if valErrs != nil {
if _, ok := valErrs.(*validator.InvalidValidationError); ok {
errs = append(errs, valErrs)
} else {
for _, e := range valErrs.(validator.ValidationErrors) {
if taskName == "" {
errs = append(errs, fmt.Errorf(e.Translate(trans)))
} else {
errs = append(errs, fmt.Errorf("task '%s': %s", taskName, e.Translate(trans)))
}
}
}
}
if len(errs) > 0 {
errs = append(errs, warnings...)
return errs, false
return errs
}

func initValidator() error {
govalidator = validator.New()
govalidator.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("yaml"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})

translator := en.New()
uni = ut.New(translator, translator)

var translatorFound bool
trans, translatorFound = uni.GetTranslator("en")
if !translatorFound {
return fmt.Errorf("failed to initialize validator with translator")
}
return warnings, true
en_translations.RegisterDefaultTranslations(govalidator, trans)
return nil
}

// GetConfigs reads and parses tasks from the dunner file
Expand Down
32 changes: 19 additions & 13 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,25 @@ func TestConfigs_Validate(t *testing.T) {
tasks["stats"] = []Task{getSampleTask()}
configs := &Configs{Tasks: tasks}

errs, ok := configs.Validate()
errs := configs.Validate()

if !ok || len(errs) != 0 {
t.Fatalf("Configs Validation failed, expected to pass")
if len(errs) != 0 {
t.Fatalf("Configs Validation failed, expected to pass. got: %s", errs)
}
}

func TestConfigs_ValidateWithNoTasks(t *testing.T) {
tasks := make(map[string][]Task, 0)
configs := &Configs{Tasks: tasks}

errs, ok := configs.Validate()
errs := configs.Validate()

if !ok || len(errs) != 1 {
t.Fatalf("Configs validation failed")
if len(errs) != 1 {
t.Fatalf("Configs validation failed, expected 1 error, got %s", errs)
}
if errs[0].Error() != "dunner: No tasks defined" {
t.Fatalf("Configs Validation error message not as expected")
expected := "Tasks must contain at least 1 item"
if errs[0].Error() != expected {
t.Fatalf("expected: %s, got: %s", expected, errs[0].Error())
}
}

Expand All @@ -87,14 +88,19 @@ func TestConfigs_ValidateWithParseErrors(t *testing.T) {
tasks["stats"] = []Task{task}
configs := &Configs{Tasks: tasks}

errs, ok := configs.Validate()
errs := configs.Validate()

if ok || len(errs) != 2 {
t.Fatalf("Configs validation failed")
if len(errs) != 2 {
t.Fatalf("expected 2 errors, got %d", len(errs))
}

if errs[0].Error() != "dunner: [stats] Image repository name cannot be empty" || errs[1].Error() != "dunner: [stats] Commands not defined for task with image " {
t.Fatalf("Configs Validation error message not as expected")
expected1 := "task 'stats': image is a required field"
expected2 := "task 'stats': command must contain at least 1 item"
if errs[0].Error() != expected1 {
t.Fatalf("expected: %s, got: %s", expected1, errs[0].Error())
}
if errs[1].Error() != expected2 {
t.Fatalf("expected: %s, got: %s", expected2, errs[1].Error())
}
}

Expand Down
25 changes: 0 additions & 25 deletions pkg/config/validate.go

This file was deleted.

10 changes: 5 additions & 5 deletions pkg/dunner/dunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ func Do(_ *cobra.Command, args []string) {
if err != nil {
log.Fatal(err)
}
errs, ok := configs.Validate()
for _, err := range errs {
log.Error(err)
}
if !ok {
errs := configs.Validate()
if len(errs) != 0 {
for _, err := range errs {
log.Error(err)
}
os.Exit(1)
}

Expand Down

0 comments on commit 3e727d3

Please sign in to comment.