yutani 0.0.5 → 0.1.12
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 +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
|