guard-bosh 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ require 'guard/compat'
2
+
3
+ module Guard
4
+ class Bosh
5
+ class Notifier
6
+ def notify(errors)
7
+ if errors.empty?
8
+ Guard::Compat::UI.notify(
9
+ 'Succeeded', title: 'BOSH', image: :success, priority: -2)
10
+ else
11
+ Guard::Compat::UI.error(
12
+ '%s: %s' % [errors.first[:template], errors.first[:detail]])
13
+ Guard::Compat::UI.notify(
14
+ 'Failed', title: 'BOSH', image: :failed, priority: 2)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Guard
2
+ class Bosh
3
+ class PackageResolver
4
+ def initialize(release_dir)
5
+ @release_dir = release_dir
6
+ end
7
+
8
+ def resolve(job)
9
+ job_spec = @release_dir + 'jobs' + job + 'spec'
10
+ YAML.load_file(job_spec)['packages']
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ module Guard
2
+ class Bosh
3
+ class TemplateChecker
4
+ def initialize(deployment_manifest:,
5
+ properties_calculator:,
6
+ apply_specification:,
7
+ template_renderer:)
8
+ @deployment_manifest = deployment_manifest
9
+ @properties_calculator = properties_calculator
10
+ @apply_specification = apply_specification
11
+ @template_renderer = template_renderer
12
+ end
13
+
14
+ def check(manifest_job_name:, job_name:, template:)
15
+ properties = @properties_calculator.calculate_effective_properties(
16
+ manifest_job_name: manifest_job_name, job_name: job_name)
17
+ apply_spec = @apply_specification.generate(
18
+ properties: properties,
19
+ job_name: manifest_job_name
20
+ )
21
+ @template_renderer.render(context: apply_spec, template: template)
22
+ end
23
+
24
+ def self.build(deployment_manifest:, release_dir:)
25
+ properties_calculator = EffectivePropertiesCalculator.new(loaders: [
26
+ JobDefaultPropertiesLoader.new(release_dir: release_dir),
27
+ GlobalPropertiesLoader.new(deployment_manifest: deployment_manifest),
28
+ JobPropertiesLoader.new(deployment_manifest: deployment_manifest)
29
+ ])
30
+ apply_specification = ApplySpecification.new(
31
+ deployment_manifest: deployment_manifest,
32
+ package_resolver: PackageResolver.new(release_dir)
33
+ )
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
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ require 'json'
2
+ require 'bosh/template/renderer'
3
+ require 'bosh/template/unknown_property'
4
+
5
+ module Guard
6
+ class Bosh
7
+ class TemplateRenderer
8
+ def render(context:, template:)
9
+ renderer = ::Bosh::Template::Renderer.new(
10
+ context: JSON.generate(context))
11
+ begin
12
+ renderer.render(template)
13
+ { template: template, status: :success, detail: '' }
14
+ rescue ::Bosh::Template::UnknownProperty => e
15
+ {
16
+ template: template,
17
+ status: :failure,
18
+ detail: "Missing property: #{e.name}"
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ clearing :on
2
+ directories %w(jobs templates)
3
+
4
+ manifest_path = 'path/to/manifest.yml'
5
+ guard :bosh, deployment_manifest: manifest_path do
6
+ watch(%r{^jobs/.*})
7
+ watch(manifest_path)
8
+ end
9
+
10
+ notification :tmux, display_message: true
@@ -0,0 +1,184 @@
1
+ require 'guard/bosh/apply_specification'
2
+
3
+ describe Guard::Bosh::ApplySpecification do
4
+ let(:package_resolver) { double }
5
+ subject do
6
+ Guard::Bosh::ApplySpecification.new(
7
+ deployment_manifest: manifest_stub,
8
+ package_resolver: package_resolver
9
+ )
10
+ end
11
+
12
+ let(:manifest_stub) do
13
+ {
14
+ 'name' => 'redis-deployment',
15
+ 'jobs' => [{
16
+ 'instances' => 1,
17
+ 'name' => 'redis_leader_z1',
18
+ 'networks' => [{ 'name' => 'redis1', 'static_ips' => ['203.0.113.2'] }],
19
+ 'persistent_disk' => 0,
20
+ 'resource_pool' => 'small_z1',
21
+ 'templates' => [{ 'name' => 'redis', 'release' => 'redis' }]
22
+ }],
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
+ 'resource_pools' => [
34
+ {
35
+ 'cloud_properties' => { 'name' => 'random' },
36
+ 'name' => 'small_z1',
37
+ 'network' => 'redis1',
38
+ 'size' => 3,
39
+ 'stemcell' => {
40
+ 'name' => 'bosh-warden-boshlite-ubuntu-trusty-go_agent',
41
+ 'version' => '389'
42
+ }
43
+ }
44
+ ]
45
+ }
46
+ end
47
+
48
+ before do
49
+ allow(package_resolver).to receive(:resolve).with('redis').and_return(['redis'])
50
+ end
51
+
52
+ it 'includes the effective properties provided' do
53
+ effective_properties = {
54
+ 'redis' => {
55
+ 'master' => '203.0.113.2'
56
+ }
57
+ }
58
+ apply_spec = subject.generate(
59
+ properties: effective_properties, job_name: 'redis_leader_z1')
60
+ expect(apply_spec['properties']).to include(effective_properties)
61
+ end
62
+
63
+ it 'includes a job index' do
64
+ apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
65
+ expect(apply_spec['index']).to eq(0)
66
+ end
67
+
68
+ it 'includes the job details' do
69
+ expected_job = {
70
+ 'name' => 'redis_leader_z1',
71
+ 'template' => 'redis',
72
+ 'version' => '1',
73
+ 'sha1' => 'c7f277de5b283e5ceffe55674dc56fad2257ecab',
74
+ 'blobstore_id' => '8a66ab45-4831-4ce3-aa8f-313fe33a9891',
75
+ 'templates' => [
76
+ {
77
+ 'name' => 'redis',
78
+ 'version' => '1',
79
+ 'sha1' => '88d6ea417857efda58916f9cb9bd5dd3a0f76f00',
80
+ 'blobstore_id' => '2356dff1-18fd-4314-a9bd-199b9d6c5c45'
81
+ }
82
+ ]
83
+ }
84
+ apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
85
+ expect(apply_spec['job']).to eq(expected_job)
86
+ end
87
+
88
+ it 'includes the package details' do
89
+ expected_packages = {
90
+ 'redis' => {
91
+ 'name' => 'redis',
92
+ 'version' => '1.0',
93
+ 'sha1' => 'b945ce51b3635bb0ebfb2207323514381bcee824',
94
+ 'blobstore_id' => '608c41bc-d491-4773-9812-8f24276eace1'
95
+ }
96
+ }
97
+ expect(package_resolver).to receive(:resolve).with('redis').and_return(['redis'])
98
+ apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
99
+ expect(apply_spec['packages']).to eq(expected_packages)
100
+ end
101
+
102
+ it 'includes a dummy configuration hash' do
103
+ apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
104
+ expect(apply_spec['configuration_hash']).to_not be_empty
105
+ end
106
+
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
150
+ end
151
+
152
+ it 'includes the resource pool' do
153
+ resource_pool = {
154
+ 'cloud_properties' => {
155
+ 'name' => 'random'
156
+ },
157
+ 'name' => 'small_z1',
158
+ 'stemcell' => {
159
+ 'name' => 'bosh-warden-boshlite-ubuntu-trusty-go_agent',
160
+ 'version' => '389'
161
+ }
162
+ }
163
+ apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
164
+ expect(apply_spec['resource_pool']).to eq(resource_pool)
165
+ end
166
+
167
+ it 'includes the deployment name' do
168
+ apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
169
+ expect(apply_spec['deployment']).to eq('redis-deployment')
170
+ end
171
+
172
+ it 'includes the persistent disk' do
173
+ apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
174
+ expect(apply_spec['persistent_disk']).to eq(0)
175
+ end
176
+
177
+ it 'includes a dummy rendered templates archive' do
178
+ apply_spec = subject.generate(properties: {}, job_name: 'redis_leader_z1')
179
+ expect(apply_spec['rendered_templates_archive']).to eq(
180
+ 'sha1' => 'c299ead74faf9ee9b47b3548e5df427e3e9a2c70',
181
+ 'blobstore_id' => '72fb06ef-0f40-4280-85e8-b5930e672308'
182
+ )
183
+ end
184
+ end
@@ -0,0 +1,65 @@
1
+ require 'guard/bosh/change_assessor'
2
+
3
+ require 'pathname'
4
+
5
+ describe Guard::Bosh::ChangeAssessor do
6
+ subject do
7
+ Guard::Bosh::ChangeAssessor.new(Pathname.new('templates/manifest.yml'))
8
+ end
9
+
10
+ context 'when a final.yml has been modified' do
11
+ it 'reports a scope that means the change can be ignored' do
12
+ change_scope, _ = subject.determine_scope(['config/final.yml'])
13
+ expect(change_scope).to eq(:none)
14
+ end
15
+ end
16
+
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
19
+ change_scope, _ = subject.determine_scope(['templates/manifest.yml'])
20
+ expect(change_scope).to eq(:all)
21
+ end
22
+ end
23
+
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
26
+ change_scope, _ = subject.determine_scope(['jobs/redis/spec'])
27
+ expect(change_scope).to eq(:all_templates_for_job)
28
+ end
29
+ it 'reports the correct job name' do
30
+ _, job_name = subject.determine_scope(['jobs/redis/spec'])
31
+ expect(job_name).to eq('redis')
32
+ end
33
+ end
34
+
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'])
38
+ expect(change_scope).to eq(:single_template)
39
+ end
40
+ it 'reports the correct job name' do
41
+ _, job_name = subject.determine_scope(['jobs/redis/templates/config/redis.conf.erb'])
42
+ expect(job_name).to eq('redis')
43
+ end
44
+ end
45
+
46
+ 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
48
+ change_scope, _ = subject.determine_scope([
49
+ 'jobs/redis/spec',
50
+ 'jobs/postgresql/spec'
51
+ ])
52
+ expect(change_scope).to eq(:all)
53
+ end
54
+ end
55
+
56
+ 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
58
+ change_scope, _ = subject.determine_scope([
59
+ 'jobs/redis/templates/config/redis.conf.erb',
60
+ 'jobs/postgresql/templates/config/pg_hba.conf.erb'
61
+ ])
62
+ expect(change_scope).to eq(:all)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,64 @@
1
+ require 'guard/bosh/job_properties_loader'
2
+ require 'guard/bosh/job_default_properties_loader'
3
+ require 'guard/bosh/global_properties_loader'
4
+ require 'guard/bosh/effective_properties_calculator'
5
+
6
+ describe Guard::Bosh::EffectivePropertiesCalculator do
7
+ let(:job_defaults_loader) do
8
+ instance_double(Guard::Bosh::JobDefaultPropertiesLoader)
9
+ end
10
+
11
+ let(:global_properties_loader) do
12
+ instance_double(Guard::Bosh::GlobalPropertiesLoader)
13
+ end
14
+
15
+ let(:job_properties_loader) do
16
+ instance_double(Guard::Bosh::JobPropertiesLoader)
17
+ end
18
+
19
+ subject do
20
+ Guard::Bosh::EffectivePropertiesCalculator.new(loaders: [
21
+ job_defaults_loader,
22
+ global_properties_loader,
23
+ job_properties_loader
24
+ ])
25
+ end
26
+
27
+ it 'merges the properties' do
28
+ expect(job_defaults_loader).to receive(:load_properties).and_return(
29
+
30
+ 'redis' => {
31
+ 'password' => 'password',
32
+ 'port' => 6379
33
+ }
34
+
35
+ )
36
+ expect(global_properties_loader).to receive(:load_properties).and_return(
37
+
38
+ 'redis' => {
39
+ 'password' => 'secure-password',
40
+ 'slaves' => ['203.0.113.3', '203.0.113.4']
41
+ },
42
+ 'network' => 'redis1'
43
+
44
+ )
45
+ expect(job_properties_loader).to receive(:load_properties).and_return(
46
+
47
+ 'redis' => {
48
+ 'master' => '203.0.113.2'
49
+ }
50
+
51
+ )
52
+
53
+ properties = subject.calculate_effective_properties(manifest_job_name: 'redis_z1')
54
+ expect(properties).to eq(
55
+ 'redis' => {
56
+ 'password' => 'secure-password',
57
+ 'port' => 6379,
58
+ 'master' => '203.0.113.2',
59
+ 'slaves' => ['203.0.113.3', '203.0.113.4']
60
+ },
61
+ 'network' => 'redis1'
62
+ )
63
+ end
64
+ end
@@ -0,0 +1,38 @@
1
+ require 'guard/bosh/global_properties_loader'
2
+
3
+ describe Guard::Bosh::GlobalPropertiesLoader do
4
+ let(:deployment_manifest) do
5
+ {
6
+ 'jobs' => %w(some jobs),
7
+ 'networks' => %w(some networks),
8
+ 'properties' => {
9
+ 'global' => 'property'
10
+ }
11
+ }
12
+ end
13
+ let(:deployment_manifest_without_global_properties) do
14
+ deployment_manifest.tap do |manifest|
15
+ manifest.delete('properties')
16
+ end
17
+ end
18
+ subject do
19
+ Guard::Bosh::GlobalPropertiesLoader.new(
20
+ deployment_manifest: deployment_manifest)
21
+ end
22
+
23
+ context 'when the manifest contains global properties section' do
24
+ it 'returns them' do
25
+ expect(subject.load_properties('ignored')).to eq('global' => 'property')
26
+ end
27
+ end
28
+
29
+ context 'when the manifest does not contain a global properties section' do
30
+ subject do
31
+ Guard::Bosh::GlobalPropertiesLoader.new(
32
+ deployment_manifest: deployment_manifest_without_global_properties)
33
+ end
34
+ it 'returns an empty' do
35
+ expect(subject.load_properties('ignored')).to be_empty
36
+ end
37
+ end
38
+ end