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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ba4fbd47b60fbd3cfa4fcb75a7b98f5717fee099
4
- data.tar.gz: 18e4a37db64e9e6233758fdf251c36787492af7e
3
+ metadata.gz: 3a8429b5ebef83eb6a439a01fb542973221d0386
4
+ data.tar.gz: 49d5ea9b1161aacc544489bca25d8e6b07c93c60
5
5
  SHA512:
6
- metadata.gz: 21cda1d6990d8c7f62a435b3565c1f7551cad94d94419254b80c959803174bd51beba0e31559c2b5c53ead34613ba84117c09003696dcacd4e2b789e3835d3d0
7
- data.tar.gz: f8def293272b24075575ba67c51bb6c5c69734a9b9e74cf0c5186800d36b4976c99960f638fbe3d599c94c515d087e3b24898dfd5d1cd49c800012c332a2d8bc
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 'guard'
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 the given script and creates terraform files'
23
- def build(script)
24
- Yutani.build_from_file(script)
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 files/directories'
32
- def watch(script, script_dir)
33
- guardfile = <<-EOF
34
- run_build = proc do
35
- system("yutani build #{script}")
36
- end
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
- 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)
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
- puts ".yutani.yml already exists, skipping initialization"
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
@@ -4,6 +4,8 @@ module Yutani
4
4
 
5
5
  # Strings rather than symbols are used for compatibility with YAML.
6
6
  DEFAULTS = Config[{
7
+ "scripts_dir" => "scripts",
8
+ "includes_dir" => "includes",
7
9
  "terraform_dir" => "terraform",
8
10
  "hiera_config" => {
9
11
  :backends => ["yaml"],
@@ -1,9 +1,7 @@
1
1
  module Yutani
2
2
  class DSLEntity
3
- attr_accessor :scope
4
-
5
3
  def hiera(k)
6
- Yutani::Hiera.lookup(k, @scope)
4
+ Yutani::Hiera.lookup(k)
7
5
  end
8
6
  end
9
7
  end
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, 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
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 = 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?
48
+ raise NonExistentKeyException.new(v) if v.nil?
24
49
 
25
- # let us use symbols for hash keys
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
@@ -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 = HashWithIndifferentAccess.new(scope)
7
+ @scope = scope
10
8
  @fields = {}
11
9
 
12
- instance_eval &block if block_given?
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,
@@ -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, :resources, :fields, :mods, :resource_name
3
+ attr_accessor :resource_type, :namespace, :fields
6
4
 
7
- def initialize(resource_type, resource_name, **scope, &block)
5
+ def initialize(resource_type, *namespace, &block)
8
6
  @resource_type = resource_type
9
- @resource_name = resource_name
10
- @scope = HashWithIndifferentAccess.new(scope)
7
+ @namespace = namespace
11
8
  @fields = {}
12
9
 
13
- instance_eval &block if block_given?
10
+ Docile.dsl_eval(self, &block) if block_given?
14
11
  end
15
12
 
16
- def []=(k,v)
17
- @fields[k] = v
13
+ def resource_name
14
+ @namespace.join('_')
18
15
  end
19
16
 
20
17
  def to_h
21
18
  {
22
19
  @resource_type => {
23
- @resource_name => @fields
20
+ resource_name => @fields
24
21
  }
25
22
  }
26
23
  end
27
24
 
28
- def ref(m='.', t, n, a)
29
- Reference.new(m, t, n, a)
25
+ def ref(resource_type, *namespace, attr)
26
+ "${%s}" % [resource_type, namespace.join('_'), attr].join('.')
30
27
  end
31
28
 
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
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 block_given?
51
- sub = SubResource.new(scope)
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
- # 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
- 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(scope)
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
- # 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
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 initialize(name, **scope, &block)
11
- super(name, nil, scope, {stack_name: name}, &block)
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 path
15
- "/root"
84
+ def to_fs(prefix='./terraform')
85
+ create_dir_tree(prefix).to_fs
16
86
  end
17
87
 
18
- def [](name)
19
- descendents.find{|d| d.name == name}
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 'active_support/hash_with_indifferent_access'
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
- HashWithIndifferentAccess.new(v)
29
+ IndifferentHash.new(v)
12
30
  else
13
31
  v
14
32
  end
@@ -1,3 +1,3 @@
1
1
  module Yutani
2
- VERSION = '0.0.5'
2
+ VERSION = '0.1.12'
3
3
  end
data/lib/yutani.rb CHANGED
@@ -1,38 +1,37 @@
1
- require 'hiera'
2
- require 'hashie'
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
- attr_accessor :hiera, :stacks, :logger, :entry_path
23
- end
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(STDOUT)
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
- def stack(name, **scope, &block)
35
- s = Stack.new(name, **scope, &block)
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 build_from_file(file)
51
- Yutani.entry_path = file
49
+ def scope(**kv, &block)
50
+ Hiera.push kv
52
51
 
53
- instance_eval(File.read(file), file)
52
+ # let user use symbols or strings for keys
53
+ yield Yutani::IndifferentHash.new(Hiera.scope)
54
54
 
55
- unless stacks.empty?
56
- stacks.each {|s| s.to_fs}
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.0.5
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: guard
82
+ name: docile
83
83
  requirement: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
- version: 2.14.0
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: 2.14.0
97
+ version: '1.1'
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 1.1.5
95
101
  - !ruby/object:Gem::Dependency
96
- name: guard-yield
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: '0'
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: '0'
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
@@ -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