yutani 0.0.5 → 0.1.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/yutani/cli.rb +90 -18
- data/lib/yutani/config.rb +2 -0
- data/lib/yutani/dsl_entity.rb +1 -3
- data/lib/yutani/hiera.rb +32 -7
- data/lib/yutani/provider.rb +6 -4
- data/lib/yutani/resource.rb +19 -39
- data/lib/yutani/stack.rb +96 -13
- data/lib/yutani/utils.rb +20 -2
- data/lib/yutani/version.rb +1 -1
- data/lib/yutani.rb +28 -16
- metadata +33 -10
- data/lib/yutani/mod.rb +0 -321
- data/lib/yutani/reference.rb +0 -57
- data/lib/yutani/reference_path.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a8429b5ebef83eb6a439a01fb542973221d0386
|
4
|
+
data.tar.gz: 49d5ea9b1161aacc544489bca25d8e6b07c93c60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a3c7679596ea8d4602586701ac439e84c4cbf0bb92dc475fbc6817bf194c70ef1b632705e6153c434dd53810bc813c88fbd1b5a1c71aa272f74c98a9d7c7cd9
|
7
|
+
data.tar.gz: 305d1813cc2929ecda7dcd3ddb71dbff4433337dddd9ac36241cf41cecd37b7994efe9eb689462723f66993f4995be2ad9ab3f8b47cc6d2711ac85897853bc99
|
data/lib/yutani/cli.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'thor'
|
2
|
+
require 'json'
|
3
|
+
require 'pp'
|
2
4
|
require 'yaml'
|
3
|
-
require '
|
4
|
-
require 'guard/commander'
|
5
|
+
require 'listen'
|
5
6
|
|
6
7
|
module Yutani
|
7
8
|
class Cli < Thor
|
@@ -19,27 +20,48 @@ module Yutani
|
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
22
|
-
desc 'build', 'Evaluates
|
23
|
-
def build
|
24
|
-
Yutani
|
23
|
+
desc 'build', 'Evaluates DSL scripts and creates terraform files'
|
24
|
+
def build
|
25
|
+
scripts_dir = Yutani::Config::DEFAULTS['scripts_dir']
|
26
|
+
|
27
|
+
files = Dir.glob(File.join(scripts_dir, '*.rb'))
|
28
|
+
if files.empty?
|
29
|
+
raise "Could not find any scripts in '#{scripts_dir}'"
|
30
|
+
end
|
31
|
+
|
32
|
+
files.each do |script|
|
33
|
+
Yutani.eval_file(script)
|
34
|
+
end
|
35
|
+
|
36
|
+
unless Yutani.stacks.empty?
|
37
|
+
Yutani.stacks.each {|s| s.to_fs}
|
38
|
+
end
|
25
39
|
end
|
26
40
|
|
27
41
|
# we need to know these things:
|
28
42
|
# * the directory to restrict to watching for changes
|
29
|
-
# * the script that build should evaluate
|
43
|
+
# * the script that build should evaluate
|
30
44
|
# * the glob - this is hardcoded to *.rb
|
31
|
-
desc 'watch', 'Run build upon changes to
|
32
|
-
def watch
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
45
|
+
desc 'watch', 'Run build upon changes to scripts'
|
46
|
+
def watch
|
47
|
+
Listen.to(Yutani::Config::DEFAULTS['scripts_dir']) do |m, a, d|
|
48
|
+
Yutani.logger.info "Re-build triggered: #{m} modified" unless m.empty?
|
49
|
+
Yutani.logger.info "Re-build triggered: #{a} added" unless a.empty?
|
50
|
+
Yutani.logger.info "Re-build triggered: #{d} deleted" unless d.empty?
|
37
51
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
52
|
+
build
|
53
|
+
|
54
|
+
Yutani.logger.info "Re-build finished"
|
55
|
+
end.start
|
56
|
+
|
57
|
+
# exit cleanly upon Ctrl-C
|
58
|
+
%w[INT TERM USR1].each do |sig|
|
59
|
+
Signal.trap(sig) do
|
60
|
+
exit
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
sleep
|
43
65
|
end
|
44
66
|
|
45
67
|
desc 'version', 'Prints the current version of Yutani'
|
@@ -47,16 +69,66 @@ EOF
|
|
47
69
|
puts Yutani::VERSION
|
48
70
|
end
|
49
71
|
|
72
|
+
desc 'target', 'Generate list of Terraform targets'
|
73
|
+
def target(stack_dir, *args)
|
74
|
+
files = Dir.glob(File.join(stack_dir, '*.tf.json'))
|
75
|
+
if files.empty?
|
76
|
+
raise "Could not find *.tf.json files in #{stack_dir}"
|
77
|
+
end
|
78
|
+
|
79
|
+
if args.empty?
|
80
|
+
raise "No targets specified"
|
81
|
+
end
|
82
|
+
|
83
|
+
contents = files.inject({}) do |h, f|
|
84
|
+
h.merge!(JSON.parse(File.read(f)))
|
85
|
+
h
|
86
|
+
end
|
87
|
+
|
88
|
+
targets = contents['resource'].inject({}) do |h,(k,v)|
|
89
|
+
h[k] = v.select do |k,v|
|
90
|
+
(args - k.split('_')).empty?
|
91
|
+
end
|
92
|
+
h
|
93
|
+
end.reject{|k,v| v.empty? }
|
94
|
+
|
95
|
+
target_flags = targets.inject([]) do |flags, (k,v)|
|
96
|
+
flags << v.keys.map do |resource_name|
|
97
|
+
"-target " + ["resource", k, resource_name].join('.')
|
98
|
+
end
|
99
|
+
flags
|
100
|
+
end.flatten
|
101
|
+
|
102
|
+
puts target_flags.join(" ")
|
103
|
+
end
|
104
|
+
|
105
|
+
# Invoke Terraform CLI command
|
106
|
+
def method_missing(name, *args, &block)
|
107
|
+
%x/terraform #{name} #{args}/
|
108
|
+
end
|
109
|
+
|
50
110
|
desc 'init', 'Initialize with a basic setup'
|
51
111
|
def init
|
52
112
|
if File.exists? '.yutani.yml'
|
53
|
-
|
113
|
+
Yutani.logger.warn ".yutani.yml already exists, skipping initialization"
|
54
114
|
else
|
55
115
|
File.open('.yutani.yml', 'w+') do |f|
|
56
116
|
f.write Yutani::Config::DEFAULTS.to_yaml(indent: 2)
|
57
117
|
puts ".yutani.yml created"
|
58
118
|
end
|
59
119
|
|
120
|
+
unless Dir.exists? Yutani::Config::DEFAULTS['terraform_dir']
|
121
|
+
FileUtils.mkdir Yutani::Config::DEFAULTS['terraform_dir']
|
122
|
+
end
|
123
|
+
|
124
|
+
unless Dir.exists? Yutani::Config::DEFAULTS['scripts_dir']
|
125
|
+
FileUtils.mkdir Yutani::Config::DEFAULTS['scripts_dir']
|
126
|
+
end
|
127
|
+
|
128
|
+
unless Dir.exists? Yutani::Config::DEFAULTS['includes_dir']
|
129
|
+
FileUtils.mkdir Yutani::Config::DEFAULTS['includes_dir']
|
130
|
+
end
|
131
|
+
|
60
132
|
hiera_dir = Yutani::Config::DEFAULTS['hiera_config'][:yaml][:datadir]
|
61
133
|
FileUtils.mkdir hiera_dir unless Dir.exists? hiera_dir
|
62
134
|
|
data/lib/yutani/config.rb
CHANGED
data/lib/yutani/dsl_entity.rb
CHANGED
data/lib/yutani/hiera.rb
CHANGED
@@ -1,6 +1,32 @@
|
|
1
|
+
require 'hiera'
|
2
|
+
|
3
|
+
# say something about the purpose of this wrapper, for instance
|
4
|
+
# the way in which yutani maintains a stack of hiera scopes and
|
5
|
+
# then merges them when a lookup occurs
|
1
6
|
module Yutani
|
2
7
|
module Hiera
|
8
|
+
class NonExistentKeyException < StandardError; end
|
9
|
+
|
10
|
+
@scopes = []
|
11
|
+
|
3
12
|
class << self
|
13
|
+
attr_accessor :hiera, :scopes
|
14
|
+
|
15
|
+
def scope
|
16
|
+
@scopes.inject({}){|h,scope| h.merge(scope) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def push(kv)
|
20
|
+
# hiera doesn't accept symbols for scope keys or values
|
21
|
+
@scopes.push Yutani::Utils.convert_symbols_to_strings_in_flat_hash(kv)
|
22
|
+
|
23
|
+
Yutani.logger.debug "hiera scope: %s" % scope
|
24
|
+
end
|
25
|
+
|
26
|
+
def pop
|
27
|
+
@scopes.pop
|
28
|
+
end
|
29
|
+
|
4
30
|
def hiera(config_override={})
|
5
31
|
@hiera ||= init_hiera(config_override)
|
6
32
|
end
|
@@ -14,15 +40,14 @@ module Yutani
|
|
14
40
|
)
|
15
41
|
end
|
16
42
|
|
17
|
-
def lookup(k
|
18
|
-
|
19
|
-
|
20
|
-
|
43
|
+
def lookup(k)
|
44
|
+
|
45
|
+
# hiera expects key to be a string
|
46
|
+
v = hiera.lookup(k.to_s, nil, scope)
|
21
47
|
|
22
|
-
v
|
23
|
-
Yutani.logger.warn "hiera couldn't find value for key #{k}" if v.nil?
|
48
|
+
raise NonExistentKeyException.new(v) if v.nil?
|
24
49
|
|
25
|
-
# let
|
50
|
+
# if nested hash, let user lookup nested keys with strings or symbols
|
26
51
|
Yutani::Utils.convert_nested_hash_to_indifferent_access(v)
|
27
52
|
end
|
28
53
|
end
|
data/lib/yutani/provider.rb
CHANGED
@@ -1,15 +1,13 @@
|
|
1
|
-
require 'active_support/hash_with_indifferent_access'
|
2
|
-
|
3
1
|
module Yutani
|
4
2
|
class Provider < DSLEntity
|
5
3
|
attr_accessor :provider_name, :fields
|
6
4
|
|
7
5
|
def initialize(provider_name, **scope, &block)
|
8
6
|
@provider_name = provider_name
|
9
|
-
@scope =
|
7
|
+
@scope = scope
|
10
8
|
@fields = {}
|
11
9
|
|
12
|
-
|
10
|
+
Docile.dsl_eval(self, &block) if block_given?
|
13
11
|
end
|
14
12
|
|
15
13
|
def []=(k,v)
|
@@ -22,6 +20,10 @@ module Yutani
|
|
22
20
|
}
|
23
21
|
end
|
24
22
|
|
23
|
+
def respond_to_missing?(method_name, include_private = false)
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
25
27
|
def method_missing(name, *args, &block)
|
26
28
|
if block_given?
|
27
29
|
raise StandardError,
|
data/lib/yutani/resource.rb
CHANGED
@@ -1,73 +1,53 @@
|
|
1
|
-
require 'active_support/hash_with_indifferent_access'
|
2
|
-
|
3
1
|
module Yutani
|
4
2
|
class Resource < DSLEntity
|
5
|
-
attr_accessor :resource_type, :
|
3
|
+
attr_accessor :resource_type, :namespace, :fields
|
6
4
|
|
7
|
-
def initialize(resource_type,
|
5
|
+
def initialize(resource_type, *namespace, &block)
|
8
6
|
@resource_type = resource_type
|
9
|
-
@
|
10
|
-
@scope = HashWithIndifferentAccess.new(scope)
|
7
|
+
@namespace = namespace
|
11
8
|
@fields = {}
|
12
9
|
|
13
|
-
|
10
|
+
Docile.dsl_eval(self, &block) if block_given?
|
14
11
|
end
|
15
12
|
|
16
|
-
def
|
17
|
-
@
|
13
|
+
def resource_name
|
14
|
+
@namespace.join('_')
|
18
15
|
end
|
19
16
|
|
20
17
|
def to_h
|
21
18
|
{
|
22
19
|
@resource_type => {
|
23
|
-
|
20
|
+
resource_name => @fields
|
24
21
|
}
|
25
22
|
}
|
26
23
|
end
|
27
24
|
|
28
|
-
def ref(
|
29
|
-
|
25
|
+
def ref(resource_type, *namespace, attr)
|
26
|
+
"${%s}" % [resource_type, namespace.join('_'), attr].join('.')
|
30
27
|
end
|
31
28
|
|
32
|
-
def
|
33
|
-
|
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
|
29
|
+
def respond_to_missing?(method_name, include_private = false)
|
30
|
+
true
|
47
31
|
end
|
48
32
|
|
49
33
|
def method_missing(name, *args, &block)
|
50
|
-
if
|
51
|
-
|
34
|
+
if name =~ /ref_(.*)/
|
35
|
+
# redirect ref_id, ref_name, etc, to ref()
|
36
|
+
ref(*args, $1)
|
37
|
+
elsif block_given?
|
38
|
+
# handle sub resources, like tags, listener, etc
|
39
|
+
sub = SubResource.new
|
52
40
|
sub.instance_exec(&block)
|
53
41
|
@fields[name] = sub.fields
|
54
42
|
else
|
55
|
-
|
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
|
-
sans_underscore = name.to_s.sub(/^_/, '').to_sym
|
60
|
-
@fields[sans_underscore] = args.first
|
43
|
+
@fields[name] = args.first
|
61
44
|
end
|
62
45
|
end
|
63
46
|
end
|
64
47
|
|
65
48
|
class SubResource < Resource
|
66
|
-
def initialize
|
67
|
-
@scope = scope
|
49
|
+
def initialize
|
68
50
|
@fields = {}
|
69
51
|
end
|
70
52
|
end
|
71
|
-
|
72
|
-
ResourceAttribute = Struct.new(:type, :name, :attr)
|
73
53
|
end
|
data/lib/yutani/stack.rb
CHANGED
@@ -1,22 +1,105 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
1
3
|
module Yutani
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
class Stack < DSLEntity
|
5
|
+
attr_accessor :resources, :providers, :outputs, :variables
|
6
|
+
|
7
|
+
def initialize(*namespace, &block)
|
8
|
+
@resources = []
|
9
|
+
@providers = []
|
10
|
+
@outputs = {}
|
11
|
+
@variables = {}
|
12
|
+
@namespace = namespace
|
13
|
+
|
14
|
+
Docile.dsl_eval(self, &block) if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
@namespace.join('_')
|
19
|
+
end
|
20
|
+
|
21
|
+
def resource(resource_type, *namespace, &block)
|
22
|
+
@resources <<
|
23
|
+
Resource.new(resource_type, *namespace, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def provider(name, &block)
|
27
|
+
@providers <<
|
28
|
+
Provider.new(name, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
# troposphere-like methods
|
32
|
+
def add_resource(resource)
|
33
|
+
@resources << resource
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_provider(provider)
|
37
|
+
@providers << provider
|
38
|
+
end
|
9
39
|
|
10
|
-
def
|
11
|
-
|
40
|
+
def inc(&block)
|
41
|
+
includes_dir = Yutani::Config::DEFAULTS['includes_dir']
|
42
|
+
path = File.join(includes_dir, yield)
|
43
|
+
|
44
|
+
eval File.read(path), block.binding, path
|
45
|
+
end
|
46
|
+
|
47
|
+
# this generates the contents of *.tf.main
|
48
|
+
def to_h
|
49
|
+
h = {
|
50
|
+
resource: @resources.inject(DeepMergeHash.new){|resources,r|
|
51
|
+
resources.deep_merge!(r.to_h)
|
52
|
+
},
|
53
|
+
provider: @providers.inject(DeepMergeHash.new){|providers,r|
|
54
|
+
providers.deep_merge(r.to_h)
|
55
|
+
},
|
56
|
+
output: @outputs.inject({}){|outputs,(k,v)|
|
57
|
+
outputs[k] = { value: v }
|
58
|
+
outputs
|
59
|
+
},
|
60
|
+
variable: @variables.inject({}){|variables,(k,v)|
|
61
|
+
variables[k] = {}
|
62
|
+
variables
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
# terraform doesn't like empty output and variable collections
|
67
|
+
h.delete_if {|_,v| v.empty? }
|
68
|
+
end
|
69
|
+
|
70
|
+
def pretty_json
|
71
|
+
JSON.pretty_generate(to_h)
|
72
|
+
end
|
73
|
+
|
74
|
+
def dir_path
|
75
|
+
name
|
76
|
+
end
|
77
|
+
|
78
|
+
def tar(filename)
|
79
|
+
File.open(filename, 'w+') do |tarball|
|
80
|
+
create_dir_tree('./').to_tar(tarball)
|
81
|
+
end
|
12
82
|
end
|
13
83
|
|
14
|
-
def
|
15
|
-
|
84
|
+
def to_fs(prefix='./terraform')
|
85
|
+
create_dir_tree(prefix).to_fs
|
16
86
|
end
|
17
87
|
|
18
|
-
def
|
19
|
-
|
88
|
+
def create_dir_tree(prefix)
|
89
|
+
dir_tree(DirectoryTree.new(prefix), '')
|
90
|
+
end
|
91
|
+
|
92
|
+
def dir_tree(dt, prefix)
|
93
|
+
full_dir_path = File.join(prefix, self.dir_path)
|
94
|
+
main_tf_path = File.join(full_dir_path, 'main.tf.json')
|
95
|
+
|
96
|
+
dt.add_file(
|
97
|
+
main_tf_path,
|
98
|
+
0644,
|
99
|
+
self.pretty_json
|
100
|
+
)
|
101
|
+
|
102
|
+
dt
|
20
103
|
end
|
21
104
|
end
|
22
105
|
end
|
data/lib/yutani/utils.rb
CHANGED
@@ -1,14 +1,32 @@
|
|
1
|
-
require '
|
1
|
+
require 'hashie'
|
2
2
|
|
3
3
|
module Yutani
|
4
|
+
class IndifferentHash < Hash
|
5
|
+
include Hashie::Extensions::MergeInitializer
|
6
|
+
include Hashie::Extensions::IndifferentAccess
|
7
|
+
end
|
8
|
+
|
9
|
+
class DeepMergeHash < Hash
|
10
|
+
include Hashie::Extensions::DeepMerge
|
11
|
+
end
|
12
|
+
|
4
13
|
module Utils
|
5
14
|
class << self
|
15
|
+
def convert_symbols_to_strings_in_flat_hash(h)
|
16
|
+
h.inject({}) do |h, (k,v)|
|
17
|
+
k = k.is_a?(Symbol) ? k.to_s : k
|
18
|
+
v = v.is_a?(Symbol) ? v.to_s : v
|
19
|
+
h[k] = v
|
20
|
+
h
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
6
24
|
def convert_nested_hash_to_indifferent_access(v)
|
7
25
|
case v
|
8
26
|
when Array
|
9
27
|
v.map{|i| convert_nested_hash_to_indifferent_access(i) }
|
10
28
|
when Hash
|
11
|
-
|
29
|
+
IndifferentHash.new(v)
|
12
30
|
else
|
13
31
|
v
|
14
32
|
end
|
data/lib/yutani/version.rb
CHANGED
data/lib/yutani.rb
CHANGED
@@ -1,38 +1,37 @@
|
|
1
|
-
require '
|
2
|
-
|
1
|
+
begin; require 'pry'; rescue LoadError; end
|
2
|
+
|
3
3
|
require 'logger'
|
4
|
+
require 'docile'
|
5
|
+
|
4
6
|
require 'yutani/version'
|
5
7
|
require 'yutani/config'
|
6
8
|
require 'yutani/hiera'
|
7
9
|
require 'yutani/cli'
|
8
10
|
require 'yutani/dsl_entity'
|
9
|
-
require 'yutani/reference'
|
10
11
|
require 'yutani/directory_tree'
|
11
|
-
require 'yutani/mod'
|
12
12
|
require 'yutani/stack'
|
13
13
|
require 'yutani/resource'
|
14
14
|
require 'yutani/provider'
|
15
15
|
require 'yutani/utils'
|
16
16
|
|
17
17
|
module Yutani
|
18
|
-
|
19
18
|
@stacks = []
|
20
19
|
|
21
20
|
class << self
|
22
|
-
|
23
|
-
|
21
|
+
# do we need :logger?
|
22
|
+
attr_accessor :hiera, :stacks, :logger
|
24
23
|
|
25
|
-
class << self
|
26
24
|
def logger
|
27
25
|
@logger ||= (
|
28
|
-
logger = Logger.new(
|
26
|
+
logger = Logger.new(STDERR)
|
29
27
|
logger.level = Logger.const_get(ENV.fetch('LOG_LEVEL', 'INFO'))
|
30
28
|
logger
|
31
29
|
)
|
32
30
|
end
|
33
31
|
|
34
|
-
|
35
|
-
|
32
|
+
# DSL statement
|
33
|
+
def stack(*namespace, &block)
|
34
|
+
s = Stack.new(*namespace, &block)
|
36
35
|
@stacks << s
|
37
36
|
s
|
38
37
|
end
|
@@ -47,14 +46,27 @@ module Yutani
|
|
47
46
|
Config.from(config.merge(override))
|
48
47
|
end
|
49
48
|
|
50
|
-
def
|
51
|
-
|
49
|
+
def scope(**kv, &block)
|
50
|
+
Hiera.push kv
|
52
51
|
|
53
|
-
|
52
|
+
# let user use symbols or strings for keys
|
53
|
+
yield Yutani::IndifferentHash.new(Hiera.scope)
|
54
54
|
|
55
|
-
|
56
|
-
|
55
|
+
Hiera.pop
|
56
|
+
end
|
57
|
+
|
58
|
+
def dsl_eval(str, *args, &block)
|
59
|
+
Docile.dsl_eval(self, *args, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def eval_string(*args, str, file)
|
63
|
+
dsl_eval(str, *args, file) do
|
64
|
+
instance_eval(str, file)
|
57
65
|
end
|
58
66
|
end
|
67
|
+
|
68
|
+
def eval_file(*args, file)
|
69
|
+
eval_string(File.read(file), file)
|
70
|
+
end
|
59
71
|
end
|
60
72
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: yutani
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Louis Garman
|
@@ -79,33 +79,45 @@ dependencies:
|
|
79
79
|
- !ruby/object:Gem::Version
|
80
80
|
version: 0.19.1
|
81
81
|
- !ruby/object:Gem::Dependency
|
82
|
-
name:
|
82
|
+
name: docile
|
83
83
|
requirement: !ruby/object:Gem::Requirement
|
84
84
|
requirements:
|
85
85
|
- - "~>"
|
86
86
|
- !ruby/object:Gem::Version
|
87
|
-
version:
|
87
|
+
version: '1.1'
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 1.1.5
|
88
91
|
type: :runtime
|
89
92
|
prerelease: false
|
90
93
|
version_requirements: !ruby/object:Gem::Requirement
|
91
94
|
requirements:
|
92
95
|
- - "~>"
|
93
96
|
- !ruby/object:Gem::Version
|
94
|
-
version:
|
97
|
+
version: '1.1'
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 1.1.5
|
95
101
|
- !ruby/object:Gem::Dependency
|
96
|
-
name:
|
102
|
+
name: listen
|
97
103
|
requirement: !ruby/object:Gem::Requirement
|
98
104
|
requirements:
|
105
|
+
- - "~>"
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '3.1'
|
99
108
|
- - ">="
|
100
109
|
- !ruby/object:Gem::Version
|
101
|
-
version:
|
110
|
+
version: 3.1.5
|
102
111
|
type: :runtime
|
103
112
|
prerelease: false
|
104
113
|
version_requirements: !ruby/object:Gem::Requirement
|
105
114
|
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.1'
|
106
118
|
- - ">="
|
107
119
|
- !ruby/object:Gem::Version
|
108
|
-
version:
|
120
|
+
version: 3.1.5
|
109
121
|
- !ruby/object:Gem::Dependency
|
110
122
|
name: rake
|
111
123
|
requirement: !ruby/object:Gem::Requirement
|
@@ -134,6 +146,20 @@ dependencies:
|
|
134
146
|
- - "~>"
|
135
147
|
- !ruby/object:Gem::Version
|
136
148
|
version: 0.14.2
|
149
|
+
- !ruby/object:Gem::Dependency
|
150
|
+
name: guard
|
151
|
+
requirement: !ruby/object:Gem::Requirement
|
152
|
+
requirements:
|
153
|
+
- - "~>"
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: 2.14.0
|
156
|
+
type: :development
|
157
|
+
prerelease: false
|
158
|
+
version_requirements: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - "~>"
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: 2.14.0
|
137
163
|
description: Generates JSON for Terraform
|
138
164
|
email: louisgarman+yutani@gmail.com
|
139
165
|
executables:
|
@@ -149,10 +175,7 @@ files:
|
|
149
175
|
- lib/yutani/directory_tree.rb
|
150
176
|
- lib/yutani/dsl_entity.rb
|
151
177
|
- lib/yutani/hiera.rb
|
152
|
-
- lib/yutani/mod.rb
|
153
178
|
- lib/yutani/provider.rb
|
154
|
-
- lib/yutani/reference.rb
|
155
|
-
- lib/yutani/reference_path.rb
|
156
179
|
- lib/yutani/resource.rb
|
157
180
|
- lib/yutani/stack.rb
|
158
181
|
- lib/yutani/utils.rb
|
data/lib/yutani/mod.rb
DELETED
@@ -1,321 +0,0 @@
|
|
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
|
data/lib/yutani/reference.rb
DELETED
@@ -1,57 +0,0 @@
|
|
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
|
@@ -1,28 +0,0 @@
|
|
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
|