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 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