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)}, &nothingIpNet{}
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)}, &nothingIpNet{}
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