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 }