vagrant-config_builder 0.1.0 → 0.2.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.
data/CHANGELOG ADDED
@@ -0,0 +1,26 @@
1
+ CHANGELOG
2
+ =========
3
+
4
+ 0.2.0
5
+ -----
6
+
7
+ 2013-08-16
8
+
9
+ This is a backwards compatible bugfix and feature release.
10
+
11
+ ### User notes
12
+
13
+ #### Enhancements
14
+
15
+ * (GH-10) Add support to vm model for hostname.
16
+
17
+ #### Bugfixes
18
+
19
+ * (GH-9) Correctly merge VM attributes by concatentating arrays.
20
+
21
+ 0.1.0
22
+ -----
23
+
24
+ 2013-08-08
25
+
26
+ Initial release.
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ # We depend on Vagrant for development, but we don't add it as a
7
+ # gem dependency because we expect to be installed within the
8
+ # Vagrant environment itself using `vagrant plugin`.
9
+ gem "vagrant", :git => "git://github.com/mitchellh/vagrant.git"
10
+ end
data/README.markdown CHANGED
@@ -51,6 +51,7 @@ for clarity.
51
51
  name: db
52
52
  private_networks: [ {ip: '10.20.1.2'} ]
53
53
  box: centos-5-i386
54
+ hostname: db.puppetlabs.vm
54
55
  roles: bigvm
55
56
  -
56
57
  name: web
@@ -60,6 +61,16 @@ for clarity.
60
61
  Installation
61
62
  ------------
62
63
 
63
- Installation into the Vagrant internal gems:
64
+ ### Installation into the Vagrant internal gems:
64
65
 
65
66
  * `vagrant plugin install vagrant-config_builder`
67
+
68
+ ### Installation from source
69
+
70
+ Build the gem:
71
+
72
+ * `gem build vagrant-config_builder.gemspec`
73
+
74
+ Install the gem:
75
+
76
+ * `gem install vagrant-config_builder-<version>.gem`
@@ -7,7 +7,13 @@ module ConfigBuilder
7
7
  # @api private
8
8
  class ClassRegistry
9
9
 
10
- class UnknownEntryError < Vagrant::Errors::VagrantError; end
10
+ class UnknownEntry < Vagrant::Errors::VagrantError
11
+ error_key('unknown_entry', 'config_builder.class_registry')
12
+ end
13
+
14
+ class DuplicateEntry < Vagrant::Errors::VagrantError
15
+ error_key('duplicate_entry', 'config_builder.class_registry')
16
+ end
11
17
 
12
18
  def initialize(name)
13
19
  @name = name
@@ -21,7 +27,12 @@ module ConfigBuilder
21
27
  #
22
28
  # @return [void]
23
29
  def register(identifier, klass)
24
- @klasses[identifier] = klass
30
+ if @klasses[identifier]
31
+ raise DuplicateEntry, :registry => @name,
32
+ :identifiers => @klasses.keys
33
+ else
34
+ @klasses[identifier] = klass
35
+ end
25
36
  end
26
37
 
27
38
  # Retrieve a class associated with the specified identifier
@@ -33,7 +44,9 @@ module ConfigBuilder
33
44
  if (klass = @klasses[identifier])
34
45
  klass
35
46
  else
36
- raise UnknownEntryError, "#{self.inspect} doesn't have an entry registered with key #{identifier.inspect}"
47
+ raise UnknownEntry, :registry => @name,
48
+ :identifier => identifier.inspect,
49
+ :identifiers => @klasses.keys
37
50
  end
38
51
  end
39
52
 
@@ -44,7 +57,8 @@ module ConfigBuilder
44
57
  #
45
58
  # @return [Object] The generated object
46
59
  def generate(hash)
47
- identifier = hash.delete('type')
60
+ config = hash.dup
61
+ identifier = config.delete('type')
48
62
 
49
63
  klass = retrieve(identifier)
50
64
 
@@ -52,7 +66,7 @@ module ConfigBuilder
52
66
  end
53
67
 
54
68
  def inspect
55
- "<#{self.class}: (#{@name})>"
69
+ "<#{self.class}: (#{@name}) keys: #{@klasses.keys.inspect}>"
56
70
  end
57
71
  end
58
72
  end
@@ -0,0 +1,22 @@
1
+ module ConfigBuilder
2
+ # Welcome to hell.
3
+ # @api private
4
+ class ExtensionHandler
5
+ def initialize
6
+ @logger = Log4r::Logger.new('vagrant::config_builder::extension_handler')
7
+ end
8
+
9
+ def load_from_plugins
10
+ Vagrant.plugin('2').manager.registered.each do |plugin|
11
+ load_plugin(plugin)
12
+ end
13
+ end
14
+
15
+ def load_plugin(plugin)
16
+ if plugin.respond_to? :config_builder_hook
17
+ @logger.info "Loading config_builder extension #{plugin.inspect}"
18
+ plugin.config_builder_hook
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,64 +1,75 @@
1
+ # The 'roles' filter adds a mechanism for defining generic VM roles and
2
+ # applying them to VMs.
3
+ #
4
+ # Defining roles
5
+ # --------------
6
+ #
7
+ # This filter adds support for a top level `roles` key. It contains a hash of
8
+ # role names that define a hash containing the role behavior.
9
+ #
10
+ # @note: The 'vms' field is of type Array, while the 'roles' field is of type
11
+ # Hash. This is because order of declaration matters for the actual VMs, while
12
+ # the order of declaration of roles does not matter.
13
+ #
14
+ # @example
15
+ # >> hash_config
16
+ # => {
17
+ # 'roles' => {
18
+ # 'webserver' => {
19
+ # 'synced_folders' => [
20
+ # {'host_path' => './www', 'guest_path' => '/var/www'},
21
+ # {'host_path' => './webserver-binaries', 'guest_path' => '/opt/webserver-binaries'},
22
+ # ]
23
+ # },
24
+ # 'database' => {
25
+ # 'provisioners' => [
26
+ # {'type' => 'puppet', 'manifest' => 'dbserver.pp'},
27
+ # {'type' => 'shell', 'path' => 'scripts/initialize-db.sh'},
28
+ # ],
29
+ # }
30
+ # },
31
+ # 'vms' => [
32
+ # {'name' => 'web', 'roles' => 'webserver'},
33
+ # {'name' => 'db', 'roles' => 'database'},
34
+ # {'name' => 'standalone', 'roles' => ['webserver', 'database']},
35
+ # ],
36
+ # }
37
+ #
1
38
  class ConfigBuilder::Filter::Roles
2
39
 
3
- def run(input)
4
- strip_filter_data(input)
40
+ def run(hash_config)
41
+ @roles = hash_config.delete('roles')
5
42
 
6
- vms = @vms.map { |vm_hash| filter_vm(vm_hash) }
43
+ hash_config['vms'].map! { |vm_hash| filter_vm(vm_hash) }
7
44
 
8
- add_filtered_data(input, vms)
9
-
10
- input
45
+ hash_config
11
46
  end
12
47
 
13
48
  private
14
49
 
15
- def strip_filter_data(input)
16
- @roles = input.delete('roles')
17
- @vms = input.delete('vms')
18
- end
19
-
20
- def add_filtered_data(input, vms)
21
- input['vms'] = vms
22
- end
23
-
24
- def filter_vm(vm_hash)
25
- role_names = roles_for_vm(vm_hash.delete('roles'))
26
-
27
- new_vm_hash = merge_role_definitions(role_names)
28
- new_vm_hash.merge!(vm_hash)
50
+ def filter_vm(old_vm)
51
+ role_list = old_vm.delete('roles')
52
+ node_stack = roles_for_vm(role_list)
29
53
 
30
- new_vm_hash
31
- end
32
-
33
- # Generate a hash of all the merged roles. Roles specified later in the hash
34
- # overwrite earlier roles.
35
- #
36
- # @param names [Hash]
37
- #
38
- # @return [Hash]
39
- def merge_role_definitions(role_names)
40
- vm_template = {}
54
+ node_stack << old_vm
41
55
 
42
- role_names.each do |name|
43
- role_definition = @roles[name]
44
- vm_template.merge!(role_definition)
56
+ new_vm = node_stack.inject({}) do |accumulator, role|
57
+ merge_nodes(accumulator, role)
45
58
  end
46
59
 
47
- vm_template
60
+ new_vm
48
61
  end
49
62
 
50
63
  # @return [Array]
51
64
  def roles_for_vm(role_field)
65
+
52
66
  case role_field
53
- when Array
54
- rval = role_field
55
- when String
56
- rval = [role_field]
57
- when NilClass
58
- rval = []
67
+ when Array then names = role_field
68
+ when String then names = [role_field]
69
+ when NilClass then names = []
59
70
  end
60
71
 
61
- rval
72
+ names.map { |name| role_definition(name) }
62
73
  end
63
74
 
64
75
  def role_definition(name)
@@ -68,4 +79,39 @@ class ConfigBuilder::Filter::Roles
68
79
  raise "Couldn't find role named #{name.inspect} in roles #{@roles.keys.inspect}"
69
80
  end
70
81
  end
82
+
83
+ # Merge two node hash structures, with values in `right` overwriting values
84
+ # in `left`.
85
+ #
86
+ # @param left [Hash]
87
+ # @param right [Hash]
88
+ #
89
+ # @return [Hash]
90
+ def merge_nodes(left, right)
91
+ retval = right.clone
92
+
93
+ array_keys = %w[
94
+ provisioners
95
+ synced_folders
96
+ forwarded_ports
97
+ private_networks
98
+ public_networks
99
+ ]
100
+
101
+ array_keys.each do |key|
102
+ if (left[key] and right[key])
103
+ retval[key] += left[key]
104
+ elsif left[key]
105
+ retval[key] = left[key]
106
+ end
107
+ end
108
+
109
+ single_keys = %w[provider box name]
110
+
111
+ single_keys.each do |key|
112
+ retval[key] = left[key] if left[key]
113
+ end
114
+
115
+ retval
116
+ end
71
117
  end
@@ -9,7 +9,7 @@ class ConfigBuilder::Model::Network::PrivateNetwork < ConfigBuilder::Model::Base
9
9
 
10
10
  def to_proc
11
11
  Proc.new do |vm_config|
12
- vm_config.network(:private_network, :ip => attr(:ip))
12
+ vm_config.network(:private_network, @attrs)
13
13
  end
14
14
  end
15
15
  end
@@ -70,6 +70,10 @@ class ConfigBuilder::Model::VM < ConfigBuilder::Model::Base
70
70
  # @return [String] The name of the instantiated box in this environment
71
71
  def_model_attribute :name
72
72
 
73
+ # @!attribute [rw] hostname
74
+ # @return [String] The hostname the machine should have.
75
+ def_model_attribute :hostname
76
+
73
77
  def initialize
74
78
  @defaults = {
75
79
  :provisioners => [],
@@ -85,6 +89,7 @@ class ConfigBuilder::Model::VM < ConfigBuilder::Model::Base
85
89
  vm_config = config.vm
86
90
 
87
91
  vm_config.box = attr(:box) if defined? attr(:box)
92
+ vm_config.hostname = attr(:hostname) if defined? attr(:hostname)
88
93
 
89
94
  eval_models(vm_config)
90
95
  end
@@ -6,8 +6,6 @@ module ConfigBuilder
6
6
 
7
7
  module Model
8
8
 
9
- class UnknownAttributeError < Vagrant::Errors::VagrantError; end
10
-
11
9
  require 'config_builder/model/base'
12
10
 
13
11
  require 'config_builder/model/root'
@@ -1,11 +1,14 @@
1
1
  require 'config_builder/loader'
2
2
  require 'config_builder/filter_stack'
3
3
  require 'config_builder/model'
4
+ require 'config_builder/extension_handler'
4
5
 
5
6
  module ConfigBuilder
6
7
  class Runner
7
8
 
8
9
  def run(identifier, method, value)
10
+ load_extensions
11
+
9
12
  data = ConfigBuilder::Loader.generate(identifier, method, value)
10
13
  filtered_data = run_filters(data)
11
14
  model = generate_model(filtered_data)
@@ -13,6 +16,11 @@ module ConfigBuilder
13
16
 
14
17
  private
15
18
 
19
+ def load_extensions
20
+ ext = ConfigBuilder::ExtensionHandler.new
21
+ ext.load_from_plugins
22
+ end
23
+
16
24
  def run_filters(data)
17
25
  stack = ConfigBuilder::FilterStack.new
18
26
  stack.filter(data)
@@ -1,3 +1,3 @@
1
1
  module ConfigBuilder
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -11,4 +11,14 @@ module ConfigBuilder
11
11
  runner = ConfigBuilder::Runner.new
12
12
  runner.run(identifier, method, value)
13
13
  end
14
+
15
+ def self.source_root
16
+ @source_root ||= File.expand_path('..', __FILE__)
17
+ end
18
+
19
+ def self.template_root
20
+ @template_root ||= File.expand_path('../templates', source_root)
21
+ end
14
22
  end
23
+
24
+ I18n.load_path << File.join(ConfigBuilder.template_root, 'locales/en.yml')
@@ -0,0 +1,245 @@
1
+ require 'spec_helper'
2
+
3
+ describe ConfigBuilder::Filter::Roles do
4
+
5
+ def dup(o)
6
+ Marshal.load(Marshal.dump(o))
7
+ end
8
+
9
+ let(:roles) do
10
+ {
11
+ 'shell-provisioner' => {
12
+ 'provisioners' => [
13
+ {'type' => 'shell', 'inline' => '/usr/bin/sl'},
14
+ ],
15
+ },
16
+ 'puppet-provisioner' => {
17
+ 'provisioners' => [
18
+ {'type' => 'puppet', 'manifest' => 'sl.pp'},
19
+ {'type' => 'puppet', 'manifest' => 'starwars.pp'},
20
+ ],
21
+ },
22
+ 'potato-provisioner' => {
23
+ 'provisioners' => [
24
+ {'type' => 'potato', 'potato' => 'POHTAHTO.pp'},
25
+ ],
26
+ },
27
+ 'folders-12' => {
28
+ 'synced_folders' => [
29
+ {'guest_path' => '/guest-1', 'host_path' => './host-1'},
30
+ {'guest_path' => '/guest-2', 'host_path' => './host-2'},
31
+ ],
32
+ },
33
+ 'folders-34' => {
34
+ 'synced_folders' => [
35
+ {'guest_path' => '/guest-3', 'host_path' => './host-3'},
36
+ {'guest_path' => '/guest-4', 'host_path' => './host-4'},
37
+ ],
38
+ }
39
+ }
40
+ end
41
+
42
+ let(:vms) do
43
+ [
44
+ {'name' => 'master'},
45
+ {'name' => 'debian-6-agent'},
46
+ ]
47
+ end
48
+
49
+
50
+ describe "without a top level roles key" do
51
+
52
+ let(:config) do
53
+ {'vms' => vms}
54
+ end
55
+
56
+ it "doesn't alter the structure" do
57
+ input = dup(config)
58
+ output = subject.run(input)
59
+
60
+ expect(output).to eq config
61
+ end
62
+ end
63
+
64
+ describe 'removing the role' do
65
+ let(:config) do
66
+ {
67
+ 'vms' => [{'name' => 'master'}],
68
+ 'roles' => roles,
69
+ }
70
+ end
71
+
72
+ it 'strips out the roles key' do
73
+ output = subject.run(config)
74
+ expect(output).to_not have_key 'roles'
75
+ end
76
+ end
77
+
78
+ describe 'and a vm with no roles' do
79
+ let(:vms) { [{'name' => 'master'}] }
80
+
81
+ let(:config) do
82
+ {
83
+ 'vms' => vms,
84
+ 'roles' => roles,
85
+ }
86
+ end
87
+
88
+ it "doesn't alter the vm" do
89
+ input = dup(config)
90
+ output = subject.run(input)
91
+ expect(output['vms']).to eq vms
92
+ end
93
+ end
94
+
95
+ describe 'and one vm' do
96
+ describe 'with one role' do
97
+ let(:vms) { [{'name' => 'master', 'roles' => 'shell-provisioner'}] }
98
+
99
+ let(:config) do
100
+ {
101
+ 'vms' => vms,
102
+ 'roles' => roles,
103
+ }
104
+ end
105
+
106
+ let(:filtered_vm) do
107
+ output = subject.run(config)
108
+ output['vms'][0]
109
+ end
110
+
111
+ it "removes the 'roles' key" do
112
+ expect(filtered_vm).to_not have_key 'roles'
113
+ end
114
+
115
+ it 'applies the role' do
116
+ expected = [{
117
+ 'type' => 'shell',
118
+ 'inline' => '/usr/bin/sl',
119
+ }]
120
+
121
+ expect(filtered_vm['provisioners']).to eq expected
122
+ end
123
+ end
124
+
125
+ describe 'with two roles' do
126
+ let(:vms) do
127
+ [{
128
+ 'name' => 'master',
129
+ 'roles' => ['shell-provisioner', 'folders-12'],
130
+ }]
131
+ end
132
+
133
+ let(:config) do
134
+ {
135
+ 'vms' => vms,
136
+ 'roles' => roles,
137
+ }
138
+ end
139
+
140
+ let(:filtered_vm) do
141
+ output = subject.run(config)
142
+ output['vms'][0]
143
+ end
144
+
145
+ it 'applies all of the roles' do
146
+ expected_prov = [{
147
+ 'type' => 'shell',
148
+ 'inline' => '/usr/bin/sl',
149
+ }]
150
+
151
+ expected_folder = [
152
+ {'guest_path' => '/guest-1', 'host_path' => './host-1'},
153
+ {'guest_path' => '/guest-2', 'host_path' => './host-2'},
154
+ ]
155
+
156
+ expect(filtered_vm['provisioners']).to eq expected_prov
157
+ expect(filtered_vm['synced_folders']).to eq expected_folder
158
+ end
159
+ end
160
+ end
161
+
162
+ describe "with multiple VMs and shared roles" do
163
+ let(:vms) do
164
+ [
165
+ {
166
+ 'name' => 'master',
167
+ 'roles' => ['shell-provisioner', 'potato-provisioner', 'folders-12', 'folders-34'],
168
+ },
169
+ {
170
+ 'name' => 'agent',
171
+ 'roles' => ['shell-provisioner', 'puppet-provisioner', 'folders-34'],
172
+ }
173
+ ]
174
+ end
175
+
176
+ let(:config) do
177
+ {
178
+ 'vms' => vms,
179
+ 'roles' => roles,
180
+ }
181
+ end
182
+
183
+ let(:filtered_vms) do
184
+ output = subject.run(config)
185
+ output['vms']
186
+ end
187
+
188
+ describe 'the first node' do
189
+ let(:vm) { filtered_vms[0] }
190
+ it 'has the provisioners set in the right order' do
191
+ expected = [
192
+ {'type' => 'potato', 'potato' => 'POHTAHTO.pp'},
193
+ {'type' => 'shell', 'inline' => '/usr/bin/sl'},
194
+ ]
195
+ expect(vm['provisioners']).to eq expected
196
+ end
197
+
198
+ it 'has the synced folders set in the right order' do
199
+ expected = [
200
+ {'guest_path' => '/guest-3', 'host_path' => './host-3'},
201
+ {'guest_path' => '/guest-4', 'host_path' => './host-4'},
202
+ {'guest_path' => '/guest-1', 'host_path' => './host-1'},
203
+ {'guest_path' => '/guest-2', 'host_path' => './host-2'},
204
+ ]
205
+
206
+ expect(vm['synced_folders']).to eq expected
207
+ end
208
+ end
209
+
210
+ describe 'the second node' do
211
+ let(:vm) { filtered_vms[1] }
212
+
213
+ it 'has the provisioners set in the right order' do
214
+ expected = [
215
+ {'type' => 'puppet', 'manifest' => 'sl.pp'},
216
+ {'type' => 'puppet', 'manifest' => 'starwars.pp'},
217
+ {'type' => 'shell', 'inline' => '/usr/bin/sl'},
218
+ ]
219
+ expect(vm['provisioners']).to eq expected
220
+ end
221
+
222
+ it 'has the synced folders set in the right order' do
223
+ expected = [
224
+ {'guest_path' => '/guest-3', 'host_path' => './host-3'},
225
+ {'guest_path' => '/guest-4', 'host_path' => './host-4'},
226
+ ]
227
+
228
+ expect(vm['synced_folders']).to eq expected
229
+ end
230
+ end
231
+ end
232
+
233
+ describe 'when a class references a non-existent role' do
234
+ let(:config) do
235
+ {
236
+ 'vms' => [{'name' => 'master', 'roles' => 'nope'}],
237
+ 'roles' => {'yep' => {'box' => 'moxxi'}}
238
+ }
239
+
240
+ it 'raises an error' do
241
+ expect { subject.run(config) }.to raise_error, /Couldn't find role named .*nope.*/
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+ require 'config_builder'
3
+
4
+ require 'rspec'
@@ -0,0 +1,11 @@
1
+ ---
2
+ en:
3
+ config_builder:
4
+ class_registry:
5
+ unknown_entry: |-
6
+ Unable to locate a class associated with the identifier %{identifier} in the registry
7
+ %{registry}. The relevant plugin may have not loaded the config_builder extension.
8
+ The identifiers registered with %{registry} are %{identifiers}.
9
+ duplicate_entry: |-
10
+ The class registry %{registry} already has a class registered with identifier %{identifer}
11
+ and cannot replace the existing entry.
@@ -16,4 +16,6 @@ Gem::Specification.new do |gem|
16
16
 
17
17
  gem.files = %x{git ls-files -z}.split("\0")
18
18
  gem.require_path = 'lib'
19
+
20
+ gem.add_development_dependency 'rspec', '~> 2.14.0'
19
21
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vagrant-config_builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,14 +9,32 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-08 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2013-08-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.14.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.14.0
14
30
  description:
15
31
  email: adrien@somethingsinistral.net
16
32
  executables: []
17
33
  extensions: []
18
34
  extra_rdoc_files: []
19
35
  files:
36
+ - CHANGELOG
37
+ - Gemfile
20
38
  - LICENSE
21
39
  - README.markdown
22
40
  - examples/roles.yaml
@@ -24,6 +42,7 @@ files:
24
42
  - lib/config_builder.rb
25
43
  - lib/config_builder/action/load_extensions.rb
26
44
  - lib/config_builder/class_registry.rb
45
+ - lib/config_builder/extension_handler.rb
27
46
  - lib/config_builder/filter.rb
28
47
  - lib/config_builder/filter/roles.rb
29
48
  - lib/config_builder/filter_stack.rb
@@ -46,6 +65,9 @@ files:
46
65
  - lib/config_builder/runner.rb
47
66
  - lib/config_builder/version.rb
48
67
  - lib/vagrant-config_builder.rb
68
+ - spec/config_builder/filter/roles_spec.rb
69
+ - spec/spec_helper.rb
70
+ - templates/locales/en.yml
49
71
  - vagrant-config_builder.gemspec
50
72
  homepage: https://github.com/adrienthebo/vagrant-config_builder
51
73
  licenses: