yutani 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/install-terraform.sh +3 -0
- data/bin/yutani +8 -0
- data/lib/yutani/cli.rb +77 -0
- data/lib/yutani/config.rb +34 -0
- data/lib/yutani/directory_tree.rb +43 -0
- data/lib/yutani/dsl_entity.rb +9 -0
- data/lib/yutani/hiera.rb +30 -0
- data/lib/yutani/mod.rb +321 -0
- data/lib/yutani/provider.rb +34 -0
- data/lib/yutani/reference.rb +57 -0
- data/lib/yutani/reference_path.rb +28 -0
- data/lib/yutani/resource.rb +72 -0
- data/lib/yutani/stack.rb +22 -0
- data/lib/yutani/utils.rb +18 -0
- data/lib/yutani/version.rb +3 -0
- data/lib/yutani.rb +60 -0
- metadata +187 -0
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
|
data/bin/yutani
ADDED
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
|
data/lib/yutani/hiera.rb
ADDED
@@ -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
|
data/lib/yutani/stack.rb
ADDED
@@ -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
|
data/lib/yutani/utils.rb
ADDED
@@ -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
|
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: []
|