guard-bosh 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 51ca6f0f55880bc7db9d36d693036a5626b228a0
4
- data.tar.gz: 269d78e98809510e93ae63d48da0809f8e567270
3
+ metadata.gz: 55e6d1e34e436b7f4669fada4b65d4681947b44b
4
+ data.tar.gz: 6249a5b3b822a446e533fd0afc2cd4861f62ce70
5
5
  SHA512:
6
- metadata.gz: 3a0ba80730401e7a0f7ec1d13c0fba3464fdea1c5c96f6f839bcbfa4f179103271a658bbb8d8f96c9f25b9fa2ced000d4ab50534ca0e72e54d555776f8c0cd5d
7
- data.tar.gz: 96ab6eec2791ca84dc582eae3dece52d7a749f835f4646a3f7c3904cee6f2fed76f6a2c6bc0cec06eeade259af44814cd873a8030efcb7f8a887e26d5bb6412f
6
+ metadata.gz: 8f9f84431e819e25dcfa8676b6c47a4e8b728795539f1337b60f01c2372572bc8b3383bc99c972aea5b469d004d09c59d0813c99a4e977fd1a31e2b8b632d26b
7
+ data.tar.gz: 40deae949545a9a4fda3327835509d2c921808a1826e47e48a5adc7e1545f03572711d0e5238ba1b869232399747a483189e5e2a498197fea981fa9fa29da939
@@ -1,6 +1,9 @@
1
1
  require 'guard/compat/plugin'
2
+ require 'pathname'
3
+ require 'yaml'
2
4
 
3
5
  module Guard
6
+ # Guard BOSH Plugin
4
7
  class Bosh < Plugin
5
8
  require 'pathname'
6
9
 
@@ -40,6 +43,7 @@ module Guard
40
43
  notify_errors(errors)
41
44
  end
42
45
 
46
+ # rubocop:disable Metrics/MethodLength
43
47
  def run_on_modifications(paths)
44
48
  change_scope, job_name = @change_assessor.determine_scope(paths)
45
49
  errors = case change_scope
@@ -1,13 +1,15 @@
1
- require 'ipaddr'
2
-
3
1
  module Guard
4
2
  class Bosh
3
+ # Simulated BOSH Apply Spec.
5
4
  class ApplySpecification
6
- def initialize(deployment_manifest:, package_resolver:)
5
+ def initialize(
6
+ deployment_manifest:, package_resolver:, network_generator:)
7
7
  @manifest = deployment_manifest
8
8
  @package_resolver = package_resolver
9
+ @network_generator = network_generator
9
10
  end
10
11
 
12
+ # rubocop:disable Metrics/MethodLength
11
13
  def generate(properties:, job_name:)
12
14
  {
13
15
  'deployment' => @manifest['name'],
@@ -46,12 +48,12 @@ module Guard
46
48
  all_packages.inject({}) do |result, package|
47
49
  result.merge(
48
50
 
49
- package => {
50
- 'name' => package,
51
- 'version' => '1.0',
52
- 'sha1' => 'b945ce51b3635bb0ebfb2207323514381bcee824',
53
- 'blobstore_id' => '608c41bc-d491-4773-9812-8f24276eace1'
54
- }
51
+ package => {
52
+ 'name' => package,
53
+ 'version' => '1.0',
54
+ 'sha1' => 'b945ce51b3635bb0ebfb2207323514381bcee824',
55
+ 'blobstore_id' => '608c41bc-d491-4773-9812-8f24276eace1'
56
+ }
55
57
 
56
58
  )
57
59
  end
@@ -79,40 +81,8 @@ module Guard
79
81
  end
80
82
 
81
83
  def network(job_name)
82
- manifest_job = manifest_job_with_name(job_name)
83
- job_network = manifest_job['networks'].first
84
- network_definition = @manifest['networks'].find do |n|
85
- n['name'] == job_network['name']
86
- end
87
- {
88
- job_network['name'] => {
89
- 'cloud_properties' => network_definition['subnets'].first['cloud_properties'],
90
- 'dns_record_name' => dns_record_name(
91
- job_name, job_network['name'], @manifest['name']),
92
- 'ip' => ip_address(job_network, network_definition),
93
- 'netmask' => netmask(network_definition['subnets'].first['range']),
94
- 'default' => %w(dns gateway)
95
- }
96
- }
97
- end
98
-
99
- def ip_address(job_network, network_definition)
100
- if job_network.key?('static_ips') &&
101
- Array(job_network['static_ips']).any?
102
- job_network['static_ips'].first
103
- else
104
- # We could be better here and calculate the dynamic address properly
105
- network_definition['subnets'].first['range'].split('/').first
106
- end
107
- end
108
-
109
- def dns_record_name(job, network, deployment)
110
- "0.#{job}.#{network}.#{deployment}.bosh".gsub('_', '-')
111
- end
112
-
113
- def netmask(range)
114
- cidr = range.split('/').last
115
- IPAddr.new('255.255.255.255').mask(cidr).to_s
84
+ @network_generator.generate(
85
+ deployment_manifest: @manifest, job_name: job_name)
116
86
  end
117
87
 
118
88
  def resource_pool(job_name)
@@ -1,5 +1,9 @@
1
+ require 'pathname'
2
+
1
3
  module Guard
2
4
  class Bosh
5
+ # Determines the impact of a code change to enable a subset of files to be
6
+ # re-evaluated.
3
7
  class ChangeAssessor
4
8
  def initialize(deployment_manifest)
5
9
  @deployment_manifest = deployment_manifest
@@ -9,11 +13,7 @@ module Guard
9
13
  paths = raw_paths.map { |p| Pathname.new(p) }
10
14
  return :all if paths.include?(@deployment_manifest)
11
15
 
12
- jobs = paths.select { |p| spec_path?(p) }.map do |p|
13
- p.dirname.basename.to_s
14
- end
15
-
16
- spec_scope = scope(jobs, :all_templates_for_job)
16
+ spec_scope = scope(jobs_for_paths(paths), :all_templates_for_job)
17
17
  return spec_scope if spec_scope
18
18
 
19
19
  jobs = paths.select { |p| template_path?(p) }.map do |t|
@@ -27,6 +27,12 @@ module Guard
27
27
 
28
28
  private
29
29
 
30
+ def jobs_for_paths(paths)
31
+ paths.select { |p| spec_path?(p) }.map do |p|
32
+ p.dirname.basename.to_s
33
+ end
34
+ end
35
+
30
36
  def spec_path?(path)
31
37
  path.basename.to_s == 'spec'
32
38
  end
@@ -2,6 +2,10 @@ require 'deep_merge'
2
2
 
3
3
  module Guard
4
4
  class Bosh
5
+ # The effective set of properties is the union of:
6
+ # * The default properties declared in the job spec
7
+ # * The properties declared at the top-level of the manifest
8
+ # * The properties declared at the job-level of the manifest
5
9
  class EffectivePropertiesCalculator
6
10
  def initialize(loaders:)
7
11
  @loaders = loaders
@@ -1,5 +1,7 @@
1
1
  module Guard
2
2
  class Bosh
3
+ # Properties defined at the top-level of the manifest. These may be shared
4
+ # by multiple BOSH jobs.
3
5
  class GlobalPropertiesLoader
4
6
  def initialize(deployment_manifest:)
5
7
  @deployment_manifest = deployment_manifest
@@ -2,6 +2,7 @@ require 'yaml'
2
2
 
3
3
  module Guard
4
4
  class Bosh
5
+ # The property defaults (if any) defined for a BOSH job in the job spec.
5
6
  class JobDefaultPropertiesLoader
6
7
  def initialize(release_dir:)
7
8
  @release_dir = release_dir
@@ -1,5 +1,6 @@
1
1
  module Guard
2
2
  class Bosh
3
+ # The properties defined in the manifest at the job level.
3
4
  class JobPropertiesLoader
4
5
  def initialize(deployment_manifest:)
5
6
  @manifest = deployment_manifest
@@ -1,7 +1,9 @@
1
1
  require 'pathname'
2
+ require 'yaml'
2
3
 
3
4
  module Guard
4
5
  class Bosh
6
+ # Wraps access to manifest jobs and templates
5
7
  class JobRepository
6
8
  def initialize(deployment_manifest)
7
9
  @manifest = deployment_manifest
@@ -0,0 +1,57 @@
1
+ require 'ipaddr'
2
+
3
+ module Guard
4
+ class Bosh
5
+ # Generates a simulated network section for the apply spec
6
+ class NetworkGenerator
7
+ # rubocop:disable Metrics/MethodLength
8
+ def generate(deployment_manifest:, job_name:)
9
+ job_network, network_definition =
10
+ manifest_sections(deployment_manifest, job_name)
11
+ {
12
+ job_network['name'] => {
13
+ 'cloud_properties' => network_definition['subnets'].first[
14
+ 'cloud_properties'],
15
+ 'dns_record_name' => dns_record_name(
16
+ job_name, job_network['name'], deployment_manifest['name']),
17
+ 'ip' => ip_address(job_network, network_definition),
18
+ 'netmask' => netmask(network_definition['subnets'].first['range']),
19
+ 'default' => %w(dns gateway)
20
+ }
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ def manifest_sections(deployment_manifest, job_name)
27
+ manifest_job = deployment_manifest['jobs'].find do |job|
28
+ job['name'] == job_name
29
+ end
30
+ job_network = manifest_job['networks'].first
31
+ network_definition = deployment_manifest['networks'].find do |n|
32
+ n['name'] == job_network['name']
33
+ end
34
+ [job_network, network_definition]
35
+ end
36
+
37
+ def ip_address(job_network, network_definition)
38
+ if job_network.key?('static_ips') &&
39
+ Array(job_network['static_ips']).any?
40
+ job_network['static_ips'].first
41
+ else
42
+ # We could be better here and calculate the dynamic address properly
43
+ network_definition['subnets'].first['range'].split('/').first
44
+ end
45
+ end
46
+
47
+ def dns_record_name(job, network, deployment)
48
+ "0.#{job}.#{network}.#{deployment}.bosh".gsub('_', '-')
49
+ end
50
+
51
+ def netmask(range)
52
+ cidr = range.split('/').last
53
+ IPAddr.new('255.255.255.255').mask(cidr).to_s
54
+ end
55
+ end
56
+ end
57
+ end
@@ -2,18 +2,29 @@ require 'guard/compat'
2
2
 
3
3
  module Guard
4
4
  class Bosh
5
+ # Report success or failure to the user
5
6
  class Notifier
6
7
  def notify(errors)
7
8
  if errors.empty?
8
9
  Guard::Compat::UI.notify(
9
10
  'Succeeded', title: 'BOSH', image: :success, priority: -2)
10
11
  else
11
- Guard::Compat::UI.error(
12
- '%s: %s' % [errors.first[:template], errors.first[:detail]])
12
+ Guard::Compat::UI.error(error_line(errors))
13
13
  Guard::Compat::UI.notify(
14
14
  'Failed', title: 'BOSH', image: :failed, priority: 2)
15
15
  end
16
16
  end
17
+
18
+ private
19
+
20
+ def error_line(errors)
21
+ error = errors.first
22
+ [
23
+ error[:template],
24
+ error[:line] == :unknown ? '?' : error[:line],
25
+ " #{error[:detail]}"
26
+ ].join(':')
27
+ end
17
28
  end
18
29
  end
19
30
  end
@@ -1,5 +1,8 @@
1
+ require 'yaml'
2
+
1
3
  module Guard
2
4
  class Bosh
5
+ # Packages for a given BOSH job
3
6
  class PackageResolver
4
7
  def initialize(release_dir)
5
8
  @release_dir = release_dir
@@ -1,5 +1,16 @@
1
+ require 'guard/bosh/apply_specification'
2
+ require 'guard/bosh/effective_properties_calculator'
3
+ require 'guard/bosh/global_properties_loader'
4
+ require 'guard/bosh/job_default_properties_loader'
5
+ require 'guard/bosh/job_properties_loader'
6
+ require 'guard/bosh/network_generator'
7
+ require 'guard/bosh/package_resolver'
8
+ require 'guard/bosh/template_renderer'
9
+
1
10
  module Guard
2
11
  class Bosh
12
+ # Encapsulates building the apply spec, and rendering job templates against
13
+ # it to identify errors.
3
14
  class TemplateChecker
4
15
  def initialize(deployment_manifest:,
5
16
  properties_calculator:,
@@ -22,22 +33,30 @@ module Guard
22
33
  end
23
34
 
24
35
  def self.build(deployment_manifest:, release_dir:)
25
- properties_calculator = EffectivePropertiesCalculator.new(loaders: [
36
+ new(
37
+ deployment_manifest: deployment_manifest,
38
+ properties_calculator:
39
+ properties_calculator(deployment_manifest, release_dir),
40
+ apply_specification:
41
+ apply_specification(deployment_manifest, release_dir),
42
+ template_renderer: TemplateRenderer.new
43
+ )
44
+ end
45
+
46
+ def self.properties_calculator(deployment_manifest, release_dir)
47
+ EffectivePropertiesCalculator.new(loaders: [
26
48
  JobDefaultPropertiesLoader.new(release_dir: release_dir),
27
49
  GlobalPropertiesLoader.new(deployment_manifest: deployment_manifest),
28
50
  JobPropertiesLoader.new(deployment_manifest: deployment_manifest)
29
51
  ])
30
- apply_specification = ApplySpecification.new(
52
+ end
53
+
54
+ def self.apply_specification(deployment_manifest, release_dir)
55
+ ApplySpecification.new(
31
56
  deployment_manifest: deployment_manifest,
57
+ network_generator: NetworkGenerator.new,
32
58
  package_resolver: PackageResolver.new(release_dir)
33
59
  )
34
- template_renderer = TemplateRenderer.new
35
- new(
36
- deployment_manifest: deployment_manifest,
37
- properties_calculator: properties_calculator,
38
- apply_specification: apply_specification,
39
- template_renderer: template_renderer
40
- )
41
60
  end
42
61
  end
43
62
  end
@@ -4,37 +4,65 @@ require 'bosh/template/unknown_property'
4
4
 
5
5
  module Guard
6
6
  class Bosh
7
+ # Render a template with the provided context and report any errors.
7
8
  class TemplateRenderer
8
9
  def render(context:, template:)
9
10
  renderer = ::Bosh::Template::Renderer.new(
10
11
  context: JSON.generate(context))
11
12
 
12
- # The re-writing of messages we do here is intended to make the output
13
- # more readable, but may not be wise.
14
13
  begin
15
14
  renderer.render(template)
16
15
  { template: template, status: :success, detail: '' }
17
- rescue ::Bosh::Template::UnknownProperty => e
18
- error(template, "missing property: #{e.name}")
19
- rescue NoMethodError => e
20
- error(template, e.message.sub(/ for #<[^>]+>$/, ''))
21
- rescue NameError => e
22
- error(template, e.message.sub(/ for #<[^>]+>$/, ''))
23
- rescue SyntaxError => e
24
- error(template, e.message.split("\n").first.sub(
25
- /^\(erb\):[0-9]+: /, ''))
16
+ rescue StandardError, SyntaxError => e
17
+ generate_user_facing_error(template, e)
26
18
  end
27
19
  end
28
20
 
29
21
  private
30
22
 
31
- def error(template, message)
23
+ def generate_user_facing_error(template, ex)
24
+ case ex
25
+ when ::Bosh::Template::UnknownProperty
26
+ error(template, "missing property: #{ex.name}", line(ex))
27
+ when NoMethodError, NameError
28
+ error(template, remove_bosh_template(ex.message), line(ex))
29
+ when SyntaxError
30
+ context = find_erb_error(ex.message)
31
+ error(template, context['message'], context['line'].to_i)
32
+ end
33
+ end
34
+
35
+ def error(template, message, line)
32
36
  {
33
37
  template: template,
34
38
  status: :failure,
35
- detail: message
39
+ detail: message,
40
+ line: line
36
41
  }
37
42
  end
43
+
44
+ def line(error)
45
+ erb_error = find_erb_error(error.backtrace)
46
+ if erb_error.nil?
47
+ :unknown
48
+ else
49
+ erb_error['line'].to_i
50
+ end
51
+ end
52
+
53
+ ERB_LINE_PATTERN = /^\(erb\):(?<line>[0-9]+): ?(?<message>.*)/
54
+
55
+ def find_erb_error(error_lines)
56
+ # '(erb):4: syntax error, unexpected keyword_do_block'
57
+ Array(error_lines).lazy.grep(ERB_LINE_PATTERN) do |error_line|
58
+ ERB_LINE_PATTERN.match(error_line)
59
+ end.first
60
+ end
61
+
62
+ def remove_bosh_template(message)
63
+ # ' for #<Bosh::Template::EvaluationContext:0x00000000000000>'
64
+ message.sub(/ for #<Bosh::Template::[^>]+>$/, '')
65
+ end
38
66
  end
39
67
  end
40
68
  end
@@ -3,7 +3,7 @@ directories %w(jobs templates)
3
3
 
4
4
  manifest_path = 'path/to/manifest.yml'
5
5
  guard :bosh, deployment_manifest: manifest_path do
6
- watch(%r{^jobs/.*})
6
+ watch(/^jobs\/.*/)
7
7
  watch(manifest_path)
8
8
  end
9
9
 
@@ -1,11 +1,15 @@
1
1
  require 'guard/bosh/apply_specification'
2
+ require 'guard/bosh/network_generator'
3
+ require 'guard/bosh/package_resolver'
2
4
 
3
5
  describe Guard::Bosh::ApplySpecification do
4
- let(:package_resolver) { double }
6
+ let(:package_resolver) { instance_double(Guard::Bosh::PackageResolver) }
7
+ let(:network_generator) { instance_double(Guard::Bosh::NetworkGenerator) }
5
8
  subject do
6
9
  Guard::Bosh::ApplySpecification.new(
7
10
  deployment_manifest: manifest_stub,
8
- package_resolver: package_resolver
11
+ package_resolver: package_resolver,
12
+ network_generator: network_generator
9
13
  )
10
14
  end
11
15
 
@@ -15,26 +19,14 @@ describe Guard::Bosh::ApplySpecification do
15
19
  'jobs' => [{
16
20
  'instances' => 1,
17
21
  'name' => 'redis_leader_z1',
18
- 'networks' => [{ 'name' => 'redis1', 'static_ips' => ['203.0.113.2'] }],
19
22
  'persistent_disk' => 0,
20
23
  'resource_pool' => 'small_z1',
21
24
  'templates' => [{ 'name' => 'redis', 'release' => 'redis' }]
22
25
  }],
23
- 'networks' => [
24
- {
25
- 'name' => 'redis1',
26
- 'subnets' => [{
27
- 'cloud_properties' => { 'name' => 'iaas_network_name' },
28
- 'range' => '203.0.113.2/24',
29
- 'static' => ['10.244.2.2']
30
- }]
31
- }
32
- ],
33
26
  'resource_pools' => [
34
27
  {
35
28
  'cloud_properties' => { 'name' => 'random' },
36
29
  'name' => 'small_z1',
37
- 'network' => 'redis1',
38
30
  'size' => 3,
39
31
  'stemcell' => {
40
32
  'name' => 'bosh-warden-boshlite-ubuntu-trusty-go_agent',
@@ -46,7 +38,12 @@ describe Guard::Bosh::ApplySpecification do
46
38
  end
47
39
 
48
40
  before do
49
- allow(package_resolver).to receive(:resolve).with('redis').and_return(['redis'])
41
+ allow(package_resolver).to receive(:resolve).with(
42
+ 'redis').and_return(['redis'])
43
+ allow(network_generator).to receive(:generate).with(
44
+ deployment_manifest: manifest_stub,
45
+ job_name: 'redis_leader_z1'
46
+ )
50
47
  end
51
48
 
52
49
  it 'includes the effective properties provided' do
@@ -94,7 +91,8 @@ describe Guard::Bosh::ApplySpecification do
94
91
  'blobstore_id' => '608c41bc-d491-4773-9812-8f24276eace1'
95
92
  }
96
93
  }
97
- expect(package_resolver).to receive(:resolve).with('redis').and_return(['redis'])
94
+ expect(package_resolver).to receive(:resolve).with(
95
+ 'redis').and_return(['redis'])
98
96
  apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
99
97
  expect(apply_spec['packages']).to eq(expected_packages)
100
98
  end
@@ -104,49 +102,14 @@ describe Guard::Bosh::ApplySpecification do
104
102
  expect(apply_spec['configuration_hash']).to_not be_empty
105
103
  end
106
104
 
107
- context 'when the job has a static ip' do
108
- it 'includes the network config' do
109
- expected_networks = {
110
- 'redis1' => {
111
- 'cloud_properties' => { 'name' => 'iaas_network_name' },
112
- 'dns_record_name' => '0.redis-leader-z1.redis1.redis-deployment.bosh',
113
- 'ip' => '203.0.113.2',
114
- 'netmask' => '255.255.255.0',
115
- 'default' => %w(dns gateway)
116
- }
117
- }
118
- apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
119
- expect(apply_spec['networks']).to eq(expected_networks)
120
- end
121
- end
122
-
123
- context 'when the job has a dynamic ip' do
124
- let(:manifest_stub_without_static_ip) do
125
- manifest_stub.tap do |stub|
126
- stub['jobs'].first['networks'].first.delete('static_ips')
127
- end
128
- end
129
-
130
- subject do
131
- Guard::Bosh::ApplySpecification.new(
132
- deployment_manifest: manifest_stub_without_static_ip,
133
- package_resolver: package_resolver
134
- )
135
- end
136
-
137
- it 'includes the network config' do
138
- expected_networks = {
139
- 'redis1' => {
140
- 'cloud_properties' => { 'name' => 'iaas_network_name' },
141
- 'dns_record_name' => '0.redis-leader-z1.redis1.redis-deployment.bosh',
142
- 'ip' => '203.0.113.2',
143
- 'netmask' => '255.255.255.0',
144
- 'default' => %w(dns gateway)
145
- }
146
- }
147
- apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
148
- expect(apply_spec['networks']).to eq(expected_networks)
149
- end
105
+ it 'includes the network config' do
106
+ generated_networks = { 'redis1' => {} }
107
+ expect(network_generator).to receive(:generate).with(
108
+ deployment_manifest: manifest_stub,
109
+ job_name: 'redis_leader_z1'
110
+ ).and_return(generated_networks)
111
+ apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
112
+ expect(apply_spec['networks']).to eq(generated_networks)
150
113
  end
151
114
 
152
115
  it 'includes the resource pool' do
@@ -8,21 +8,21 @@ describe Guard::Bosh::ChangeAssessor do
8
8
  end
9
9
 
10
10
  context 'when a final.yml has been modified' do
11
- it 'reports a scope that means the change can be ignored' do
11
+ it 'reports scope that means the change can be ignored' do
12
12
  change_scope, _ = subject.determine_scope(['config/final.yml'])
13
13
  expect(change_scope).to eq(:none)
14
14
  end
15
15
  end
16
16
 
17
17
  context 'when the deployment manifest has been modified' do
18
- it 'reports a scope that means all jobs and templates must be re-evaluated' do
18
+ it 'reports scope meaning all jobs and templates must be re-evaluated' do
19
19
  change_scope, _ = subject.determine_scope(['templates/manifest.yml'])
20
20
  expect(change_scope).to eq(:all)
21
21
  end
22
22
  end
23
23
 
24
24
  context 'when a job spec has been modified' do
25
- it 'reports a scope that means all templates must be evaluated for a single job' do
25
+ it 'reports scope meaning templates must be evaluated for a single job' do
26
26
  change_scope, _ = subject.determine_scope(['jobs/redis/spec'])
27
27
  expect(change_scope).to eq(:all_templates_for_job)
28
28
  end
@@ -33,18 +33,20 @@ describe Guard::Bosh::ChangeAssessor do
33
33
  end
34
34
 
35
35
  context 'when a job template has been modified' do
36
- it 'reports a scope that means a single template must be evaluated for a single job' do
37
- change_scope, _ = subject.determine_scope(['jobs/redis/templates/config/redis.conf.erb'])
36
+ it 'reports scope meaning a single template must be evaluated for a job' do
37
+ change_scope, _ = subject.determine_scope(
38
+ ['jobs/redis/templates/config/redis.conf.erb'])
38
39
  expect(change_scope).to eq(:single_template)
39
40
  end
40
41
  it 'reports the correct job name' do
41
- _, job_name = subject.determine_scope(['jobs/redis/templates/config/redis.conf.erb'])
42
+ _, job_name = subject.determine_scope(
43
+ ['jobs/redis/templates/config/redis.conf.erb'])
42
44
  expect(job_name).to eq('redis')
43
45
  end
44
46
  end
45
47
 
46
48
  context 'when multiple job specs have been modified across jobs' do
47
- it 'reports a scope that means all jobs and templates must be re-evaluated' do
49
+ it 'reports scope that means all jobs and templates must be re-evaluated' do
48
50
  change_scope, _ = subject.determine_scope([
49
51
  'jobs/redis/spec',
50
52
  'jobs/postgresql/spec'
@@ -54,7 +56,7 @@ describe Guard::Bosh::ChangeAssessor do
54
56
  end
55
57
 
56
58
  context 'when multiple templates have been modified across jobs' do
57
- it 'reports a scope that means all jobs and templates must be re-evaluated' do
59
+ it 'reports scope that means all jobs and templates must be re-evaluated' do
58
60
  change_scope, _ = subject.determine_scope([
59
61
  'jobs/redis/templates/config/redis.conf.erb',
60
62
  'jobs/postgresql/templates/config/pg_hba.conf.erb'
@@ -27,30 +27,31 @@ describe Guard::Bosh::EffectivePropertiesCalculator do
27
27
  it 'merges the properties' do
28
28
  expect(job_defaults_loader).to receive(:load_properties).and_return(
29
29
 
30
- 'redis' => {
31
- 'password' => 'password',
32
- 'port' => 6379
33
- }
30
+ 'redis' => {
31
+ 'password' => 'password',
32
+ 'port' => 6379
33
+ }
34
34
 
35
35
  )
36
36
  expect(global_properties_loader).to receive(:load_properties).and_return(
37
37
 
38
- 'redis' => {
39
- 'password' => 'secure-password',
40
- 'slaves' => ['203.0.113.3', '203.0.113.4']
41
- },
42
- 'network' => 'redis1'
38
+ 'redis' => {
39
+ 'password' => 'secure-password',
40
+ 'slaves' => ['203.0.113.3', '203.0.113.4']
41
+ },
42
+ 'network' => 'redis1'
43
43
 
44
44
  )
45
45
  expect(job_properties_loader).to receive(:load_properties).and_return(
46
46
 
47
- 'redis' => {
48
- 'master' => '203.0.113.2'
49
- }
47
+ 'redis' => {
48
+ 'master' => '203.0.113.2'
49
+ }
50
50
 
51
51
  )
52
52
 
53
- properties = subject.calculate_effective_properties(manifest_job_name: 'redis_z1')
53
+ properties = subject.calculate_effective_properties(
54
+ manifest_job_name: 'redis_z1')
54
55
  expect(properties).to eq(
55
56
  'redis' => {
56
57
  'password' => 'secure-password',
@@ -38,7 +38,8 @@ describe Guard::Bosh::JobDefaultPropertiesLoader do
38
38
 
39
39
  it 'returns the properties that have defaults in the job spec' do
40
40
  expect(YAML).to receive(:load_file).with(
41
- Pathname.new('/path/to/a/release/jobs/job-name/spec')).and_return(job_spec_excerpt)
41
+ Pathname.new('/path/to/a/release/jobs/job-name/spec')).and_return(
42
+ job_spec_excerpt)
42
43
  job_defaults = subject.load_properties(job_name: 'job-name')
43
44
  expect(job_defaults).to include(
44
45
  'redis' => {
@@ -54,14 +55,16 @@ describe Guard::Bosh::JobDefaultPropertiesLoader do
54
55
 
55
56
  it 'ignores properties that do not have defaults defined' do
56
57
  expect(YAML).to receive(:load_file).with(
57
- Pathname.new('/path/to/a/release/jobs/job-name/spec')).and_return(job_spec_excerpt)
58
+ Pathname.new('/path/to/a/release/jobs/job-name/spec')).and_return(
59
+ job_spec_excerpt)
58
60
  job_defaults = subject.load_properties(job_name: 'job-name')
59
61
  expect(job_defaults['redis'].keys).not_to include('master')
60
62
  end
61
63
 
62
64
  it 'includes an empty hash for intermediate keys' do
63
65
  expect(YAML).to receive(:load_file).with(
64
- Pathname.new('/path/to/a/release/jobs/job-name/spec')).and_return(job_spec_excerpt)
66
+ Pathname.new('/path/to/a/release/jobs/job-name/spec')).and_return(
67
+ job_spec_excerpt)
65
68
  job_defaults = subject.load_properties(job_name: 'job-name')
66
69
  expect(job_defaults['redis'].keys).to include('blazingly')
67
70
  expect(job_defaults['redis']['blazingly']).to be_empty
@@ -7,7 +7,9 @@ describe Guard::Bosh::JobPropertiesLoader do
7
7
  {
8
8
  'instances' => 1,
9
9
  'name' => 'redis_leader_z1',
10
- 'networks' => [{ 'name' => 'redis1', 'static_ips' => ['10.244.2.6'] }],
10
+ 'networks' => [
11
+ { 'name' => 'redis1', 'static_ips' => ['10.244.2.6'] }
12
+ ],
11
13
  'persistent_disk' => 0,
12
14
  'properties' => { 'network' => 'redis1', 'redis' => nil },
13
15
  'resource_pool' => 'small_z1',
@@ -16,9 +18,17 @@ describe Guard::Bosh::JobPropertiesLoader do
16
18
  {
17
19
  'instances' => 2,
18
20
  'name' => 'redis_z1',
19
- 'networks' => [{ 'name' => 'redis1', 'static_ips' => ['10.244.2.10', '10.244.2.14'] }],
21
+ 'networks' => [
22
+ {
23
+ 'name' => 'redis1',
24
+ 'static_ips' => ['10.244.2.10', '10.244.2.14']
25
+ }
26
+ ],
20
27
  'persistent_disk' => 0,
21
- 'properties' => { 'network' => 'redis1', 'redis' => { 'master' => '10.244.2.6' } },
28
+ 'properties' => {
29
+ 'network' => 'redis1',
30
+ 'redis' => { 'master' => '10.244.2.6' }
31
+ },
22
32
  'resource_pool' => 'small_z1',
23
33
  'templates' => [{ 'name' => 'redis', 'release' => 'redis' }],
24
34
  'update' => { 'canaries' => 10 }
@@ -34,18 +34,20 @@ describe Guard::Bosh::JobRepository do
34
34
  describe '#find_by_template' do
35
35
  context 'when there are multiple jobs that use a job template' do
36
36
  it 'returns all jobs that use that template' do
37
- expect(subject.find_by_template('redis')).to eq(%w(redis_leader_z1 redis_slave_z2))
37
+ expect(subject.find_by_template('redis')).to eq(
38
+ %w(redis_leader_z1 redis_slave_z2))
38
39
  end
39
40
  end
40
41
  end
41
42
 
42
43
  describe '#template_paths' do
43
44
  it 'looks up the template paths from the job specification' do
44
- expect(YAML).to receive(:load_file).with(Pathname.new('jobs/redis/spec')).and_return(
45
- 'templates' => {
46
- 'redis_ctl.sh.erb' => 'bin/redis_ctl.sh',
47
- 'redis.conf.erb' => 'config/redis.conf'
48
- }
45
+ expect(YAML).to receive(:load_file).with(
46
+ Pathname.new('jobs/redis/spec')).and_return(
47
+ 'templates' => {
48
+ 'redis_ctl.sh.erb' => 'bin/redis_ctl.sh',
49
+ 'redis.conf.erb' => 'config/redis.conf'
50
+ }
49
51
  )
50
52
  expect(subject.template_paths('redis')).to eq([
51
53
  Pathname.new('jobs/redis/templates/redis_ctl.sh.erb'),
@@ -0,0 +1,65 @@
1
+ require 'guard/bosh/network_generator'
2
+
3
+ describe Guard::Bosh::NetworkGenerator do
4
+ let(:manifest_stub) do
5
+ {
6
+ 'name' => 'redis-deployment',
7
+ 'jobs' => [{
8
+ 'name' => 'redis_leader_z1',
9
+ 'networks' => [{ 'name' => 'redis1', 'static_ips' => ['203.0.113.2'] }],
10
+ 'templates' => [{ 'name' => 'redis', 'release' => 'redis' }]
11
+ }],
12
+ 'networks' => [
13
+ {
14
+ 'name' => 'redis1',
15
+ 'subnets' => [{
16
+ 'cloud_properties' => { 'name' => 'iaas_network_name' },
17
+ 'range' => '203.0.113.2/24',
18
+ 'static' => ['10.244.2.2']
19
+ }]
20
+ }
21
+ ]
22
+ }
23
+ end
24
+
25
+ context 'when the job has a static ip' do
26
+ it 'includes the network config' do
27
+ expected_networks = {
28
+ 'redis1' => {
29
+ 'cloud_properties' => { 'name' => 'iaas_network_name' },
30
+ 'dns_record_name' => '0.redis-leader-z1.redis1.redis-deployment.bosh',
31
+ 'ip' => '203.0.113.2',
32
+ 'netmask' => '255.255.255.0',
33
+ 'default' => %w(dns gateway)
34
+ }
35
+ }
36
+ generated_networks = subject.generate(
37
+ deployment_manifest: manifest_stub, job_name: 'redis_leader_z1')
38
+ expect(generated_networks).to eq(expected_networks)
39
+ end
40
+ end
41
+
42
+ context 'when the job has a dynamic ip' do
43
+ let(:manifest_stub_without_static_ip) do
44
+ manifest_stub.tap do |stub|
45
+ stub['jobs'].first['networks'].first.delete('static_ips')
46
+ end
47
+ end
48
+
49
+ it 'includes the network config' do
50
+ expected_networks = {
51
+ 'redis1' => {
52
+ 'cloud_properties' => { 'name' => 'iaas_network_name' },
53
+ 'dns_record_name' => '0.redis-leader-z1.redis1.redis-deployment.bosh',
54
+ 'ip' => '203.0.113.2',
55
+ 'netmask' => '255.255.255.0',
56
+ 'default' => %w(dns gateway)
57
+ }
58
+ }
59
+ generated_networks = subject.generate(
60
+ deployment_manifest: manifest_stub_without_static_ip,
61
+ job_name: 'redis_leader_z1')
62
+ expect(generated_networks).to eq(expected_networks)
63
+ end
64
+ end
65
+ end
@@ -27,14 +27,38 @@ describe Guard::Bosh::Notifier do
27
27
  priority: 2
28
28
  )
29
29
  subject.notify([
30
- { template: 'config.erb', status: :failure, detail: 'Missing property: redis.port' }
30
+ {
31
+ template: 'config.erb',
32
+ status: :failure,
33
+ detail: 'Missing property: redis.port'
34
+ }
31
35
  ])
32
36
  end
33
37
  it 'outputs the template the error occurred in and the detail' do
34
- expect(Guard::Compat::UI).to receive(:error).with('config.erb: Missing property: redis.port')
38
+ expect(Guard::Compat::UI).to receive(:error).with(
39
+ 'config.erb:10: missing property: redis.port')
35
40
  subject.notify([
36
- { template: 'config.erb', status: :failure, detail: 'Missing property: redis.port' }
41
+ {
42
+ template: 'config.erb',
43
+ status: :failure,
44
+ detail: 'missing property: redis.port',
45
+ line: 10
46
+ }
37
47
  ])
38
48
  end
49
+ context 'when the line number is not known' do
50
+ it 'outputs the template the error occurred in and the detail' do
51
+ expect(Guard::Compat::UI).to receive(:error).with(
52
+ 'config.erb:?: missing property: redis.port')
53
+ subject.notify([
54
+ {
55
+ template: 'config.erb',
56
+ status: :failure,
57
+ detail: 'missing property: redis.port',
58
+ line: :unknown
59
+ }
60
+ ])
61
+ end
62
+ end
39
63
  end
40
64
  end
@@ -23,7 +23,8 @@ describe Guard::Bosh::TemplateChecker do
23
23
 
24
24
  before do
25
25
  allow(properties_calculator).to receive(:calculate_effective_properties)
26
- allow(apply_specification).to receive(:generate).and_return('apply' => 'specification')
26
+ allow(apply_specification).to receive(:generate).and_return(
27
+ 'apply' => 'specification')
27
28
  allow(template_renderer).to receive(:render)
28
29
  end
29
30
 
@@ -56,7 +57,8 @@ describe Guard::Bosh::TemplateChecker do
56
57
  end
57
58
 
58
59
  it 'returns any errors generated by the template render' do
59
- expect(template_renderer).to receive(:render).and_return(['missing property'])
60
+ expect(template_renderer).to receive(:render).and_return(
61
+ ['missing property'])
60
62
  errors = subject.check(
61
63
  manifest_job_name: 'redis_leader_z1',
62
64
  job_name: 'redis',
@@ -21,15 +21,28 @@ describe Guard::Bosh::TemplateRenderer do
21
21
  end
22
22
  end
23
23
 
24
+ let(:backtrace) do
25
+ [
26
+ "gems/bosh-template-0/lib/bosh/template/evaluation_context.rb:00:in `p'",
27
+ "(erb):3:in `get_binding'",
28
+ "rubies/ruby-0.0.0/lib/ruby/0.0.0/erb.rb:000:in `eval'"
29
+ ]
30
+ end
31
+
32
+ def with_backtrace(error)
33
+ error.tap { |e| e.set_backtrace(backtrace) }
34
+ end
35
+
24
36
  context 'when the template refers to an unknown property' do
25
37
  it 'reports the missing property' do
26
38
  expect(bosh_renderer).to receive(:render).with('config.erb').and_raise(
27
- ::Bosh::Template::UnknownProperty.new('redis.port'))
39
+ with_backtrace(::Bosh::Template::UnknownProperty.new('redis.port')))
28
40
  result = subject.render(context: {}, template: 'config.erb')
29
41
  expect(result).to eq(
30
42
  template: 'config.erb',
31
43
  status: :failure,
32
- detail: 'missing property: redis.port'
44
+ detail: 'missing property: redis.port',
45
+ line: 3
33
46
  )
34
47
  end
35
48
  end
@@ -37,12 +50,19 @@ describe Guard::Bosh::TemplateRenderer do
37
50
  context 'when the template calls a misnamed helper method' do
38
51
  it 'reports the missing helper method' do
39
52
  expect(bosh_renderer).to receive(:render).with('config.erb').and_raise(
40
- NoMethodError.new("undefined method `o' for #<Bosh::Template::EvaluationContext:0x00000000000000>"))
53
+ with_backtrace(
54
+ NoMethodError.new(
55
+ "undefined method `o' for "\
56
+ '#<Bosh::Template::EvaluationContext:0x00000000000000>'
57
+ )
58
+ )
59
+ )
41
60
  result = subject.render(context: {}, template: 'config.erb')
42
61
  expect(result).to eq(
43
62
  template: 'config.erb',
44
63
  status: :failure,
45
- detail: "undefined method `o'"
64
+ detail: "undefined method `o'",
65
+ line: 3
46
66
  )
47
67
  end
48
68
  end
@@ -50,12 +70,19 @@ describe Guard::Bosh::TemplateRenderer do
50
70
  context 'when the template references a missing name' do
51
71
  it 'reports the missing name' do
52
72
  expect(bosh_renderer).to receive(:render).with('config.erb').and_raise(
53
- NameError.new("undefined local variable or method `missing' for #<Bosh::Template::EvaluationContext:0x00000000000000>"))
73
+ with_backtrace(
74
+ NameError.new(
75
+ "undefined local variable or method `missing' for "\
76
+ '#<Bosh::Template::EvaluationContext:0x00000000000000>'
77
+ )
78
+ )
79
+ )
54
80
  result = subject.render(context: {}, template: 'config.erb')
55
81
  expect(result).to eq(
56
82
  template: 'config.erb',
57
83
  status: :failure,
58
- detail: "undefined local variable or method `missing'"
84
+ detail: "undefined local variable or method `missing'",
85
+ line: 3
59
86
  )
60
87
  end
61
88
  end
@@ -63,13 +90,35 @@ describe Guard::Bosh::TemplateRenderer do
63
90
  context 'when the template is not well-formed' do
64
91
  it 'reports the template error' do
65
92
  expect(bosh_renderer).to receive(:render).with('config.erb').and_raise(
66
- SyntaxError.new("(erb):7: syntax error, unexpected end-of-input, expecting keyword_end\n; _erbout.force_encoding(__ENCODING__)"))
93
+ with_backtrace(
94
+ SyntaxError.new(
95
+ '(erb):7: syntax error, unexpected end-of-input, '\
96
+ "expecting keyword_end\n; _erbout.force_encoding(__ENCODING__)"
97
+ )
98
+ )
99
+ )
67
100
  result = subject.render(context: {}, template: 'config.erb')
68
101
  expect(result).to eq(
69
102
  template: 'config.erb',
70
103
  status: :failure,
71
- detail: 'syntax error, unexpected end-of-input, expecting keyword_end'
104
+ detail: 'syntax error, unexpected end-of-input, expecting keyword_end',
105
+ line: 7
72
106
  )
73
107
  end
74
108
  end
109
+
110
+ context 'when the backtrace does not include an (erb) line' do
111
+ it 'reports the template error but without a line number' do
112
+ error = NameError.new(
113
+ "undefined local variable or method `missing' for "\
114
+ '#<Bosh::Template::EvaluationContext:0x00000000000000>')
115
+ error.set_backtrace([
116
+ "gems/bosh-template-0/lib/bosh/template/evaluation_context.rb:00:in `p'"
117
+ ])
118
+ expect(bosh_renderer).to receive(:render).with(
119
+ 'config.erb').and_raise(error)
120
+ result = subject.render(context: {}, template: 'config.erb')
121
+ expect(result).to include(line: :unknown)
122
+ end
123
+ end
75
124
  end
@@ -100,10 +100,11 @@ describe Guard::Bosh do
100
100
  expect(job_repository).to receive(:find_by_template).with(
101
101
  'redis').and_return(%w(redis_leader_z1 redis_slave_z2))
102
102
 
103
- expect(job_repository).to receive(:template_paths).with('redis').and_return([
104
- 'jobs/redis/templates/redis_ctl.sh.erb',
105
- 'jobs/redis/templates/redis.conf.erb'
106
- ])
103
+ expect(job_repository).to receive(:template_paths).with(
104
+ 'redis').and_return([
105
+ 'jobs/redis/templates/redis_ctl.sh.erb',
106
+ 'jobs/redis/templates/redis.conf.erb'
107
+ ])
107
108
  end
108
109
  context 'when there are no errors' do
109
110
  it 'checks the template for errors' do
@@ -137,20 +138,22 @@ describe Guard::Bosh do
137
138
  before do
138
139
  expect(job_repository).to receive(:job_templates).and_return(
139
140
  %w(postgresql redis))
140
- expect(job_repository).to receive(:template_paths).with('postgresql').and_return([
141
- 'jobs/postgresql/templates/pg_hba.conf.erb'
142
- ])
143
- expect(job_repository).to receive(:template_paths).with('redis').and_return([
144
- 'jobs/redis/templates/redis_ctl.sh.erb',
145
- 'jobs/redis/templates/redis.conf.erb'
146
- ])
141
+ expect(job_repository).to receive(:template_paths).with(
142
+ 'postgresql').and_return([
143
+ 'jobs/postgresql/templates/pg_hba.conf.erb'
144
+ ])
145
+ expect(job_repository).to receive(:template_paths).with(
146
+ 'redis').and_return([
147
+ 'jobs/redis/templates/redis_ctl.sh.erb',
148
+ 'jobs/redis/templates/redis.conf.erb'
149
+ ])
147
150
  expect(template_checker).to receive(:check).with(
148
151
  manifest_job_name: 'redis_slave_z2',
149
152
  job_name: 'redis',
150
153
  template: 'jobs/redis/templates/redis.conf.erb'
151
154
  ).and_return([])
152
155
  expect(job_repository).to receive(:find_by_template).with(
153
- 'redis').and_return(%w(redis_leader_z1 redis_slave_z2))
156
+ 'redis').and_return(%w(redis_leader_z1 redis_slave_z2))
154
157
  expect(job_repository).to receive(:find_by_template).with(
155
158
  'postgresql').and_return(['postgresql_z1'])
156
159
  expect(template_checker).to receive(:check).with(
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: guard-bosh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Crump
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-01 00:00:00.000000000 Z
11
+ date: 2015-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bosh-template
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '10.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.29'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.29'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: simplecov
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -123,6 +137,7 @@ files:
123
137
  - lib/guard/bosh/job_default_properties_loader.rb
124
138
  - lib/guard/bosh/job_properties_loader.rb
125
139
  - lib/guard/bosh/job_repository.rb
140
+ - lib/guard/bosh/network_generator.rb
126
141
  - lib/guard/bosh/notifier.rb
127
142
  - lib/guard/bosh/package_resolver.rb
128
143
  - lib/guard/bosh/template_checker.rb
@@ -135,6 +150,7 @@ files:
135
150
  - spec/guard/bosh/job_default_properties_loader_spec.rb
136
151
  - spec/guard/bosh/job_properties_loader_spec.rb
137
152
  - spec/guard/bosh/job_repository_spec.rb
153
+ - spec/guard/bosh/network_generator_spec.rb
138
154
  - spec/guard/bosh/notifier_spec.rb
139
155
  - spec/guard/bosh/package_resolver_spec.rb
140
156
  - spec/guard/bosh/template_checker_spec.rb
@@ -161,7 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
177
  version: '0'
162
178
  requirements: []
163
179
  rubyforge_project:
164
- rubygems_version: 2.4.5
180
+ rubygems_version: 2.2.2
165
181
  signing_key:
166
182
  specification_version: 4
167
183
  summary: Fast feedback when developing BOSH releases