github.com/jlmeeker/kismatic@v1.10.1-0.20180612190640-57f9005a1f1a/pkg/install/validate.go (about)

     1  package install
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net"
     7  	"os"
     8  	"path/filepath"
     9  	"reflect"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/apprenda/kismatic/pkg/validation"
    17  
    18  	"github.com/apprenda/kismatic/pkg/ssh"
    19  	"github.com/apprenda/kismatic/pkg/util"
    20  )
    21  
    22  // TODO: There is need to run validation against anything that is validatable.
    23  // Expose the validatable interface so that it can be consumed when
    24  // validating objects other than a Plan or a Node
    25  
    26  // ValidatePlan runs validation against the installation plan to ensure
    27  // that the plan contains valid user input. Returns true, nil if the validation
    28  // is successful. Otherwise, returns false and a collection of validation errors.
    29  func ValidatePlan(p *Plan) (bool, []error) {
    30  	v := newValidator()
    31  	v.validate(p)
    32  	return v.valid()
    33  }
    34  
    35  // ValidateNode runs validation against the given node.
    36  func ValidateNode(node *Node) (bool, []error) {
    37  	v := newValidator()
    38  	v.validate(node)
    39  	return v.valid()
    40  }
    41  
    42  // ValidateNodes runs validation against the given node.
    43  // Validates if the details of the nodes are unique.
    44  func ValidateNodes(nodes []Node) (bool, []error) {
    45  	v := newValidator()
    46  	v.validate(nodeList{Nodes: nodes})
    47  	return v.valid()
    48  }
    49  
    50  // ValidatePlanSSHConnections tries to establish SSH connections to all nodes in the cluster
    51  func ValidatePlanSSHConnections(p *Plan) (bool, []error) {
    52  	v := newValidator()
    53  
    54  	s := sshConnectionSet{p.Cluster.SSH, p.GetUniqueNodes()}
    55  
    56  	v.validateWithErrPrefix("Node Connnection", s)
    57  
    58  	return v.valid()
    59  }
    60  
    61  type sshConnectionSet struct {
    62  	SSHConfig SSHConfig
    63  	Nodes     []Node
    64  }
    65  
    66  // ValidateSSHConnection tries to establish SSH connection with the details provieded for a single node
    67  func ValidateSSHConnection(con *SSHConnection, prefix string) (bool, []error) {
    68  	v := newValidator()
    69  	s := sshConnectionSet{*con.SSHConfig, []Node{*con.Node}}
    70  	v.validateWithErrPrefix(prefix, s)
    71  	return v.valid()
    72  }
    73  
    74  // ValidateCertificates checks if certificates exist and are valid
    75  func ValidateCertificates(p *Plan, pki *LocalPKI) (bool, []error) {
    76  	v := newValidator()
    77  
    78  	warn, err := pki.ValidateClusterCertificates(p)
    79  	if err != nil && len(err) > 0 {
    80  		v.addError(err...)
    81  	}
    82  	if warn != nil && len(warn) > 0 {
    83  		v.addError(warn...)
    84  	}
    85  
    86  	return v.valid()
    87  }
    88  
    89  // ValidateStorageVolume validates the storage volume attributes
    90  func ValidateStorageVolume(sv StorageVolume) (bool, []error) {
    91  	return sv.validate()
    92  }
    93  
    94  type validatable interface {
    95  	validate() (bool, []error)
    96  }
    97  
    98  type validator struct {
    99  	errs []error
   100  }
   101  
   102  func newValidator() *validator {
   103  	return &validator{
   104  		errs: []error{},
   105  	}
   106  }
   107  
   108  func (v *validator) addError(err ...error) {
   109  	v.errs = append(v.errs, err...)
   110  }
   111  
   112  func (v *validator) validate(obj validatable) {
   113  	if ok, err := obj.validate(); !ok {
   114  		v.addError(err...)
   115  	}
   116  }
   117  
   118  func (v *validator) validateWithErrPrefix(prefix string, objs ...validatable) {
   119  	for _, obj := range objs {
   120  		if ok, err := obj.validate(); !ok {
   121  			newErrs := make([]error, len(err), len(err))
   122  			for i, err := range err {
   123  				newErrs[i] = fmt.Errorf("%s: %v", prefix, err)
   124  			}
   125  			v.addError(newErrs...)
   126  		}
   127  	}
   128  }
   129  
   130  func (v *validator) valid() (bool, []error) {
   131  	if len(v.errs) > 0 {
   132  		return false, v.errs
   133  	}
   134  	return true, nil
   135  }
   136  
   137  func (p *Plan) validate() (bool, []error) {
   138  	v := newValidator()
   139  
   140  	v.validate(&p.Cluster)
   141  	v.validate(&p.DockerRegistry)
   142  	if p.Cluster.DisconnectedInstallation && !p.PrivateRegistryProvided() {
   143  		v.addError(fmt.Errorf("A container image registry is required when disconnected_installation is true"))
   144  	}
   145  
   146  	v.validateWithErrPrefix("Docker", p.Docker)
   147  	v.validate(&additionalFilesGroup{AdditionalFiles: p.AdditionalFiles, Plan: p})
   148  	v.validate(&p.AddOns)
   149  	v.validate(nodeList{Nodes: p.getAllNodes()})
   150  	v.validateWithErrPrefix("Etcd nodes", &p.Etcd)
   151  	v.validateWithErrPrefix("Master nodes", &p.Master)
   152  	v.validateWithErrPrefix("Worker nodes", &p.Worker)
   153  	v.validateWithErrPrefix("Ingress nodes", &p.Ingress)
   154  	v.validate(p.NFS)
   155  	v.validateWithErrPrefix("Storage nodes", &p.Storage)
   156  
   157  	return v.valid()
   158  }
   159  
   160  func (c *Cluster) validate() (bool, []error) {
   161  	v := newValidator()
   162  	if c.Name == "" {
   163  		v.addError(errors.New("Cluster name cannot be empty"))
   164  	}
   165  	// must be a valid semver, start with "v" and be a "suppored" version
   166  	if !kubernetesVersionValid(c.Version) {
   167  		v.addError(fmt.Errorf("Cluster version %q invalid, must be a valid %q version, ie %q", c.Version, kubernetesMinorVersionString, kubernetesVersionString))
   168  	} else {
   169  		// only go out and get latest version if not disconnected install
   170  		if !c.DisconnectedInstallation {
   171  			// should not fail here as its a valid regex
   172  			version, err := parseVersion(c.Version)
   173  			// TODO print a warning
   174  			if err == nil {
   175  				latestSemver, latest, err := kubernetesLatestStableVersion() // will always return some version
   176  				if err == nil {
   177  					if version.GT(latestSemver) {
   178  						v.addError(fmt.Errorf("Cluster version %q invalid, the latest stable version is %q", c.Version, latest))
   179  					}
   180  				}
   181  				// continue with the installation if an error occurs getting the latest version
   182  			}
   183  		}
   184  	}
   185  
   186  	v.validate(&c.Networking)
   187  	v.validate(&c.Certificates)
   188  	v.validate(&c.SSH)
   189  	v.validate(&c.APIServerOptions)
   190  	v.validate(&c.KubeControllerManagerOptions)
   191  	v.validate(&c.KubeProxyOptions)
   192  	v.validate(&c.KubeSchedulerOptions)
   193  	v.validate(&c.KubeletOptions)
   194  	v.validate(&c.CloudProvider)
   195  
   196  	return v.valid()
   197  }
   198  
   199  func (n *NetworkConfig) validate() (bool, []error) {
   200  	v := newValidator()
   201  	if n.PodCIDRBlock == "" {
   202  		v.addError(errors.New("Pod CIDR block cannot be empty"))
   203  	}
   204  	if _, _, err := net.ParseCIDR(n.PodCIDRBlock); n.PodCIDRBlock != "" && err != nil {
   205  		v.addError(fmt.Errorf("Invalid Pod CIDR block provided: %v", err))
   206  	}
   207  
   208  	if n.ServiceCIDRBlock == "" {
   209  		v.addError(errors.New("Service CIDR block cannot be empty"))
   210  	}
   211  	if _, _, err := net.ParseCIDR(n.ServiceCIDRBlock); n.ServiceCIDRBlock != "" && err != nil {
   212  		v.addError(fmt.Errorf("Invalid Service CIDR block provided: %v", err))
   213  	}
   214  	return v.valid()
   215  }
   216  
   217  func (c *CertsConfig) validate() (bool, []error) {
   218  	v := newValidator()
   219  	if _, err := time.ParseDuration(c.Expiry); err != nil {
   220  		v.addError(fmt.Errorf("Invalid certificate expiry %q provided: %v", c.Expiry, err))
   221  	}
   222  	if _, err := time.ParseDuration(c.CAExpiry); c.CAExpiry != "" && err != nil { // don't error when empty for backwards compat
   223  		v.addError(fmt.Errorf("Invalid CA certificate expiry %q provider: %v", c.CAExpiry, err))
   224  	}
   225  	return v.valid()
   226  }
   227  
   228  func (s *SSHConfig) validate() (bool, []error) {
   229  	v := newValidator()
   230  	if s.User == "" {
   231  		v.addError(errors.New("SSH user field is required"))
   232  	}
   233  	if s.Key == "" {
   234  		v.addError(errors.New("SSH key field is required"))
   235  	}
   236  	if _, err := os.Stat(s.Key); os.IsNotExist(err) {
   237  		v.addError(fmt.Errorf("SSH Key file was not found at %q", s.Key))
   238  	}
   239  	if !filepath.IsAbs(s.Key) {
   240  		v.addError(errors.New("SSH Key field must be an absolute path"))
   241  	}
   242  	if s.Port < 1 || s.Port > 65535 {
   243  		v.addError(fmt.Errorf("SSH port %d is invalid. Port must be in the range 1-65535", s.Port))
   244  	}
   245  	return v.valid()
   246  }
   247  
   248  func (c *CloudProvider) validate() (bool, []error) {
   249  	v := newValidator()
   250  	if c.Provider != "" {
   251  		if !util.Contains(c.Provider, cloudProviders()) {
   252  			v.addError(fmt.Errorf("%q is not a valid cloud provider. Options are %v", c.Provider, cloudProviders()))
   253  		}
   254  		if c.Config != "" {
   255  			if _, err := os.Stat(c.Config); os.IsNotExist(err) {
   256  				v.addError(fmt.Errorf("cloud config file was not found at %q", c.Config))
   257  			}
   258  		}
   259  	}
   260  	return v.valid()
   261  }
   262  
   263  type additionalFilesGroup struct {
   264  	AdditionalFiles []AdditionalFile
   265  	Plan            *Plan
   266  }
   267  
   268  func (fg *additionalFilesGroup) validate() (bool, []error) {
   269  	v := newValidator()
   270  	for _, f := range fg.AdditionalFiles {
   271  		if len(f.Hosts) < 1 {
   272  			v.addError(errors.New("File hosts cannot be empty"))
   273  		}
   274  		for _, h := range f.Hosts {
   275  			if !(fg.Plan.HostExists(h) || h == "all" || fg.Plan.ValidRole(h)) {
   276  				v.addError(fmt.Errorf("File host %q does not match any hosts or roles in the plan file", h))
   277  			}
   278  		}
   279  		if !f.SkipValidation {
   280  			if _, err := os.Stat(f.Source); os.IsNotExist(err) {
   281  				v.addError(fmt.Errorf("File source %q doesn't exist", f.Source))
   282  			}
   283  		}
   284  		if f.Source == "" || !filepath.IsAbs(f.Source) {
   285  			v.addError(fmt.Errorf("File source %q must be a valid absolute path", f.Source))
   286  		}
   287  		if f.Destination == "" || !filepath.IsAbs(f.Destination) {
   288  			v.addError(fmt.Errorf("File destination %q must be a valid absolute path", f.Destination))
   289  		}
   290  	}
   291  	return v.valid()
   292  }
   293  
   294  func (f *AddOns) validate() (bool, []error) {
   295  	v := newValidator()
   296  	v.validate(f.CNI)
   297  	v.validate(f.DNS)
   298  	v.validate(f.HeapsterMonitoring)
   299  	v.validate(&f.Dashboard)
   300  	v.validate(&f.PackageManager)
   301  	return v.valid()
   302  }
   303  
   304  func (n *CNI) validate() (bool, []error) {
   305  	v := newValidator()
   306  	if n != nil && !n.Disable {
   307  		if !util.Contains(n.Provider, cniProviders()) {
   308  			v.addError(fmt.Errorf("%q is not a valid CNI provider. Options are %v", n.Provider, cniProviders()))
   309  		}
   310  		if n.Provider == "calico" {
   311  			if !util.Contains(n.Options.Calico.Mode, calicoMode()) {
   312  				v.addError(fmt.Errorf("%q is not a valid Calico mode. Options are %v", n.Options.Calico.Mode, calicoMode()))
   313  			}
   314  			if !util.Contains(n.Options.Calico.LogLevel, calicoLogLevel()) {
   315  				v.addError(fmt.Errorf("%q is not a valid Calico log level. Options are %v", n.Options.Calico.LogLevel, calicoLogLevel()))
   316  			}
   317  		}
   318  	}
   319  	return v.valid()
   320  }
   321  
   322  func (n DNS) validate() (bool, []error) {
   323  	v := newValidator()
   324  	if !n.Disable {
   325  		if !util.Contains(n.Provider, dnsProviders()) {
   326  			v.addError(fmt.Errorf("%q is not a valid DNS provider. Optins are %v", n.Provider, dnsProviders()))
   327  		}
   328  	}
   329  	return v.valid()
   330  }
   331  
   332  func (h *HeapsterMonitoring) validate() (bool, []error) {
   333  	v := newValidator()
   334  	if h != nil && !h.Disable {
   335  		if h.Options.Heapster.Replicas <= 0 {
   336  			v.addError(fmt.Errorf("Heapster replicas %d is not valid, must be greater than 0", h.Options.HeapsterReplicas))
   337  		}
   338  		if !util.Contains(h.Options.Heapster.ServiceType, serviceTypes()) {
   339  			v.addError(fmt.Errorf("Heapster Service Type %q is not a valid option %v", h.Options.Heapster.ServiceType, serviceTypes()))
   340  		}
   341  	}
   342  	return v.valid()
   343  }
   344  
   345  func (d *Dashboard) validate() (bool, []error) {
   346  	v := newValidator()
   347  	if d != nil && !d.Disable {
   348  		if !util.Contains(d.Options.ServiceType, serviceTypes()) {
   349  			v.addError(fmt.Errorf("Dashboard Service Type %q is not a valid option %v", d.Options.ServiceType, serviceTypes()))
   350  		}
   351  		if d.Options.NodePort != "" && d.Options.ServiceType != "NodePort" {
   352  			v.addError(fmt.Errorf("Dashboard Node Port option can only be used with Service Type 'NodePort'"))
   353  		}
   354  	}
   355  	return v.valid()
   356  }
   357  
   358  func (p *PackageManager) validate() (bool, []error) {
   359  	v := newValidator()
   360  	if !p.Disable {
   361  		if !util.Contains(p.Provider, packageManagerProviders()) {
   362  			v.addError(fmt.Errorf("Package Manager %q is not a valid option %v", p.Provider, packageManagerProviders()))
   363  		}
   364  	}
   365  	return v.valid()
   366  }
   367  
   368  // validate SSH access to the nodes
   369  func (s sshConnectionSet) validate() (bool, []error) {
   370  	v := newValidator()
   371  
   372  	err := ssh.ValidUnencryptedPrivateKey(s.SSHConfig.Key)
   373  	if err != nil {
   374  		v.addError(fmt.Errorf("SSH key validation error: %v", err))
   375  	} else {
   376  		var wg sync.WaitGroup
   377  		errQueue := make(chan error, len(s.Nodes))
   378  		// number of nodes
   379  		wg.Add(len(s.Nodes))
   380  		for _, node := range s.Nodes {
   381  			go func(ip string) {
   382  				defer wg.Done()
   383  				sshErr := ssh.TestConnection(ip, s.SSHConfig.Port, s.SSHConfig.User, s.SSHConfig.Key)
   384  				// Need to send something the buffered channel
   385  				if sshErr != nil {
   386  					errQueue <- fmt.Errorf("SSH connectivity validation failed for %q: %v", ip, sshErr)
   387  				} else {
   388  					errQueue <- nil
   389  				}
   390  			}(node.IP)
   391  		}
   392  
   393  		// Wait for all nodes to complete, then close channel
   394  		go func() {
   395  			wg.Wait()
   396  			close(errQueue)
   397  		}()
   398  
   399  		// Read any error
   400  		for err := range errQueue {
   401  			if err != nil {
   402  				v.addError(err)
   403  			}
   404  		}
   405  	}
   406  
   407  	return v.valid()
   408  }
   409  
   410  type nodeList struct {
   411  	Nodes []Node
   412  }
   413  
   414  func (nl nodeList) validate() (bool, []error) {
   415  	v := newValidator()
   416  	v.addError(validateNoDuplicateNodeInfo(nl.Nodes)...)
   417  	v.addError(validateKubeletOptionsDefinedOnce(nl.Nodes)...)
   418  	return v.valid()
   419  }
   420  
   421  func validateNoDuplicateNodeInfo(nodes []Node) []error {
   422  	errs := []error{}
   423  	hostnames := map[string]string{}
   424  	ips := map[string]string{}
   425  	internalIPs := map[string]string{}
   426  	for _, n := range nodes {
   427  		// Validate all hostnames are unique
   428  		if val, ok := hostnames[n.Host]; n.Host != "" && ok && val != n.HashCode() {
   429  			errs = append(errs, fmt.Errorf("Two different nodes cannot have the same hostname %q", n.Host))
   430  		} else if n.Host != "" {
   431  			hostnames[n.Host] = n.HashCode()
   432  		}
   433  		// Validate all IPs are unique
   434  		if val, ok := ips[n.IP]; n.IP != "" && ok && val != n.HashCode() {
   435  			errs = append(errs, fmt.Errorf("Two different nodes cannot have the same IP %q", n.IP))
   436  		} else if n.IP != "" {
   437  			ips[n.IP] = n.HashCode()
   438  		}
   439  		// Validate all internal IPs are unique
   440  		if val, ok := internalIPs[n.InternalIP]; n.InternalIP != "" && ok && val != n.HashCode() {
   441  			errs = append(errs, fmt.Errorf("Two different nodes cannot have the same internal IP %q", n.InternalIP))
   442  		} else if n.InternalIP != "" {
   443  			internalIPs[n.InternalIP] = n.HashCode()
   444  		}
   445  	}
   446  	return errs
   447  }
   448  
   449  func validateKubeletOptionsDefinedOnce(nodes []Node) []error {
   450  	errs := []error{}
   451  	seenNodes := map[string]map[string]string{}
   452  	for _, n := range nodes {
   453  		if val, ok := seenNodes[n.HashCode()]; ok && !reflect.DeepEqual(val, n.KubeletOptions.Overrides) {
   454  			errs = append(errs, fmt.Errorf("Cannot redefine kubelet options for node %q", n.Host))
   455  		} else {
   456  			seenNodes[n.HashCode()] = n.KubeletOptions.Overrides
   457  		}
   458  	}
   459  	return errs
   460  }
   461  
   462  func (ng *NodeGroup) validate() (bool, []error) {
   463  	v := newValidator()
   464  	if ng == nil || len(ng.Nodes) <= 0 {
   465  		v.addError(fmt.Errorf("At least one node is required"))
   466  	}
   467  	if ng.ExpectedCount <= 0 {
   468  		v.addError(fmt.Errorf("Node count must be greater than 0"))
   469  	}
   470  	if len(ng.Nodes) != ng.ExpectedCount && (len(ng.Nodes) > 0 && ng.ExpectedCount > 0) {
   471  		v.addError(fmt.Errorf("Expected node count (%d) does not match the number of nodes provided (%d)", ng.ExpectedCount, len(ng.Nodes)))
   472  	}
   473  	for i, n := range ng.Nodes {
   474  		v.validateWithErrPrefix(fmt.Sprintf("Node #%d", i+1), &n)
   475  	}
   476  
   477  	return v.valid()
   478  }
   479  
   480  // In order to make this node group optional, we consider it to be valid if:
   481  // - it's nil
   482  // - the number of nodes is zero, and the expected count is zero
   483  // We eagerly test the mismatch between given and expected node counts
   484  // because otherwise the regular NodeGroup validation returns confusing errors.
   485  func (ong *OptionalNodeGroup) validate() (bool, []error) {
   486  	if ong == nil {
   487  		return true, nil
   488  	}
   489  	if len(ong.Nodes) == 0 && ong.ExpectedCount == 0 {
   490  		return true, nil
   491  	}
   492  	if len(ong.Nodes) != ong.ExpectedCount {
   493  		return false, []error{fmt.Errorf("Expected node count (%d) does not match the number of nodes provided (%d)", ong.ExpectedCount, len(ong.Nodes))}
   494  	}
   495  	ng := NodeGroup(*ong)
   496  	return ng.validate()
   497  }
   498  
   499  func (mng *MasterNodeGroup) validate() (bool, []error) {
   500  	v := newValidator()
   501  
   502  	if len(mng.Nodes) <= 0 {
   503  		v.addError(fmt.Errorf("At least one node is required"))
   504  	}
   505  	if mng.ExpectedCount <= 0 {
   506  		v.addError(fmt.Errorf("Node count must be greater than 0"))
   507  	}
   508  	if len(mng.Nodes) != mng.ExpectedCount && (len(mng.Nodes) > 0 && mng.ExpectedCount > 0) {
   509  		v.addError(fmt.Errorf("Expected node count (%d) does not match the number of nodes provided (%d)", mng.ExpectedCount, len(mng.Nodes)))
   510  	}
   511  	for i, n := range mng.Nodes {
   512  		v.validateWithErrPrefix(fmt.Sprintf("Node #%d", i+1), &n)
   513  	}
   514  
   515  	if mng.LoadBalancedFQDN == "" {
   516  		v.addError(fmt.Errorf("Load balanced FQDN is required"))
   517  	}
   518  
   519  	if mng.LoadBalancedShortName == "" {
   520  		v.addError(fmt.Errorf("Load balanced shortname is required"))
   521  	}
   522  
   523  	return v.valid()
   524  }
   525  
   526  func (n *Node) validate() (bool, []error) {
   527  	v := newValidator()
   528  	if n.Host == "" {
   529  		v.addError(fmt.Errorf("Node host field is required"))
   530  	}
   531  	if n.IP == "" {
   532  		v.addError(fmt.Errorf("Node IP field is required"))
   533  	}
   534  	if ip := net.ParseIP(n.IP); ip == nil && n.IP != "" {
   535  		v.addError(fmt.Errorf("Invalid IP provided"))
   536  	}
   537  	if ip := net.ParseIP(n.InternalIP); n.InternalIP != "" && ip == nil {
   538  		v.addError(fmt.Errorf("Invalid InternalIP provided"))
   539  	}
   540  	// Validate node labels don't start with 'kismatic/' as that is reserved
   541  	for key, val := range n.Labels {
   542  		if strings.HasPrefix(key, "kismatic/") {
   543  			v.addError(fmt.Errorf("Node label %q cannot start with 'kismatic/'", key))
   544  		}
   545  		errs := validation.IsQualifiedName(key)
   546  		for _, err := range errs {
   547  			v.addError(fmt.Errorf("Node label name %q is not valid %s", key, err))
   548  		}
   549  		errs = validation.IsValidLabelValue(val)
   550  		for _, err := range errs {
   551  			v.addError(fmt.Errorf("Node label %q is not valid %s", val, err))
   552  		}
   553  	}
   554  	// Validate node taints don't start with 'kismatic/' as that is reserved
   555  	// Don't validate effects as those will likely change
   556  	for _, taint := range n.Taints {
   557  		if strings.HasPrefix(taint.Key, "kismatic/") {
   558  			v.addError(fmt.Errorf("Node taint %q cannot start with 'kismatic/'", taint.Key))
   559  		}
   560  		errs := validation.IsQualifiedName(taint.Key)
   561  		for _, err := range errs {
   562  			v.addError(fmt.Errorf("Node taint name %q is not valid %s", taint.Key, err))
   563  		}
   564  		errs = validation.IsValidLabelValue(taint.Value)
   565  		for _, err := range errs {
   566  			v.addError(fmt.Errorf("Node taint %q is not valid %s", taint.Value, err))
   567  		}
   568  		if !util.Contains(taint.Effect, taintEffects()) {
   569  			v.addError(fmt.Errorf("Node taint effect %q is not valid. Valid effects are: %v", taint.Effect, taintEffects()))
   570  		}
   571  	}
   572  	return v.valid()
   573  }
   574  
   575  func (dr *DockerRegistry) validate() (bool, []error) {
   576  	v := newValidator()
   577  	if (dr.Server == "" && dr.Address == "") && (dr.CAPath != "") {
   578  		v.addError(fmt.Errorf("Docker Registry server cannot be empty when CA is provided"))
   579  	}
   580  	if (dr.Server == "" && dr.Address == "") && (dr.Username != "") {
   581  		v.addError(fmt.Errorf("Docker Registry server cannot be empty when a username is provided"))
   582  	}
   583  	if _, err := os.Stat(dr.CAPath); dr.CAPath != "" && os.IsNotExist(err) {
   584  		v.addError(fmt.Errorf("Docker Registry CA file was not found at %q", dr.CAPath))
   585  	}
   586  	if dr.Username != "" && dr.Password == "" {
   587  		v.addError(fmt.Errorf("Docker Registry password cannot be blank for username %q", dr.Username))
   588  	}
   589  	if dr.Password != "" && dr.Username == "" {
   590  		v.addError(fmt.Errorf("Docker Registry username cannot be blank when a password is provided"))
   591  	}
   592  	return v.valid()
   593  }
   594  
   595  func (d Docker) validate() (bool, []error) {
   596  	v := newValidator()
   597  	v.validateWithErrPrefix("Storage", d.Storage)
   598  	return v.valid()
   599  }
   600  
   601  func (ds DockerStorage) validate() (bool, []error) {
   602  	v := newValidator()
   603  	v.validateWithErrPrefix("Direct LVM", ds.DirectLVM)
   604  	if ds.DirectLVMBlockDevice.Path != "" && ds.Driver != "devicemapper" {
   605  		v.addError(errors.New("DirectLVMBlockDevice Path can only be used with 'devicemapper' storage driver"))
   606  	}
   607  	if ds.DirectLVMBlockDevice.Path != "" && !filepath.IsAbs(ds.DirectLVMBlockDevice.Path) {
   608  		v.addError(errors.New("DirectLVMBlockDevice Path must be absolute"))
   609  	}
   610  	return v.valid()
   611  }
   612  
   613  func (dlvm *DockerStorageDirectLVMDeprecated) validate() (bool, []error) {
   614  	v := newValidator()
   615  	if dlvm != nil && dlvm.Enabled {
   616  		if dlvm.BlockDevice == "" {
   617  			v.addError(errors.New("DirectLVM is enabled, but no block device was specified"))
   618  		}
   619  		if !filepath.IsAbs(dlvm.BlockDevice) {
   620  			v.addError(errors.New("Path to the block device must be absolute"))
   621  		}
   622  	}
   623  	return v.valid()
   624  }
   625  
   626  func (nfs *NFS) validate() (bool, []error) {
   627  	v := newValidator()
   628  	if nfs == nil {
   629  		return v.valid()
   630  	}
   631  	uniqueVolumes := make(map[NFSVolume]bool)
   632  	for _, vol := range nfs.Volumes {
   633  		v.validate(vol)
   634  		if _, ok := uniqueVolumes[vol]; ok {
   635  			v.addError(fmt.Errorf("Duplicate NFS volume %v", vol))
   636  		} else {
   637  			uniqueVolumes[vol] = true
   638  		}
   639  	}
   640  	return v.valid()
   641  }
   642  
   643  func (nfsVol NFSVolume) validate() (bool, []error) {
   644  	v := newValidator()
   645  	if nfsVol.Host == "" {
   646  		v.addError(errors.New("NFS volume host cannot be empty"))
   647  	}
   648  	if nfsVol.Path == "" {
   649  		v.addError(errors.New("NFS volume path cannot be empty"))
   650  	}
   651  	if len(nfsVol.Path) > 0 && nfsVol.Path[0] != '/' {
   652  		v.addError(errors.New("NFS volume path must be absolute"))
   653  	}
   654  	return v.valid()
   655  }
   656  
   657  func (sv StorageVolume) validate() (bool, []error) {
   658  	v := newValidator()
   659  	notAllowed := ": / \\ & < > |"
   660  	if strings.ContainsAny(sv.Name, notAllowed) {
   661  		v.addError(fmt.Errorf("Volume name may not contain spaces or any of the following characters: %q", notAllowed))
   662  	}
   663  	if sv.SizeGB < 1 {
   664  		v.addError(errors.New("Volume size must be 1GB or larger"))
   665  	}
   666  	if sv.DistributionCount < 1 {
   667  		v.addError(errors.New("Distribution count must be greater than zero"))
   668  	}
   669  	if sv.ReplicateCount < 1 {
   670  		v.addError(errors.New("Replication count must be greater than zero"))
   671  	}
   672  	for _, a := range sv.AllowAddresses {
   673  		if ok := validateAllowedAddress(a); !ok {
   674  			v.addError(fmt.Errorf("Invalid address %q in the list of allowed addresses", a))
   675  		}
   676  	}
   677  	reclaimPolicies := []string{"Retain", "Recycle", "Delete"} // API is case-sensitive
   678  	if !util.Contains(sv.ReclaimPolicy, reclaimPolicies) {
   679  		v.addError(fmt.Errorf("%q is not a valid reclaim policy. Valid reclaim policies are: %v", sv.ReclaimPolicy, reclaimPolicies))
   680  	}
   681  
   682  	if len(sv.AccessModes) < 1 {
   683  		v.addError(errors.New("Access mode was not provided"))
   684  	}
   685  
   686  	accessModes := []string{"ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany"} // API is case-sensitive
   687  	for _, m := range sv.AccessModes {
   688  		if !util.Contains(m, accessModes) {
   689  			v.addError(fmt.Errorf("%q is not a valid access mode. Valid access modes are: %v", m, accessModes))
   690  		}
   691  	}
   692  	return v.valid()
   693  }
   694  
   695  func validateAllowedAddress(address string) bool {
   696  	// First, validate that there are four octets with 1, 2 or 3 chars, separated by dots
   697  	r := regexp.MustCompile(`^[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}$`)
   698  	if !r.MatchString(address) {
   699  		return false
   700  	}
   701  	// Validate each octet on its own
   702  	oct := strings.Split(address, ".")
   703  	for _, o := range oct {
   704  		// Valid if the octet is a wildcard, or if it's a number between 0-255 (inclusive)
   705  		n, err := strconv.Atoi(o)
   706  		valid := o == "*" || (err == nil && 0 <= n && n <= 255)
   707  		if !valid {
   708  			return false
   709  		}
   710  	}
   711  	return true
   712  }