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  }