bocuse 0.1.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/bin/bocuse +19 -0
- data/lib/bocuse/configuration.rb +103 -0
- data/lib/bocuse/file.rb +72 -0
- data/lib/bocuse/project.rb +155 -0
- data/lib/bocuse/unit.rb +44 -0
- data/lib/bocuse/value.rb +43 -0
- data/lib/bocuse.rb +7 -0
- data/spec/integration/cli_spec.rb +21 -0
- data/spec/integration/complex_spec.rb +57 -0
- data/spec/integration/helpers_spec.rb +14 -0
- data/spec/integration/node_finding_spec.rb +18 -0
- data/spec/integration/templates_spec.rb +33 -0
- data/spec/lib/bocuse/configuration_spec.rb +148 -0
- data/spec/lib/bocuse/file_spec.rb +52 -0
- data/spec/lib/bocuse/project_spec.rb +42 -0
- data/spec/lib/bocuse/value_spec.rb +23 -0
- metadata +176 -0
data/bin/bocuse
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
|
4
|
+
require 'thor'
|
5
|
+
|
6
|
+
$:.unshift File.expand_path '../../lib', __FILE__
|
7
|
+
require 'bocuse'
|
8
|
+
|
9
|
+
class BocuseCLI < Thor
|
10
|
+
desc "compile NODE_FQDN", "Compiles the node given and outputs its JSON configuration."
|
11
|
+
def compile(node_fqdn)
|
12
|
+
project = Bocuse::Project.new(Dir.pwd)
|
13
|
+
node_config = project.nodes[node_fqdn]
|
14
|
+
|
15
|
+
puts MultiJson.encode node_config.to_h
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
BocuseCLI.start
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Bocuse
|
2
|
+
|
3
|
+
# This is the core class of bocuse.
|
4
|
+
#
|
5
|
+
# Its functions are:
|
6
|
+
# * to be able to spit out a configuration hash when prompted.
|
7
|
+
# * to be configurable
|
8
|
+
#
|
9
|
+
# It will mainly return proxies that will lodge themselves in its internal
|
10
|
+
# hash. The proxies usually represent an internal hash value.
|
11
|
+
#
|
12
|
+
class Configuration
|
13
|
+
|
14
|
+
attr_reader :store,
|
15
|
+
:unresolved_block
|
16
|
+
|
17
|
+
def initialize(hash=nil, &block)
|
18
|
+
@store = hash && hash.dup || Hash.new
|
19
|
+
|
20
|
+
call(block) if block
|
21
|
+
end
|
22
|
+
|
23
|
+
# Explicit accessor for POROs.
|
24
|
+
#
|
25
|
+
# Note: Unwraps Values.
|
26
|
+
#
|
27
|
+
def [] key
|
28
|
+
value = @store[key]
|
29
|
+
value.to_h rescue value
|
30
|
+
end
|
31
|
+
|
32
|
+
# Sets key to value.
|
33
|
+
#
|
34
|
+
def []= key, value
|
35
|
+
@store[key] = value
|
36
|
+
end
|
37
|
+
|
38
|
+
# The main method.
|
39
|
+
#
|
40
|
+
# It will react to method calls, install the right keys on the internal
|
41
|
+
# store and return proxies on which callers can perform operations, e.g.
|
42
|
+
# <<.
|
43
|
+
#
|
44
|
+
# Note: The user only interacts with Configurations. Not with the internal
|
45
|
+
# values.
|
46
|
+
#
|
47
|
+
# Except when the user explicitly takes the
|
48
|
+
# values out using [].
|
49
|
+
#
|
50
|
+
def method_missing name, *args
|
51
|
+
case args.size
|
52
|
+
when 0
|
53
|
+
if block_given?
|
54
|
+
subconfig = Configuration.new &Proc.new
|
55
|
+
store[name] = subconfig
|
56
|
+
else
|
57
|
+
store[name] ||= Value.new
|
58
|
+
end
|
59
|
+
when 1
|
60
|
+
store[name] = Value.new args.first
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns a configuration hash.
|
65
|
+
#
|
66
|
+
# NOTE: Resolves any unresolved blocks.
|
67
|
+
#
|
68
|
+
# NOTE: Call to_json on this to get a JSON representation of the hash.
|
69
|
+
#
|
70
|
+
def to_h
|
71
|
+
copy = {}
|
72
|
+
store.each do |key, value|
|
73
|
+
value = value.to_h if value.respond_to?(:to_h)
|
74
|
+
copy[key] = value if value
|
75
|
+
end
|
76
|
+
copy
|
77
|
+
end
|
78
|
+
|
79
|
+
# Performs a deep duplication of this configuration object. This is mainly
|
80
|
+
# used by the user for quickly generating a lot of copies of a template.
|
81
|
+
#
|
82
|
+
def dup(&block)
|
83
|
+
dup_cfg = Configuration.new
|
84
|
+
|
85
|
+
store.each do |key, value|
|
86
|
+
dup_cfg[key] = value.dup
|
87
|
+
end
|
88
|
+
|
89
|
+
dup_cfg.call(block) if block
|
90
|
+
dup_cfg
|
91
|
+
end
|
92
|
+
|
93
|
+
# Executes the block such that it may modify this object.
|
94
|
+
#
|
95
|
+
def call(block)
|
96
|
+
fail ArgumentError unless block
|
97
|
+
|
98
|
+
block.call(self) if block.arity>0
|
99
|
+
instance_eval(&block) if block.arity == 0
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
data/lib/bocuse/file.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
|
2
|
+
# Represents a bocuse File.
|
3
|
+
#
|
4
|
+
# This is usually a file that contains one or more of the
|
5
|
+
# following directives:
|
6
|
+
# * node
|
7
|
+
# * template
|
8
|
+
#
|
9
|
+
# It is used to generate a config hash from the given files.
|
10
|
+
#
|
11
|
+
class Bocuse::File
|
12
|
+
attr_reader :path
|
13
|
+
|
14
|
+
# When inside a construct that has a configuration associated, this will
|
15
|
+
# return that configuration instance.
|
16
|
+
attr_reader :current_configuration
|
17
|
+
|
18
|
+
def initialize(path, context)
|
19
|
+
@path = path
|
20
|
+
@context = context
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a config hash.
|
24
|
+
#
|
25
|
+
# Call to_h on the returned configuration to get the config hash.
|
26
|
+
#
|
27
|
+
# Will return nil if there is no configuration to speak of inside the file.
|
28
|
+
# Use "node" or "template" to define a configuration.
|
29
|
+
#
|
30
|
+
def evaluate
|
31
|
+
::File.open path, 'r' do |file|
|
32
|
+
self.instance_eval file.read, file.path
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# The files read by #evaluate will trigger these methods.
|
37
|
+
#
|
38
|
+
def node name
|
39
|
+
unit = Bocuse::Unit.new(Proc.new, @context)
|
40
|
+
|
41
|
+
configuration_block do |configuration|
|
42
|
+
unit.call(configuration)
|
43
|
+
@context.register_node name, configuration
|
44
|
+
end
|
45
|
+
end
|
46
|
+
def template
|
47
|
+
# Delay template evaluation until someone tries to call include_template.
|
48
|
+
# At that time, we'll have a configuration object to have the template
|
49
|
+
# manipulate.
|
50
|
+
unit = Bocuse::Unit.new(Proc.new, @context)
|
51
|
+
@context.register_template path, unit
|
52
|
+
unit
|
53
|
+
end
|
54
|
+
private
|
55
|
+
def configuration_block
|
56
|
+
# NOTE since thread-safety is not an issue here (who would use threads
|
57
|
+
# to define configuration?), we can use a simple file-global state to
|
58
|
+
# keep track of which configuration is in progress. We use the begin-end
|
59
|
+
# construct to make sure that configurations are not used beyond the end
|
60
|
+
# of the node block.
|
61
|
+
|
62
|
+
configuration = @current_configuration = Bocuse::Configuration.new
|
63
|
+
|
64
|
+
begin
|
65
|
+
yield configuration
|
66
|
+
ensure
|
67
|
+
@current_configuration = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
configuration
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Bocuse
|
4
|
+
# A bocuse project comes in two flavours: simple and co-located with chef.
|
5
|
+
# The simple project is a directory with the subdirectories
|
6
|
+
# nodes/
|
7
|
+
# templates/
|
8
|
+
# lib/
|
9
|
+
#
|
10
|
+
# The nodes/ subdirectory contains all node descriptions, templates/
|
11
|
+
# contains templates and lib/ is added to the load path and can contain code
|
12
|
+
# to help with construction of the configuration.
|
13
|
+
#
|
14
|
+
# When you co-host bocuse with chef, normally you would store all these
|
15
|
+
# directories one level deeper. Here's what your hierarchy should look like
|
16
|
+
# in this case:
|
17
|
+
#
|
18
|
+
# config/
|
19
|
+
# nodes/
|
20
|
+
# templates/
|
21
|
+
# lib/
|
22
|
+
#
|
23
|
+
# This class does most of the base path handling and of the path
|
24
|
+
# manipulation.
|
25
|
+
#
|
26
|
+
class Project
|
27
|
+
attr_reader :base_path
|
28
|
+
|
29
|
+
# Initialize a bocuse project by specifying its base directory.
|
30
|
+
#
|
31
|
+
def initialize(base_directory=nil)
|
32
|
+
base_directory = base_directory || Dir.pwd
|
33
|
+
|
34
|
+
if chef_cohosted?(base_directory)
|
35
|
+
base_directory = ::File.join(base_directory, 'config')
|
36
|
+
end
|
37
|
+
|
38
|
+
@base_path = Pathname.new(base_directory)
|
39
|
+
@templates = Hash.new
|
40
|
+
@nodes = Hash.new
|
41
|
+
|
42
|
+
# Make sure that project libraries can be loaded:
|
43
|
+
lib_dir = base_path.join('lib')
|
44
|
+
$:.unshift lib_dir if ::File.directory?(lib_dir)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns one of the projects files as a {File}.
|
48
|
+
#
|
49
|
+
# @param path [String] a relative or absolute file path
|
50
|
+
# @return [File] file at path below the project directory
|
51
|
+
#
|
52
|
+
def file path
|
53
|
+
path = Pathname.new(path)
|
54
|
+
path = base_path.join(path) unless path.absolute?
|
55
|
+
|
56
|
+
Bocuse::File.new(path, self)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Evaluates a file given by path.
|
60
|
+
#
|
61
|
+
# @param path [String] file path, either absolute or relative to project
|
62
|
+
# base directory
|
63
|
+
# @return [void]
|
64
|
+
#
|
65
|
+
def evaluate path
|
66
|
+
file(path).evaluate
|
67
|
+
end
|
68
|
+
|
69
|
+
# Registers a template for project usage.
|
70
|
+
#
|
71
|
+
def register_template path, template
|
72
|
+
path = path.to_s
|
73
|
+
template_base = base_path.join('templates/').to_s
|
74
|
+
|
75
|
+
# Is this template path below base_path?
|
76
|
+
if path.start_with?(template_base)
|
77
|
+
path = path[template_base.size..-1]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Does the path end in .rb? (most will)
|
81
|
+
if path.end_with?('.rb')
|
82
|
+
path = path.slice 0..-4
|
83
|
+
end
|
84
|
+
|
85
|
+
@templates.store path.to_sym, template
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns a template by name. Official template names can be either
|
89
|
+
# absolute paths or relative paths to template files, but do NOT end in
|
90
|
+
# .rb.
|
91
|
+
#
|
92
|
+
# foo -> templates/foo.rb
|
93
|
+
# /templates/bar -> /usr/templates/bar.rb
|
94
|
+
#
|
95
|
+
def template name
|
96
|
+
unless @templates.has_key?(name.to_sym)
|
97
|
+
file_name = name.to_s
|
98
|
+
file_name += '.rb' unless file_name.end_with?('.rb')
|
99
|
+
|
100
|
+
# Try to find and load this template:
|
101
|
+
template_path = base_path.join('templates', file_name)
|
102
|
+
file(template_path).evaluate
|
103
|
+
|
104
|
+
# A successful load will register the template.
|
105
|
+
end
|
106
|
+
|
107
|
+
@templates.fetch(name.to_sym)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Registers a machine node in the project.
|
111
|
+
#
|
112
|
+
def register_node name, node
|
113
|
+
@nodes.store name, node
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns all nodes in this project as a hash.
|
117
|
+
#
|
118
|
+
# @return [Hash<name,Configuration>] A hash mapping node names to their
|
119
|
+
# configuration objects.
|
120
|
+
#
|
121
|
+
def nodes
|
122
|
+
evaluate_all unless @nodes.size > 0
|
123
|
+
@nodes
|
124
|
+
end
|
125
|
+
|
126
|
+
# Evaluates all files in the project.
|
127
|
+
#
|
128
|
+
# Retrieve the found configurations via
|
129
|
+
# Nodes.find pattern_or_name
|
130
|
+
#
|
131
|
+
# @param dir [String] directory to use as project base directory
|
132
|
+
# @return [void]
|
133
|
+
#
|
134
|
+
def evaluate_all
|
135
|
+
nodes_glob = base_path.join('nodes', '**', '*.rb')
|
136
|
+
|
137
|
+
Dir[nodes_glob].each do |filename|
|
138
|
+
file(filename).evaluate
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class << self
|
143
|
+
def bocuse_path?(path)
|
144
|
+
%w(nodes).all? { |el|
|
145
|
+
::File.directory?(::File.join(path, el)) }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def chef_cohosted?(base)
|
151
|
+
!self.class.bocuse_path?(base) &&
|
152
|
+
self.class.bocuse_path?(::File.join(base, 'config'))
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/bocuse/unit.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Bocuse
|
2
|
+
# A configuration unit.
|
3
|
+
#
|
4
|
+
class Bocuse::Unit
|
5
|
+
# Returns the current configuration, but only during a call to this unit.
|
6
|
+
attr_reader :current_configuration
|
7
|
+
|
8
|
+
def initialize(block, context)
|
9
|
+
@block = block
|
10
|
+
@context = context
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(configuration)
|
14
|
+
@current_configuration = configuration
|
15
|
+
|
16
|
+
# Fill in the config hash argument if the block cares at all.
|
17
|
+
instance_exec(configuration, &@block)
|
18
|
+
ensure
|
19
|
+
@current_configuration = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# Cook adds to the toplevel recipes of this file's configuration.
|
23
|
+
#
|
24
|
+
def cook recipe
|
25
|
+
current_configuration.recipes << recipe
|
26
|
+
end
|
27
|
+
|
28
|
+
# Make the given module a helper module for this node.
|
29
|
+
#
|
30
|
+
def helper_module(mod)
|
31
|
+
extend mod
|
32
|
+
end
|
33
|
+
|
34
|
+
# Include the given template name.
|
35
|
+
#
|
36
|
+
# Note: This could be pushed to the configuration.
|
37
|
+
#
|
38
|
+
def include_template identifier
|
39
|
+
template_block = @context.template identifier
|
40
|
+
|
41
|
+
template_block.call(current_configuration)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/bocuse/value.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Bocuse
|
2
|
+
|
3
|
+
# This is a value object that by default starts out life as not having
|
4
|
+
# decided what exactly it is.
|
5
|
+
#
|
6
|
+
# On first adding another object, it assumes an internal form.
|
7
|
+
#
|
8
|
+
# It will complain if its internal form does not correspond to the assumed
|
9
|
+
# external one, ie. when you want to << something, but it has already
|
10
|
+
# assumed the internal form of a hash (<< does not exist on a hash).
|
11
|
+
#
|
12
|
+
class Value
|
13
|
+
|
14
|
+
module Empty; def self.empty?; true; end end
|
15
|
+
|
16
|
+
def initialize internal = Empty
|
17
|
+
@internal = internal
|
18
|
+
end
|
19
|
+
|
20
|
+
def empty?
|
21
|
+
@internal && @internal.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def []= key, value
|
25
|
+
@internal = {} if empty?
|
26
|
+
@internal[key] = value
|
27
|
+
end
|
28
|
+
|
29
|
+
def << element
|
30
|
+
@internal = [] if empty?
|
31
|
+
@internal << element
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_h
|
35
|
+
@internal unless Empty == @internal
|
36
|
+
end
|
37
|
+
|
38
|
+
def dup
|
39
|
+
Value.new(@internal.dup)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/lib/bocuse.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'multi_json'
|
4
|
+
|
5
|
+
describe "CLI:" do
|
6
|
+
describe 'compile NODE_FQDN' do
|
7
|
+
it "parses nodes and outputs a single nodes JSON" do
|
8
|
+
Dir.chdir File.expand_path('../../files/complex', __FILE__)
|
9
|
+
|
10
|
+
# Explicitly call the current binary.
|
11
|
+
#
|
12
|
+
|
13
|
+
MultiJson.load(`#{project_path('bin/bocuse')} \
|
14
|
+
compile complex.production.example.com`).should ==
|
15
|
+
{
|
16
|
+
"recipes"=>["nginx", "git", "app::install", "app::deploy"],
|
17
|
+
"key_location" =>
|
18
|
+
"/Users/kschiess/git/own/bocuse/spec/files/complex/config/nodes/production/key.txt"}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe 'complex example with templates' do
|
6
|
+
let(:project) { Bocuse::Project.new(fixture('complex')) }
|
7
|
+
|
8
|
+
describe 'hash of node "test.staging.example.com"' do
|
9
|
+
it 'looks right' do
|
10
|
+
configuration = project.evaluate('nodes/staging/complex.rb')
|
11
|
+
|
12
|
+
configuration.to_h.should == {
|
13
|
+
:user => "root",
|
14
|
+
:users => [
|
15
|
+
{
|
16
|
+
:username => "some_user",
|
17
|
+
:password => "toooootally_secret",
|
18
|
+
:authorized_keys => ["key1", "key2"],
|
19
|
+
:shell => "/bin/someshell",
|
20
|
+
:gid => 1000,
|
21
|
+
:uid => 1000,
|
22
|
+
:sudo => true
|
23
|
+
}
|
24
|
+
],
|
25
|
+
:webserver => {
|
26
|
+
:domains => ["staging.example.com"],
|
27
|
+
:listen_ip => "1.2.3.4",
|
28
|
+
:ssl_support => true
|
29
|
+
},
|
30
|
+
:server => {
|
31
|
+
:ip => "11.22.33.44",
|
32
|
+
:external_net => 28,
|
33
|
+
:internal_ip => "1.1.1.1",
|
34
|
+
:internal_net => "10",
|
35
|
+
:context => 20
|
36
|
+
},
|
37
|
+
:cache1 => {
|
38
|
+
:address => "1.1.1.1"
|
39
|
+
},
|
40
|
+
:cache2 => {
|
41
|
+
:address => "1.1.1.1"
|
42
|
+
},
|
43
|
+
:empty => true,
|
44
|
+
:recipes => ["nginx", "git", "app::install", "app::deploy"]
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
describe 'hash of node "complex.production.example.com"' do
|
49
|
+
it "contains only recipes that are relevant to the node" do
|
50
|
+
config = project.nodes.
|
51
|
+
find { |name, _| name == 'complex.production.example.com' }.
|
52
|
+
last
|
53
|
+
|
54
|
+
config.to_h[:recipes].should =~ %w(nginx git app::install app::deploy)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'helpers example' do
|
4
|
+
let(:project) { Bocuse::Project.new(fixture('helpers')) }
|
5
|
+
|
6
|
+
describe 'loading "node"' do
|
7
|
+
it 'works' do
|
8
|
+
configuration = project.nodes['node']
|
9
|
+
configuration[:foo].should == 'bar'
|
10
|
+
configuration[:nested][:foo].should == 'bar'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe 'finding nodes in files/many' do
|
6
|
+
let(:project) { Bocuse::Project.new(fixture('many')) }
|
7
|
+
|
8
|
+
describe 'loading all nodes' do
|
9
|
+
it 'works with defaults' do
|
10
|
+
project.should have(4).nodes
|
11
|
+
end
|
12
|
+
it 'works with specific string' do
|
13
|
+
nodes = project.nodes.select { |name, _| 'subfolder1' === name }
|
14
|
+
nodes.size.should == 1
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe 'Templates' do
|
6
|
+
let(:project) { Bocuse::Project.new(fixture('complex')) }
|
7
|
+
|
8
|
+
describe 'loading' do
|
9
|
+
it 'works' do
|
10
|
+
config = Bocuse::Configuration.new
|
11
|
+
project.template(:users).call(config)
|
12
|
+
|
13
|
+
config.to_h.should == {
|
14
|
+
:user => "root",
|
15
|
+
:users => [
|
16
|
+
{
|
17
|
+
:username => "some_user",
|
18
|
+
:password => "toooootally_secret",
|
19
|
+
:authorized_keys => ["key1", "key2"],
|
20
|
+
:shell => "/bin/someshell",
|
21
|
+
:gid => 1000,
|
22
|
+
:uid => 1000,
|
23
|
+
:sudo => true
|
24
|
+
}
|
25
|
+
]
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#require_ingredients' do
|
31
|
+
it "complains if the current configuration doesn't contain the given keys"
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Bocuse::Configuration do
|
6
|
+
|
7
|
+
let(:configuration) { described_class.new }
|
8
|
+
|
9
|
+
describe 'construction' do
|
10
|
+
it "constructs given a hash" do
|
11
|
+
h = { foo: 'bar' }
|
12
|
+
|
13
|
+
config = described_class.new(h)
|
14
|
+
|
15
|
+
config.to_h.should == h
|
16
|
+
config.to_h.should_not equal(h)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'uninitialized values' do
|
21
|
+
it 'will be ignored' do
|
22
|
+
something = configuration.something
|
23
|
+
|
24
|
+
configuration.to_h.should == {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
describe '.something as a value' do
|
28
|
+
it 'returns the right value' do
|
29
|
+
configuration.something.to_h.should == nil # Bocuse::Value::Empty is returned as nil
|
30
|
+
end
|
31
|
+
it 'returns the right value' do
|
32
|
+
configuration.something nil # explicit nil
|
33
|
+
|
34
|
+
configuration.something.to_h.should == nil
|
35
|
+
end
|
36
|
+
it 'returns the right value' do
|
37
|
+
configuration.something 'value'
|
38
|
+
|
39
|
+
configuration.something.to_h.should == 'value' # <= This is the tested getter.
|
40
|
+
end
|
41
|
+
it 'is modifiable in place' do
|
42
|
+
something = configuration.something
|
43
|
+
|
44
|
+
something << "hello"
|
45
|
+
|
46
|
+
configuration.something.to_h.should == ["hello"]
|
47
|
+
end
|
48
|
+
it 'is modifiable in place' do
|
49
|
+
something = configuration.something
|
50
|
+
|
51
|
+
something[:key] = "value"
|
52
|
+
|
53
|
+
configuration.something.to_h.should == { :key => 'value' }
|
54
|
+
end
|
55
|
+
it 'is modifiable in place' do
|
56
|
+
something = configuration.something
|
57
|
+
|
58
|
+
something[:key] = nil # Explicit nil.
|
59
|
+
|
60
|
+
configuration.to_h.should == { :something => { :key => nil } }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
describe '.something "value"' do
|
64
|
+
it 'stores the value correctly' do
|
65
|
+
configuration.something 'value'
|
66
|
+
|
67
|
+
configuration.to_h.should == { :something => 'value' }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
describe '.something do ... end' do
|
71
|
+
it 'stores the value correctly' do
|
72
|
+
configuration.something do
|
73
|
+
key 'value'
|
74
|
+
end
|
75
|
+
|
76
|
+
configuration.to_h.should == { :something => { :key => 'value' } }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
describe '.something do |param| ... end' do
|
80
|
+
it "doesn't allow implicit access" do
|
81
|
+
expect {
|
82
|
+
configuration.something do |something|
|
83
|
+
foo 'bar'
|
84
|
+
end
|
85
|
+
}.to raise_error
|
86
|
+
end
|
87
|
+
it "represents a configuration, one level deeper" do
|
88
|
+
result = nil
|
89
|
+
configuration.something do |something|
|
90
|
+
result = something
|
91
|
+
end
|
92
|
+
|
93
|
+
result.should be_instance_of(described_class)
|
94
|
+
end
|
95
|
+
it 'stores the value correctly' do
|
96
|
+
configuration.something do |cfg|
|
97
|
+
cfg.key 'value'
|
98
|
+
end
|
99
|
+
|
100
|
+
configuration.to_h.should == { :something => { :key => 'value' } }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
describe '#[]' do
|
104
|
+
it 'offers a [] method' do
|
105
|
+
configuration[:something].should == nil
|
106
|
+
end
|
107
|
+
it 'returns the right value' do
|
108
|
+
configuration.something :value
|
109
|
+
|
110
|
+
configuration[:something].should == :value
|
111
|
+
end
|
112
|
+
it 'returns the right value' do
|
113
|
+
configuration.something do
|
114
|
+
key 'value'
|
115
|
+
end
|
116
|
+
|
117
|
+
configuration[:something].should == { :key => 'value' }
|
118
|
+
end
|
119
|
+
it 'is not modifiable in place as it is a PORO' do
|
120
|
+
something = configuration[:something]
|
121
|
+
|
122
|
+
expect { something << "hello" }.to raise_error
|
123
|
+
|
124
|
+
# configuration.to_h.should == { :something => ["hello"] } # This might be expected by a user.
|
125
|
+
end
|
126
|
+
end
|
127
|
+
describe '#dup' do
|
128
|
+
before(:each) {
|
129
|
+
configuration.foo do
|
130
|
+
bar []
|
131
|
+
end }
|
132
|
+
let(:dup) { configuration.dup }
|
133
|
+
|
134
|
+
it "is performed in depth" do
|
135
|
+
configuration[:foo][:bar].should_not equal(dup[:foo][:bar])
|
136
|
+
end
|
137
|
+
it "returns a duplicate" do
|
138
|
+
configuration.to_h.should == dup.to_h
|
139
|
+
end
|
140
|
+
it "takes a block, allowing modification of the result" do
|
141
|
+
dup = configuration.dup do |cfg|
|
142
|
+
cfg.baz 'this is new'
|
143
|
+
end
|
144
|
+
|
145
|
+
dup[:baz].should == 'this is new'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'bocuse/file'
|
4
|
+
|
5
|
+
describe Bocuse::File do
|
6
|
+
let(:context) { flexmock('context') }
|
7
|
+
let(:file) { described_class.new('path', context) }
|
8
|
+
|
9
|
+
before(:each) {
|
10
|
+
context.
|
11
|
+
should_receive(
|
12
|
+
:register_node, :register_template).by_default
|
13
|
+
}
|
14
|
+
|
15
|
+
it "checks that helper inclusion only works for the current file"
|
16
|
+
|
17
|
+
describe 'node' do
|
18
|
+
it 'works correctly' do
|
19
|
+
configuration = file.node 'name' do |cfg|
|
20
|
+
cfg.something :value
|
21
|
+
cfg.something_else do
|
22
|
+
key 'value'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
configuration.to_h.should == { :something => :value, :something_else => { :key => "value" } }
|
27
|
+
end
|
28
|
+
it "registers the node in the context" do
|
29
|
+
context.should_receive(:register_node).
|
30
|
+
with('name', Bocuse::Configuration).once
|
31
|
+
|
32
|
+
file.node('name') { }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'template' do
|
37
|
+
it 'works correctly' do
|
38
|
+
template = file.template do |cfg|
|
39
|
+
cfg.something :value
|
40
|
+
cfg.something_else do
|
41
|
+
key 'value'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
config = Bocuse::Configuration.new
|
46
|
+
template.call config
|
47
|
+
|
48
|
+
config.to_h.should == { :something => :value, :something_else => { :key => "value" } }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'bocuse/project'
|
4
|
+
|
5
|
+
describe Bocuse::Project do
|
6
|
+
let(:project) { described_class.new(fixture('complex')) }
|
7
|
+
|
8
|
+
describe '#base_path' do
|
9
|
+
it "detects a simple project" do
|
10
|
+
described_class.new(fixture('complex/config')).
|
11
|
+
base_path.should == fixture('complex/config')
|
12
|
+
end
|
13
|
+
it "detects a co-hosted project" do
|
14
|
+
described_class.new(fixture('complex')).
|
15
|
+
base_path.should == fixture('complex/config')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
describe '#file' do
|
19
|
+
let(:file) { project.file('nodes/production/complex') }
|
20
|
+
|
21
|
+
it "returns a Bocuse::Unit instance" do
|
22
|
+
file.should be_instance_of(Bocuse::File)
|
23
|
+
end
|
24
|
+
it "has the correct expanded path" do
|
25
|
+
file.path.should ==
|
26
|
+
fixture('complex/config/nodes/production/complex')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
describe '#register_node' do
|
30
|
+
|
31
|
+
end
|
32
|
+
describe '#register_template / #template' do
|
33
|
+
before(:each) {
|
34
|
+
base = project.base_path
|
35
|
+
template = base.join('templates', 'foo', 'bar.rb')
|
36
|
+
project.register_template template, :configuration }
|
37
|
+
|
38
|
+
it "returns the template" do
|
39
|
+
project.template('foo/bar').should == :configuration
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Bocuse::Value do
|
6
|
+
|
7
|
+
describe 'to_h' do
|
8
|
+
it 'works correctly' do
|
9
|
+
Bocuse::Value.new.to_h.should == nil
|
10
|
+
end
|
11
|
+
it 'works correctly' do
|
12
|
+
value = Bocuse::Value.new
|
13
|
+
value << 1
|
14
|
+
value.to_h.should == [1]
|
15
|
+
end
|
16
|
+
it 'works correctly' do
|
17
|
+
value = Bocuse::Value.new
|
18
|
+
value[:address] = '1.2.3.4'
|
19
|
+
value.to_h.should == { :address => '1.2.3.4' }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bocuse
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Florian Hanke
|
9
|
+
- Kaspar Schiess
|
10
|
+
- Jens-Christian Fischer
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2012-07-06 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rspec
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '0'
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ! '>='
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '0'
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: guard
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: guard-rspec
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: flexmock
|
66
|
+
requirement: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
type: :development
|
73
|
+
prerelease: false
|
74
|
+
version_requirements: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: multi_json
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 1.0.0
|
88
|
+
type: :runtime
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 1.0.0
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: thor
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.15'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ~>
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0.15'
|
112
|
+
description: ! " bocuse teaches chef-solo a few tricks. A strict front-end to chef-solo,
|
113
|
+
\n it reads a configuration syntax that is under source control and \n generates
|
114
|
+
JSON for chef-solo.\n\n This library puts the full power of Ruby at your fingertips
|
115
|
+
when composing\n configuration for your nodes using templates and helpers. It
|
116
|
+
is the \n missing link between puppet and chef. \n"
|
117
|
+
email:
|
118
|
+
- florian.hanke@technologyastronauts.ch
|
119
|
+
- kaspar.schiess@technologyastronauts.ch
|
120
|
+
- jcf@mobino.com
|
121
|
+
executables:
|
122
|
+
- bocuse
|
123
|
+
extensions: []
|
124
|
+
extra_rdoc_files: []
|
125
|
+
files:
|
126
|
+
- lib/bocuse/configuration.rb
|
127
|
+
- lib/bocuse/file.rb
|
128
|
+
- lib/bocuse/project.rb
|
129
|
+
- lib/bocuse/unit.rb
|
130
|
+
- lib/bocuse/value.rb
|
131
|
+
- lib/bocuse.rb
|
132
|
+
- spec/integration/cli_spec.rb
|
133
|
+
- spec/integration/complex_spec.rb
|
134
|
+
- spec/integration/helpers_spec.rb
|
135
|
+
- spec/integration/node_finding_spec.rb
|
136
|
+
- spec/integration/templates_spec.rb
|
137
|
+
- spec/lib/bocuse/configuration_spec.rb
|
138
|
+
- spec/lib/bocuse/file_spec.rb
|
139
|
+
- spec/lib/bocuse/project_spec.rb
|
140
|
+
- spec/lib/bocuse/value_spec.rb
|
141
|
+
- bin/bocuse
|
142
|
+
homepage: http://github.com/mobino/bocuse
|
143
|
+
licenses: []
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ! '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
|
+
none: false
|
156
|
+
requirements:
|
157
|
+
- - ! '>='
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
requirements: []
|
161
|
+
rubyforge_project:
|
162
|
+
rubygems_version: 1.8.24
|
163
|
+
signing_key:
|
164
|
+
specification_version: 3
|
165
|
+
summary: A front-end language to chef-solo.
|
166
|
+
test_files:
|
167
|
+
- spec/integration/cli_spec.rb
|
168
|
+
- spec/integration/complex_spec.rb
|
169
|
+
- spec/integration/helpers_spec.rb
|
170
|
+
- spec/integration/node_finding_spec.rb
|
171
|
+
- spec/integration/templates_spec.rb
|
172
|
+
- spec/lib/bocuse/configuration_spec.rb
|
173
|
+
- spec/lib/bocuse/file_spec.rb
|
174
|
+
- spec/lib/bocuse/project_spec.rb
|
175
|
+
- spec/lib/bocuse/value_spec.rb
|
176
|
+
has_rdoc:
|