guard-bosh 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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