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 +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: []
|