1package manifest 2 3import ( 4 "net" 5 "regexp" 6 "strings" 7 8 bosherr "github.com/cloudfoundry/bosh-utils/errors" 9 boshlog "github.com/cloudfoundry/bosh-utils/logger" 10 11 binet "github.com/cloudfoundry/bosh-cli/common/net" 12 boshinst "github.com/cloudfoundry/bosh-cli/installation" 13 birelsetmanifest "github.com/cloudfoundry/bosh-cli/release/set/manifest" 14) 15 16type Validator interface { 17 Validate(Manifest, birelsetmanifest.Manifest) error 18 ValidateReleaseJobs(Manifest, boshinst.ReleaseManager) error 19} 20 21type validator struct { 22 logger boshlog.Logger 23} 24 25func NewValidator(logger boshlog.Logger) Validator { 26 return &validator{ 27 logger: logger, 28 } 29} 30 31func (v *validator) Validate(deploymentManifest Manifest, releaseSetManifest birelsetmanifest.Manifest) error { 32 errs := []error{} 33 if v.isBlank(deploymentManifest.Name) { 34 errs = append(errs, bosherr.Error("name must be provided")) 35 } 36 37 networksErrors := v.validateNetworks(deploymentManifest.Networks) 38 errs = append(errs, networksErrors...) 39 40 for idx, resourcePool := range deploymentManifest.ResourcePools { 41 if v.isBlank(resourcePool.Name) { 42 errs = append(errs, bosherr.Errorf("resource_pools[%d].name must be provided", idx)) 43 } 44 if v.isBlank(resourcePool.Network) { 45 errs = append(errs, bosherr.Errorf("resource_pools[%d].network must be provided", idx)) 46 } else if _, ok := v.networkNames(deploymentManifest)[resourcePool.Network]; !ok { 47 errs = append(errs, bosherr.Errorf("resource_pools[%d].network must be the name of a network", idx)) 48 } 49 50 if v.isBlank(resourcePool.Stemcell.URL) { 51 errs = append(errs, bosherr.Errorf("resource_pools[%d].stemcell.url must be provided", idx)) 52 } 53 54 matched, err := regexp.MatchString("^(file|http|https)://", resourcePool.Stemcell.URL) 55 if err != nil || !matched { 56 errs = append(errs, bosherr.Errorf("resource_pools[%d].stemcell.url must be a valid URL (file:// or http(s)://)", idx)) 57 } 58 59 if strings.HasPrefix(resourcePool.Stemcell.URL, "http") && v.isBlank(resourcePool.Stemcell.SHA1) { 60 errs = append(errs, bosherr.Errorf("resource_pools[%d].stemcell.sha1 must be provided for http URL", idx)) 61 } 62 } 63 64 for idx, diskPool := range deploymentManifest.DiskPools { 65 if v.isBlank(diskPool.Name) { 66 errs = append(errs, bosherr.Errorf("disk_pools[%d].name must be provided", idx)) 67 } 68 if diskPool.DiskSize <= 0 { 69 errs = append(errs, bosherr.Errorf("disk_pools[%d].disk_size must be > 0", idx)) 70 } 71 } 72 73 if len(deploymentManifest.Jobs) > 1 { 74 errs = append(errs, bosherr.Error("jobs must be of size 1")) 75 } 76 77 for idx, job := range deploymentManifest.Jobs { 78 if v.isBlank(job.Name) { 79 errs = append(errs, bosherr.Errorf("jobs[%d].name must be provided", idx)) 80 } 81 if job.PersistentDisk < 0 { 82 errs = append(errs, bosherr.Errorf("jobs[%d].persistent_disk must be >= 0", idx)) 83 } 84 if job.PersistentDiskPool != "" { 85 if _, ok := v.diskPoolNames(deploymentManifest)[job.PersistentDiskPool]; !ok { 86 errs = append(errs, bosherr.Errorf("jobs[%d].persistent_disk_pool must be the name of a disk pool", idx)) 87 } 88 } 89 if job.Instances < 0 { 90 errs = append(errs, bosherr.Errorf("jobs[%d].instances must be >= 0", idx)) 91 } 92 if len(job.Networks) == 0 { 93 errs = append(errs, bosherr.Errorf("jobs[%d].networks must be a non-empty array", idx)) 94 } 95 if v.isBlank(job.ResourcePool) { 96 errs = append(errs, bosherr.Errorf("jobs[%d].resource_pool must be provided", idx)) 97 } else { 98 if _, ok := v.resourcePoolNames(deploymentManifest)[job.ResourcePool]; !ok { 99 errs = append(errs, bosherr.Errorf("jobs[%d].resource_pool must be the name of a resource pool", idx)) 100 } 101 } 102 103 errs = append(errs, v.validateJobNetworks(job.Networks, deploymentManifest.Networks, idx)...) 104 105 if job.Lifecycle != "" && job.Lifecycle != JobLifecycleService { 106 errs = append(errs, bosherr.Errorf("jobs[%d].lifecycle must be 'service' ('%s' not supported)", idx, job.Lifecycle)) 107 } 108 109 templateNames := map[string]struct{}{} 110 for templateIdx, template := range job.Templates { 111 if v.isBlank(template.Name) { 112 errs = append(errs, bosherr.Errorf("jobs[%d].templates[%d].name must be provided", idx, templateIdx)) 113 } 114 if _, found := templateNames[template.Name]; found { 115 errs = append(errs, bosherr.Errorf("jobs[%d].templates[%d].name '%s' must be unique", idx, templateIdx, template.Name)) 116 } 117 templateNames[template.Name] = struct{}{} 118 119 if v.isBlank(template.Release) { 120 errs = append(errs, bosherr.Errorf("jobs[%d].templates[%d].release must be provided", idx, templateIdx)) 121 } else { 122 _, found := releaseSetManifest.FindByName(template.Release) 123 if !found { 124 errs = append(errs, bosherr.Errorf("jobs[%d].templates[%d].release '%s' must refer to release in releases", idx, templateIdx, template.Release)) 125 } 126 } 127 } 128 } 129 130 if len(errs) > 0 { 131 return bosherr.NewMultiError(errs...) 132 } 133 134 return nil 135} 136 137func (v *validator) ValidateReleaseJobs(deploymentManifest Manifest, releaseManager boshinst.ReleaseManager) error { 138 errs := []error{} 139 140 for idx, job := range deploymentManifest.Jobs { 141 for templateIdx, template := range job.Templates { 142 release, found := releaseManager.Find(template.Release) 143 if !found { 144 errs = append(errs, bosherr.Errorf("jobs[%d].templates[%d].release '%s' must refer to release in releases", idx, templateIdx, template.Release)) 145 } else { 146 _, found := release.FindJobByName(template.Name) 147 if !found { 148 errs = append(errs, bosherr.Errorf("jobs[%d].templates[%d] must refer to a job in '%s', but there is no job named '%s'", idx, templateIdx, release.Name(), template.Name)) 149 } 150 } 151 } 152 } 153 154 if len(errs) > 0 { 155 return bosherr.NewMultiError(errs...) 156 } 157 158 return nil 159} 160 161func (v *validator) isBlank(str string) bool { 162 return str == "" || strings.TrimSpace(str) == "" 163} 164 165func (v *validator) networkNames(deploymentManifest Manifest) map[string]struct{} { 166 names := make(map[string]struct{}) 167 for _, network := range deploymentManifest.Networks { 168 names[network.Name] = struct{}{} 169 } 170 return names 171} 172 173func (v *validator) diskPoolNames(deploymentManifest Manifest) map[string]struct{} { 174 names := make(map[string]struct{}) 175 for _, diskPool := range deploymentManifest.DiskPools { 176 names[diskPool.Name] = struct{}{} 177 } 178 return names 179} 180 181func (v *validator) resourcePoolNames(deploymentManifest Manifest) map[string]struct{} { 182 names := make(map[string]struct{}) 183 for _, resourcePool := range deploymentManifest.ResourcePools { 184 names[resourcePool.Name] = struct{}{} 185 } 186 return names 187} 188 189func (v *validator) isValidIP(ip string) bool { 190 parsedIP := net.ParseIP(ip) 191 return parsedIP != nil 192} 193 194type maybeIPNet interface { 195 Try(func(*net.IPNet) error) error 196} 197 198type nothingIpNet struct{} 199 200func (in *nothingIpNet) Try(fn func(*net.IPNet) error) error { 201 return nil 202} 203 204type somethingIpNet struct { 205 ipNet *net.IPNet 206} 207 208func (in *somethingIpNet) Try(fn func(*net.IPNet) error) error { 209 return fn(in.ipNet) 210} 211 212func (v *validator) validateRange(idx int, ipRange string) ([]error, maybeIPNet) { 213 if v.isBlank(ipRange) { 214 return []error{bosherr.Errorf("networks[%d].subnets[0].range must be provided", idx)}, ¬hingIpNet{} 215 } 216 217 _, ipNet, err := net.ParseCIDR(ipRange) 218 if err != nil { 219 return []error{bosherr.Errorf("networks[%d].subnets[0].range must be an ip range", idx)}, ¬hingIpNet{} 220 } 221 222 return []error{}, &somethingIpNet{ipNet: ipNet} 223} 224 225func (v *validator) validateNetworks(networks []Network) []error { 226 errs := []error{} 227 228 for idx, network := range networks { 229 networkErrors := v.validateNetwork(network, idx) 230 errs = append(errs, networkErrors...) 231 } 232 233 return errs 234} 235 236func (v *validator) validateNetwork(network Network, networkIdx int) []error { 237 errs := []error{} 238 239 if v.isBlank(network.Name) { 240 errs = append(errs, bosherr.Errorf("networks[%d].name must be provided", networkIdx)) 241 } 242 243 if network.Type != Dynamic && network.Type != Manual && network.Type != VIP { 244 errs = append(errs, bosherr.Errorf("networks[%d].type must be 'manual', 'dynamic', or 'vip'", networkIdx)) 245 } 246 247 if network.Type == Manual { 248 if len(network.Subnets) != 1 { 249 errs = append(errs, bosherr.Errorf("networks[%d].subnets must be of size 1", networkIdx)) 250 } else { 251 ipRange := network.Subnets[0].Range 252 rangeErrors, maybeIpNet := v.validateRange(networkIdx, ipRange) 253 errs = append(errs, rangeErrors...) 254 255 gateway := network.Subnets[0].Gateway 256 gatewayErrors := v.validateGateway(networkIdx, gateway, maybeIpNet) 257 errs = append(errs, gatewayErrors...) 258 } 259 } 260 261 return errs 262} 263 264func (v *validator) validateJobNetworks(jobNetworks []JobNetwork, networks []Network, jobIdx int) []error { 265 errs := []error{} 266 defaultCounts := make(map[NetworkDefault]int) 267 268 for networkIdx, jobNetwork := range jobNetworks { 269 if v.isBlank(jobNetwork.Name) { 270 errs = append(errs, bosherr.Errorf("jobs[%d].networks[%d].name must be provided", jobIdx, networkIdx)) 271 } 272 273 var matchingNetwork Network 274 275 found := false 276 277 for _, network := range networks { 278 if network.Name == jobNetwork.Name { 279 found = true 280 matchingNetwork = network 281 } 282 } 283 284 if !found { 285 errs = append(errs, bosherr.Errorf("jobs[%d].networks[%d] not found in networks", jobIdx, networkIdx)) 286 } 287 288 for ipIdx, ip := range jobNetwork.StaticIPs { 289 staticIPErrors := v.validateStaticIP(ip, jobNetwork, matchingNetwork, jobIdx, networkIdx, ipIdx) 290 errs = append(errs, staticIPErrors...) 291 } 292 293 for defaultIdx, value := range jobNetwork.Defaults { 294 if value != NetworkDefaultDNS && value != NetworkDefaultGateway { 295 errs = append(errs, bosherr.Errorf("jobs[%d].networks[%d].default[%d] must be 'dns' or 'gateway'", jobIdx, networkIdx, defaultIdx)) 296 } 297 } 298 299 for _, dflt := range jobNetwork.Defaults { 300 count, present := defaultCounts[dflt] 301 if present { 302 defaultCounts[dflt] = count + 1 303 } else { 304 defaultCounts[dflt] = 1 305 } 306 } 307 } 308 309 for _, dflt := range []NetworkDefault{"dns", "gateway"} { 310 count, found := defaultCounts[dflt] 311 if len(jobNetworks) > 1 && !found { 312 errs = append(errs, bosherr.Errorf("with multiple networks, a default for '%s' must be specified", dflt)) 313 } else if count > 1 { 314 errs = append(errs, bosherr.Errorf("only one network can be the default for '%s'", dflt)) 315 } 316 } 317 318 return errs 319} 320 321func (v *validator) validateStaticIP(ip string, jobNetwork JobNetwork, network Network, jobIdx, networkIdx, ipIdx int) []error { 322 if !v.isValidIP(ip) { 323 return []error{bosherr.Errorf("jobs[%d].networks[%d].static_ips[%d] must be a valid IP", jobIdx, networkIdx, ipIdx)} 324 } 325 326 if network.Type != Manual { 327 return []error{} 328 } 329 330 foundInSubnetRange := false 331 for _, subnet := range network.Subnets { 332 _, rangeNet, err := net.ParseCIDR(subnet.Range) 333 if err == nil && rangeNet.Contains(net.ParseIP(ip)) { 334 foundInSubnetRange = true 335 } 336 } 337 338 if foundInSubnetRange { 339 return []error{} 340 } 341 342 return []error{bosherr.Errorf("jobs[%d].networks[%d] static ip '%s' must be within subnet range", jobIdx, networkIdx, ip)} 343} 344 345func (v *validator) validateGateway(idx int, gateway string, ipNet maybeIPNet) []error { 346 if v.isBlank(gateway) { 347 return []error{bosherr.Errorf("networks[%d].subnets[0].gateway must be provided", idx)} 348 } 349 350 errors := []error{} 351 352 _ = ipNet.Try(func(ipNet *net.IPNet) error { 353 gatewayIp := net.ParseIP(gateway) 354 if gatewayIp == nil { 355 errors = append(errors, bosherr.Errorf("networks[%d].subnets[0].gateway must be an ip", idx)) 356 } 357 358 if !ipNet.Contains(gatewayIp) { 359 errors = append(errors, bosherr.Errorf("subnet gateway '%s' must be within the specified range '%s'", gateway, ipNet)) 360 } 361 362 if ipNet.IP.Equal(gatewayIp) { 363 errors = append(errors, bosherr.Errorf("subnet gateway can't be the network address '%s'", gatewayIp)) 364 } 365 366 if binet.LastAddress(ipNet).Equal(gatewayIp) { 367 errors = append(errors, bosherr.Errorf("subnet gateway can't be the broadcast address '%s'", gatewayIp)) 368 } 369 370 return nil 371 }) 372 373 return errors 374} 375