github.com/docker/app@v0.9.1-beta3.0.20210611140623-a48f773ab002/internal/validator/validator.go (about) 1 package validator 2 3 import ( 4 "io/ioutil" 5 "sort" 6 "strings" 7 8 "github.com/docker/app/internal/validator/rules" 9 composeloader "github.com/docker/cli/cli/compose/loader" 10 "github.com/pkg/errors" 11 ) 12 13 type Validator struct { 14 Rules []rules.Rule 15 errors []error 16 } 17 18 type ValidationError struct { 19 Errors []error 20 } 21 22 type ValidationCallback func(string, string, interface{}) 23 24 func (v ValidationError) Error() string { 25 parts := []string{} 26 for _, err := range v.Errors { 27 parts = append(parts, "* "+err.Error()) 28 } 29 30 sort.Strings(parts) 31 parts = append([]string{"Compose file validation failed:"}, parts...) 32 33 return strings.Join(parts, "\n") 34 } 35 36 type Config func(*Validator) 37 type Opt func(c *Validator) error 38 39 func NewValidator(opts ...Config) Validator { 40 validator := Validator{} 41 for _, opt := range opts { 42 opt(&validator) 43 } 44 return validator 45 } 46 47 func WithRelativePathRule() Config { 48 return func(v *Validator) { 49 v.Rules = append(v.Rules, rules.NewRelativePathRule()) 50 } 51 } 52 53 func WithExternalSecretsRule() Config { 54 return func(v *Validator) { 55 v.Rules = append(v.Rules, rules.NewExternalSecretsRule()) 56 } 57 } 58 59 func NewValidatorWithDefaults() Validator { 60 return NewValidator( 61 WithRelativePathRule(), 62 WithExternalSecretsRule(), 63 ) 64 } 65 66 // Validate validates the compose file, it returns an error 67 // if it can't parse the compose file or a ValidationError 68 // that contains all the validation errors (if any), nil otherwise 69 func (v *Validator) Validate(composeFile string) error { 70 composeRaw, err := ioutil.ReadFile(composeFile) 71 if err != nil { 72 return errors.Wrapf(err, "failed to read compose file %q", composeFile) 73 } 74 cfgMap, err := composeloader.ParseYAML(composeRaw) 75 if err != nil { 76 return errors.Wrap(err, "failed to parse compose file") 77 } 78 79 // First phase, the rules collect all the dependent values they need 80 v.visitAll("", cfgMap, v.collect) 81 // Second phase, validate the compose file 82 v.visitAll("", cfgMap, v.validate) 83 84 if len(v.errors) > 0 { 85 return ValidationError{ 86 Errors: v.errors, 87 } 88 } 89 return nil 90 } 91 92 func (v *Validator) collect(parent string, key string, value interface{}) { 93 for _, rule := range v.Rules { 94 rule.Collect(parent, key, value) 95 } 96 } 97 98 func (v *Validator) validate(parent string, key string, value interface{}) { 99 for _, rule := range v.Rules { 100 if rule.Accept(parent, key) { 101 verrs := rule.Validate(value) 102 if len(verrs) > 0 { 103 v.errors = append(v.errors, verrs...) 104 } 105 } 106 } 107 } 108 109 func (v *Validator) visitAll(parent string, cfgMap interface{}, cb ValidationCallback) { 110 m, ok := cfgMap.(map[string]interface{}) 111 if !ok { 112 return 113 } 114 115 for key, value := range m { 116 switch value := value.(type) { 117 case string: 118 continue 119 default: 120 cb(parent, key, value) 121 122 path := parent + "." + key 123 if parent == "" { 124 path = key 125 } 126 127 sub, ok := m[key].(map[string]interface{}) 128 if ok { 129 v.visitAll(path, sub, cb) 130 } 131 } 132 } 133 }