yutani 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bed76670778f66c3a40d8d86e5de3cef50b718cb
4
+ data.tar.gz: 9f061e4858b478a843e4a0d786f050c1b8436e15
5
+ SHA512:
6
+ metadata.gz: d5209d59b656258ce292a7f0217996f807fc1fae43aab035bda4c56d4a556038a7603aced3f44c3e6ab9336f6d4f814f2fbaeb60b89c07cdb77c5fc923edb76a
7
+ data.tar.gz: 7cb439bef4a54df1e25239a075ffb694743286925ede586d1049deaf888eef8f2e16c1c744a5d6326e9c79a828b76759da342a05e92f0ca2ed417ad445742463
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ https://releases.hashicorp.com/terraform/0.7.13/terraform_0.7.13_linux_amd64.zip
data/bin/yutani ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'yutani'
7
+
8
+ Yutani::Cli::main(ARGV)
data/lib/yutani/cli.rb ADDED
@@ -0,0 +1,77 @@
1
+ require 'thor'
2
+ require 'yaml'
3
+ require 'guard'
4
+ require 'guard/commander'
5
+
6
+ module Yutani
7
+ class Cli < Thor
8
+ map '-v' => :version, '--version' => :version
9
+ map '--hiera-config-file' => :hiera_config_file
10
+
11
+ def self.main(args)
12
+ begin
13
+ Cli.start(args)
14
+ rescue StandardError => e
15
+ Yutani.logger.fatal "#{e.class.name} #{e.message}"
16
+ Yutani.logger.fatal Yutani::Cli.format_backtrace(e.backtrace) unless e.backtrace.empty?
17
+
18
+ exit 1
19
+ end
20
+ end
21
+
22
+ desc 'build', 'Evaluates the given script and creates terraform files'
23
+ def build(script)
24
+ Yutani.build_from_file(script)
25
+ end
26
+
27
+ # we need to know these things:
28
+ # * the directory to restrict to watching for changes
29
+ # * the script that build should evaluate
30
+ # * the glob - this is hardcoded to *.rb
31
+ desc 'watch', 'Run build upon changes to files/directories'
32
+ def watch(script, script_dir)
33
+ guardfile = <<-EOF
34
+ run_build = proc do
35
+ system("yutani build #{script}")
36
+ end
37
+
38
+ guard :yield, { :run_on_modifications => run_build } do
39
+ watch(%r|^.*\.rb$|)
40
+ end
41
+ EOF
42
+ Guard.start(guardfile_contents: guardfile, watchdir: script_dir, debug: true)
43
+ end
44
+
45
+ desc 'version', 'Prints the current version of Yutani'
46
+ def version
47
+ puts Yutani::VERSION
48
+ end
49
+
50
+ desc 'init', 'Initialize with a basic setup'
51
+ def init
52
+ if File.exists? '.yutani.yml'
53
+ puts ".yutani.yml already exists, skipping initialization"
54
+ else
55
+ File.open('.yutani.yml', 'w+') do |f|
56
+ f.write Yutani::Config::DEFAULTS.to_yaml(indent: 2)
57
+ puts ".yutani.yml created"
58
+ end
59
+
60
+ hiera_dir = Yutani::Config::DEFAULTS['hiera_config'][:yaml][:datadir]
61
+ FileUtils.mkdir hiera_dir unless Dir.exists? hiera_dir
62
+
63
+ common_yml = File.join(hiera_dir, 'common.yaml')
64
+ unless File.exists? common_yml
65
+ File.new(common_yml, 'w+')
66
+ puts "#{common_yml} created"
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def self.format_backtrace(bt)
74
+ "Backtrace: #{bt.join("\n from ")}"
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,34 @@
1
+ module Yutani
2
+ class Config < Hash
3
+ CONFIG_FILE = '.yutani.yml'
4
+
5
+ # Strings rather than symbols are used for compatibility with YAML.
6
+ DEFAULTS = Config[{
7
+ "terraform_dir" => "terraform",
8
+ "hiera_config" => {
9
+ :backends => ["yaml"],
10
+ :hierarchy => ["common"],
11
+ :yaml => {
12
+ :datadir=>"hiera"
13
+ },
14
+ :logger => "noop"
15
+ }
16
+ }]
17
+
18
+ class << self
19
+ # Returns a Configuration filled with defaults and fixed for common
20
+ # problems and backwards-compatibility.
21
+ def from(user_config)
22
+ DEFAULTS.merge Config[user_config]
23
+ end
24
+ end
25
+
26
+ def read_config_file
27
+ if File.exists? CONFIG_FILE
28
+ YAML.load_file(CONFIG_FILE)
29
+ else
30
+ {}
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ require 'rubygems/package'
2
+ require 'fileutils'
3
+
4
+ module Yutani
5
+ # An abstraction of a real directory tree on disk
6
+ # Permits us to decide later whether this will be written to disk
7
+ # or embedded in a tarball, or sent over scp, etc.
8
+ class DirectoryTree
9
+ File = Struct.new(:path, :permissions, :content)
10
+
11
+ attr_reader :files, :prefix
12
+
13
+ def initialize(prefix = './')
14
+ @prefix = prefix
15
+ @files = []
16
+ end
17
+
18
+ def add_file(path, permissions, content)
19
+ @files << File.new(::File.join(@prefix, path), permissions.to_i, content)
20
+ end
21
+
22
+ def to_fs
23
+ @files.each do |f|
24
+ FileUtils.mkdir_p(::File.dirname(f.path))
25
+ ::File.open(f.path, 'w+', f.permissions) do |new_f|
26
+ new_f.write f.content
27
+ end
28
+ end
29
+ end
30
+
31
+ def to_tar(io = STDOUT)
32
+ Gem::Package::TarWriter.new(io) do |tar|
33
+ @files.each do |f|
34
+ tar.mkdir(::File.dirname(f.path), '0755')
35
+
36
+ tar.add_file_simple(f.path, f.permissions, f.content.bytes.size) do |tar_file|
37
+ tar_file.write f.content
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ module Yutani
2
+ class DSLEntity
3
+ attr_accessor :scope
4
+
5
+ def hiera(k)
6
+ Yutani::Hiera.lookup(k, @scope)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ module Yutani
2
+ module Hiera
3
+ class << self
4
+ def hiera(config_override={})
5
+ @hiera ||= init_hiera(config_override)
6
+ end
7
+
8
+ def init_hiera(override={})
9
+ conf = Yutani.config(override)
10
+
11
+ # hiera_config_file trumps hiera_config
12
+ ::Hiera.new(config:
13
+ conf.fetch('hiera_config_file', conf['hiera_config'])
14
+ )
15
+ end
16
+
17
+ def lookup(k, scope)
18
+ # hiera expects strings, not symbols
19
+ hiera_scope = scope.inject({}){|h,(k,v)| h[k.to_s] = v.to_s; h}
20
+ Yutani.logger.debug "hiera scope: %s" % hiera_scope
21
+
22
+ v = Yutani::Hiera.hiera.lookup(k.to_s, nil, hiera_scope)
23
+ Yutani.logger.warn "hiera couldn't find value for key #{k}" if v.nil?
24
+
25
+ # let us use symbols for hash keys
26
+ Yutani::Utils.convert_nested_hash_to_indifferent_access(v)
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/yutani/mod.rb ADDED
@@ -0,0 +1,321 @@
1
+ require 'json'
2
+ require 'hashie'
3
+ require 'pp'
4
+
5
+ module Yutani
6
+ # Maps to a terraform module. Named 'mod' to avoid confusion with
7
+ # ruby 'module'.
8
+ # It can contain :
9
+ # * other modules
10
+ # * resources
11
+ # * other resources (provider, data, etc)
12
+ # * variables
13
+ # * outputs
14
+ # Its block is evaluated depending upon whether it is enclosed within
15
+ # another module
16
+ # It has the following properties
17
+ # * mandatory name of type symbol
18
+ # * optional scope of type hash
19
+ # * ability to output a tar of its contents
20
+ class Mod < DSLEntity
21
+ attr_accessor :name, :resources, :providers, :block, :mods, :params, :outputs, :variables
22
+
23
+ def initialize(name, parent, local_scope, parent_scope, &block)
24
+ @name = name.to_sym
25
+
26
+ @scope = parent_scope.merge(local_scope)
27
+ @scope[:module_name] = name
28
+ @local_scope = local_scope
29
+ @parent = parent
30
+
31
+ @mods = []
32
+ @resources = []
33
+ @providers = []
34
+ @outputs = {}
35
+ @params = {}
36
+ @variables = {}
37
+
38
+ instance_eval &block
39
+ end
40
+
41
+ def mod(name, **scope, &block)
42
+
43
+ @mods << Mod.new(name, self, scope, @scope, &block)
44
+ end
45
+
46
+ def source(path)
47
+ absolute_path = File.expand_path(path, File.dirname(Yutani.entry_path))
48
+ contents = File.read absolute_path
49
+
50
+ instance_eval contents, path
51
+ end
52
+
53
+ def resources_hash
54
+ @resources.inject({}) do |r_hash, r|
55
+ r_hash[r.resource_type] ||= {}
56
+ r_hash[r.resource_type][r.resource_name] = r
57
+ r_hash
58
+ end
59
+ end
60
+
61
+ def debug
62
+ resolve_references!(self)
63
+ #pp @mods.unshift(self).map{|m| m.to_h }
64
+ end
65
+
66
+ class MyHash < Hash
67
+ include Hashie::Extensions::DeepMerge
68
+ end
69
+
70
+ # this generates the contents of *.tf.main
71
+ def to_h
72
+ h = {
73
+ module: @mods.inject({}) {|modules,m|
74
+ modules[m.tf_name] = {}
75
+ modules[m.tf_name][:source] = m.dir_path
76
+ modules[m.tf_name].merge! m.params
77
+ modules
78
+ },
79
+ resource: @resources.inject(MyHash.new){|resources,r|
80
+ resources.deep_merge(r.to_h)
81
+ },
82
+ provider: @providers.inject(MyHash.new){|providers,r|
83
+ providers.deep_merge(r.to_h)
84
+ },
85
+ output: @outputs.inject({}){|outputs,(k,v)|
86
+ outputs[k] = { value: v }
87
+ outputs
88
+ },
89
+ variable: @variables.inject({}){|variables,(k,v)|
90
+ variables[k] = {}
91
+ variables
92
+ }
93
+ }
94
+
95
+ # terraform doesn't like empty output and variable collections
96
+ h.delete_if {|_,v| v.empty? }
97
+ end
98
+
99
+ def tf_name
100
+ dirs = @local_scope.values.map{|v| v.to_s.gsub('-', '_') }
101
+ dirs.unshift(name)
102
+ dirs.join('_')
103
+ end
104
+
105
+ def dir_path
106
+ tf_name
107
+ end
108
+
109
+ def resource(resource_type, identifiers, **scope, &block)
110
+ merged_scope = @scope.merge(scope)
111
+ @resources <<
112
+ Resource.new(resource_type, identifiers, merged_scope, &block)
113
+ end
114
+
115
+ def provider(provider_name, **scope, &block)
116
+ merged_scope = @scope.merge(scope)
117
+ @providers <<
118
+ Provider.new(provider_name, merged_scope, &block)
119
+ end
120
+
121
+ def pretty_json
122
+ JSON.pretty_generate(to_h)
123
+ end
124
+
125
+ def children
126
+ @mods
127
+ end
128
+
129
+ def descendents
130
+ children + children.map{|c| c.descendents}.flatten
131
+ end
132
+
133
+ def parent?(mod)
134
+ @parent.name == mod.name
135
+ end
136
+
137
+ # given name of mod, return bool
138
+ def child?(mod)
139
+ children.map{|m| m.name }.include? mod.name
140
+ end
141
+
142
+ def child_by_name(name)
143
+ children.find{|child| child.name == name.to_sym}
144
+ end
145
+
146
+ def path
147
+ File.join(@parent.path, name.to_s)
148
+ end
149
+
150
+ class InvalidReferencePathException < StandardError; end
151
+
152
+ # rel_path: relative path to a target mod
153
+ # ret an array of mods tracing that path
154
+ # sorted from target -> source
155
+ # mods: array of module objects, which after being built is returned
156
+ # path: array of path strings: i.e. [.. .. .. a b c]
157
+
158
+ def generate_pathway(mods, path)
159
+ curr = path.shift
160
+
161
+ case curr
162
+ when /[a-z]+/
163
+ child = child_by_name(curr)
164
+ if child.nil?
165
+ raise InvalidReferencePathException, "no such module #{curr}"
166
+ else
167
+ child.generate_pathway(mods.unshift(self), path)
168
+ end
169
+ when '..'
170
+ if @parent.nil?
171
+ raise InvalidReferencePathException, "no such module #{curr}"
172
+ else
173
+ @parent.generate_pathway(mods.unshift(self), path)
174
+ end
175
+ when nil
176
+ return mods.unshift(self)
177
+ else
178
+ raise InvalidReferencePathException, "invalid path component: #{curr}"
179
+ end
180
+ end
181
+
182
+ # recursive linked-list function, propagating a variable
183
+ # from a target module to a source module
184
+ # seed var with array of [type,name,attr]
185
+ def propagate(prev, nxt, var)
186
+ if prev.empty?
187
+ # we are the source module
188
+ if nxt.empty?
189
+ # src and target in same mod
190
+ "${%s}" % var.join('.')
191
+ else
192
+ if self.child? nxt.first
193
+ # there is no 'composition' mod,
194
+ new_var = [self.name, var].flatten
195
+ nxt.first.params[new_var.join('_')] = "${%s}" % new_var.join('.')
196
+ nxt.first.variables[new_var] = ''
197
+
198
+ nxt.shift.propagate(prev.push(self), nxt, var)
199
+ elsif self.parent?(nxt.first)
200
+ # we are propagating upward the variable
201
+ self.outputs[var.join('_')] = "${%s}" % var.join('.')
202
+ nxt.shift.propagate(prev.push(self), nxt, var.join('_'))
203
+ else
204
+ raise "Propagation error!"
205
+ end
206
+ end
207
+ else
208
+ if nxt.empty?
209
+ # we're the source module
210
+ if self.child? prev.last
211
+ # it's been propagated 'up' to us
212
+ "${module.%s.%s}" % [prev.last.name, var]
213
+ elsif self.parent? prev.last
214
+ # it's been propagated 'down' to us
215
+ "${var.%s}" % var
216
+ else
217
+ raise "Propagation error!"
218
+ end
219
+ else
220
+ if self.child? prev.last and self.child? nxt.first
221
+ # we're a 'composition' module; the common ancestor
222
+ # to source and target modules
223
+ new_var = [prev.last.name, var]
224
+ nxt.first.params[new_var.join('_')] = "${module.%s.%s}" % new_var
225
+ nxt.first.variables[new_var.join('_')] = ""
226
+
227
+ nxt.shift.propagate(prev.push(self), nxt, new_var.join('_'))
228
+ elsif self.child? prev.last and self.parent? nxt.first
229
+ # we're propagating 'upward' the variable
230
+ # towards the common ancestor
231
+
232
+ new_var = [prev.last.name, var]
233
+ self.outputs[new_var.join('_')] = "${module.%s.%s}" % new_var
234
+
235
+ nxt.shift.propagate(prev.push(self), nxt, new_var.join('_'))
236
+ elsif self.parent? prev.last and self.parent? nxt.first
237
+ # we cannot be a child to two parents in a tree!
238
+ raise "Progation error!"
239
+ elsif self.parent? prev.last and self.child? nxt.first
240
+ nxt.first.params[var] = "${var.%s}" % var
241
+ nxt.first.variables[var] = ""
242
+
243
+ nxt.shift.propagate(prev.push(self), nxt, var)
244
+ else
245
+ raise "Propagation error!"
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def resolve_references!
252
+ @resources.each do |r|
253
+ r.resolve_references! do |ref|
254
+
255
+ matching_resources = []
256
+
257
+ path_components = ref.relative_path(self).split('/')
258
+ mod_path = generate_pathway([], path_components)
259
+ target_mod = mod_path.shift
260
+
261
+ # lookup matching resources in mod_path.first
262
+ matches = ref.find_matching_resources_in_module!(target_mod)
263
+
264
+ if matches.empty?
265
+ raise ReferenceException,
266
+ "no matching resources found in mod #{target_mod.name}"
267
+ end
268
+
269
+ interpolation_strings = matches.map do |res|
270
+ ra = ResourceAttribute.new(res.resource_type, res.resource_name, ref.attr)
271
+ # clone mod_path, because propagate() will alter it
272
+ target_mod.propagate([], mod_path.clone, [ra.type, ra.name, ra.attr])
273
+ end
274
+ interpolation_strings.length == 1 ? interpolation_strings[0] :
275
+ interpolation_strings
276
+ end
277
+ end
278
+
279
+ children.each do |m|
280
+ m.resolve_references!
281
+ end
282
+ end
283
+
284
+ def tar(filename)
285
+ # ideally, this needs to be done automatically as part of to_h
286
+ resolve_references!
287
+
288
+ File.open(filename, 'w+') do |tarball|
289
+ create_dir_tree('./').to_tar(tarball)
290
+ end
291
+ end
292
+
293
+ def to_fs(prefix='./terraform')
294
+ # ideally, this needs to be done automatically as part of to_h
295
+ resolve_references!
296
+
297
+ create_dir_tree(prefix).to_fs
298
+ end
299
+
300
+ def create_dir_tree(prefix)
301
+ dir_tree(DirectoryTree.new(prefix), '')
302
+ end
303
+
304
+ def dir_tree(dt, prefix)
305
+ full_dir_path = File.join(prefix, self.dir_path)
306
+ main_tf_path = File.join(full_dir_path, 'main.tf.json')
307
+
308
+ dt.add_file(
309
+ main_tf_path,
310
+ 0644,
311
+ self.pretty_json
312
+ )
313
+
314
+ mods.each do |m|
315
+ m.dir_tree(dt, full_dir_path)
316
+ end
317
+
318
+ dt
319
+ end
320
+ end
321
+ end
@@ -0,0 +1,34 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
3
+ module Yutani
4
+ class Provider < DSLEntity
5
+ attr_accessor :provider_name, :fields
6
+
7
+ def initialize(provider_name, **scope, &block)
8
+ @provider_name = provider_name
9
+ @scope = HashWithIndifferentAccess.new(scope)
10
+ @fields = {}
11
+
12
+ instance_eval &block if block_given?
13
+ end
14
+
15
+ def []=(k,v)
16
+ @fields[k] = v
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ @provider_name => @fields
22
+ }
23
+ end
24
+
25
+ def method_missing(name, *args, &block)
26
+ if block_given?
27
+ raise StandardError,
28
+ "provider properties do not accept blocks as parameters"
29
+ else
30
+ @fields[name] = args.first
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,57 @@
1
+ require 'pathname'
2
+
3
+ module Yutani
4
+ class Reference
5
+
6
+ attr_reader :path
7
+
8
+ def initialize(path='.', t, n, a)
9
+ @path = path
10
+ @t = t
11
+ @n = n
12
+ @a = a
13
+ end
14
+
15
+ def relative_path(source_mod)
16
+ source_path = Pathname.new(source_mod.path)
17
+ target_path = Pathname.new(@path)
18
+ target_path.relative_path_from(source_path).to_s
19
+ end
20
+
21
+ def resource_type
22
+ @t
23
+ end
24
+
25
+ def resource_name
26
+ @n
27
+ end
28
+
29
+ def attr
30
+ @a
31
+ end
32
+
33
+ def find_matching_resources_in_module!(mod)
34
+ resolutions = []
35
+ # we currently support strings for t n and a, and regex
36
+ # for n only
37
+ mod.resources.each do |resource|
38
+ if resource.resource_type == self.resource_type.to_sym
39
+ case self.resource_name
40
+ when Regexp
41
+ if resource.resource_name =~ self.resource_name
42
+ resolutions.push(resource)
43
+ end
44
+ when Symbol
45
+ if resource.resource_name == self.resource_name.to_sym
46
+ resolutions.push(resource)
47
+ end
48
+ else
49
+ raise "unsupported class #{self.resource_name.class}"
50
+ end
51
+ end
52
+ end
53
+
54
+ resolutions
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,28 @@
1
+ module Yutani
2
+ class ReferencePath
3
+ def initialize(mods, resource_type, resource_name, attr)
4
+ @mods = mods
5
+ @resource_type = resource_type
6
+ @resource_name = resource_name
7
+ @attr = attr
8
+ end
9
+
10
+ def interpolation_string(prefix=nil, mods=[])
11
+ resource_params = [@resource_type, @resource_name, @attr]
12
+ if prefix.nil?
13
+ # we're referencing a resource in the local module
14
+ resource_params.join('.')
15
+ else
16
+ resource_params.join('_')
17
+ end
18
+ end
19
+
20
+ def shift(prev, curr, nxt)
21
+ traverse(prev.push(curr), nxt.shift, nxt)
22
+ end
23
+
24
+ def traverse(prev,
25
+ shift(prev, curr, nxt)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,72 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
3
+ module Yutani
4
+ class Resource < DSLEntity
5
+ attr_accessor :resource_type, :resources, :fields, :mods, :resource_name
6
+
7
+ def initialize(resource_type, resource_name, **scope, &block)
8
+ @resource_type = resource_type
9
+ @resource_name = resource_name
10
+ @scope = HashWithIndifferentAccess.new(scope)
11
+ @fields = {}
12
+
13
+ instance_eval &block if block_given?
14
+ end
15
+
16
+ def []=(k,v)
17
+ @fields[k] = v
18
+ end
19
+
20
+ def to_h
21
+ {
22
+ @resource_type => {
23
+ @resource_name => @fields
24
+ }
25
+ }
26
+ end
27
+
28
+ def ref(m='.', t, n, a)
29
+ Reference.new(m, t, n, a)
30
+ end
31
+
32
+ def resolve_references!(&block)
33
+ @fields.each do |k,v|
34
+ case v
35
+ when Reference
36
+ @fields[k] = yield v
37
+ when SubResource
38
+ v.fields.each do |k,v|
39
+ if v.is_a? Reference
40
+ v.fields[k] = yield v
41
+ end
42
+ end
43
+ else
44
+ next
45
+ end
46
+ end
47
+ end
48
+
49
+ def method_missing(name, *args, &block)
50
+ if block_given?
51
+ sub = SubResource.new(scope)
52
+ sub.instance_exec(&block)
53
+ @fields[name] = sub.fields
54
+ else
55
+ # remove leading '_' if present.
56
+ # DSL users have to do prefix with underscore when they want to use
57
+ # a resource property that has same name as an existing ruby method
58
+ # i.e. 'timeout'
59
+ @fields[name] = args.first.to_s.sub(/^_/, '')
60
+ end
61
+ end
62
+ end
63
+
64
+ class SubResource < Resource
65
+ def initialize(scope)
66
+ @scope = scope
67
+ @fields = {}
68
+ end
69
+ end
70
+
71
+ ResourceAttribute = Struct.new(:type, :name, :attr)
72
+ end
@@ -0,0 +1,22 @@
1
+ module Yutani
2
+ # a stack is a terraform module with
3
+ # additional properties:
4
+ # * module name is hardcoded to 'root'
5
+ # * can only be found at top-level (it's an error if found within another stack/module)
6
+ # * because it's the top-level module, it's immediately evaluated
7
+ # * ability to configure remote state
8
+ class Stack < Mod
9
+
10
+ def initialize(name, **scope, &block)
11
+ super(name, nil, scope, {stack_name: name}, &block)
12
+ end
13
+
14
+ def path
15
+ "/root"
16
+ end
17
+
18
+ def [](name)
19
+ descendents.find{|d| d.name == name}
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
3
+ module Yutani
4
+ module Utils
5
+ class << self
6
+ def convert_nested_hash_to_indifferent_access(v)
7
+ case v
8
+ when Array
9
+ v.map{|i| convert_nested_hash_to_indifferent_access(i) }
10
+ when Hash
11
+ HashWithIndifferentAccess.new(v)
12
+ else
13
+ v
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Yutani
2
+ VERSION = '0.0.2'
3
+ end
data/lib/yutani.rb ADDED
@@ -0,0 +1,60 @@
1
+ require 'hiera'
2
+ require 'hashie'
3
+ require 'logger'
4
+ require 'yutani/version'
5
+ require 'yutani/config'
6
+ require 'yutani/hiera'
7
+ require 'yutani/cli'
8
+ require 'yutani/dsl_entity'
9
+ require 'yutani/reference'
10
+ require 'yutani/directory_tree'
11
+ require 'yutani/mod'
12
+ require 'yutani/stack'
13
+ require 'yutani/resource'
14
+ require 'yutani/provider'
15
+ require 'yutani/utils'
16
+
17
+ module Yutani
18
+
19
+ @stacks = []
20
+
21
+ class << self
22
+ attr_accessor :hiera, :stacks, :logger, :entry_path
23
+ end
24
+
25
+ class << self
26
+ def logger
27
+ @logger ||= (
28
+ logger = Logger.new(STDOUT)
29
+ logger.level = Logger.const_get(ENV.fetch('LOG_LEVEL', 'INFO'))
30
+ logger
31
+ )
32
+ end
33
+
34
+ def stack(name, **scope, &block)
35
+ s = Stack.new(name, **scope, &block)
36
+ @stacks << s
37
+ s
38
+ end
39
+
40
+ def config(override = {})
41
+ config = Config.new
42
+ override = Config[override]
43
+
44
+ config = config.read_config_file
45
+
46
+ # Merge DEFAULTS < .yutani.yml < override
47
+ Config.from(config.merge(override))
48
+ end
49
+
50
+ def build_from_file(file)
51
+ Yutani.entry_path = file
52
+
53
+ instance_eval(File.read(file), file)
54
+
55
+ unless stacks.empty?
56
+ stacks.each {|s| s.to_fs}
57
+ end
58
+ end
59
+ end
60
+ end
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yutani
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Louis Garman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-11-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: hashie
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.4.3
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.4.3
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: 3.4.3
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.4.3
47
+ - !ruby/object:Gem::Dependency
48
+ name: hiera
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.2'
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.2.1
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '3.2'
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 3.2.1
67
+ - !ruby/object:Gem::Dependency
68
+ name: thor
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: 0.19.1
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 0.19.1
81
+ - !ruby/object:Gem::Dependency
82
+ name: guard
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: 2.14.0
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: 2.14.0
95
+ - !ruby/object:Gem::Dependency
96
+ name: guard-yield
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ - !ruby/object:Gem::Dependency
110
+ name: rake
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ - !ruby/object:Gem::Dependency
124
+ name: aruba
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: 0.14.2
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: 0.14.2
137
+ description: Generates JSON for Terraform
138
+ email: louisgarman+yutani@gmail.com
139
+ executables:
140
+ - yutani
141
+ extensions: []
142
+ extra_rdoc_files: []
143
+ files:
144
+ - bin/install-terraform.sh
145
+ - bin/yutani
146
+ - lib/yutani.rb
147
+ - lib/yutani/cli.rb
148
+ - lib/yutani/config.rb
149
+ - lib/yutani/directory_tree.rb
150
+ - lib/yutani/dsl_entity.rb
151
+ - lib/yutani/hiera.rb
152
+ - lib/yutani/mod.rb
153
+ - lib/yutani/provider.rb
154
+ - lib/yutani/reference.rb
155
+ - lib/yutani/reference_path.rb
156
+ - lib/yutani/resource.rb
157
+ - lib/yutani/stack.rb
158
+ - lib/yutani/utils.rb
159
+ - lib/yutani/version.rb
160
+ homepage: https://github.com/leg100/yutani
161
+ licenses:
162
+ - MIT
163
+ metadata: {}
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '2.3'
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: 2.3.1
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubyforge_project:
183
+ rubygems_version: 2.5.1
184
+ signing_key:
185
+ specification_version: 4
186
+ summary: Terraform DSL
187
+ test_files: []