Class: Yutani::Mod

Inherits:
DSLEntity show all
Defined in:
lib/yutani/mod.rb

Overview

Maps to a terraform module. Named ‘mod’ to avoid confusion with ruby ‘module’. It can contain :

  • other modules

  • resources

  • other resources (provider, data, etc)

  • variables

  • outputs

Its block is evaluated depending upon whether it is enclosed within another module It has the following properties

  • mandatory name of type symbol

  • optional scope of type hash

  • ability to output a tar of its contents

Direct Known Subclasses

Stack

Defined Under Namespace

Classes: InvalidReferencePathException, MyHash

Instance Attribute Summary collapse

Attributes inherited from DSLEntity

#scope

Instance Method Summary collapse

Methods inherited from DSLEntity

#hiera

Constructor Details

#initialize(name, parent, local_scope, parent_scope, &block) ⇒ Mod

Returns a new instance of Mod.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/yutani/mod.rb', line 23

def initialize(name, parent, local_scope, parent_scope, &block)
  @name                = name.to_sym

  @scope               = parent_scope.merge(local_scope)
  @scope[:module_name] = name
  @local_scope         = local_scope
  @parent              = parent

  @mods                = []
  @resources           = []
  @providers           = []
  @outputs             = {}
  @params              = {}
  @variables           = {}

  instance_eval        &block
end

Instance Attribute Details

#blockObject

Returns the value of attribute block.



21
22
23
# File 'lib/yutani/mod.rb', line 21

def block
  @block
end

#modsObject

Returns the value of attribute mods.



21
22
23
# File 'lib/yutani/mod.rb', line 21

def mods
  @mods
end

#nameObject

Returns the value of attribute name.



21
22
23
# File 'lib/yutani/mod.rb', line 21

def name
  @name
end

#outputsObject

Returns the value of attribute outputs.



21
22
23
# File 'lib/yutani/mod.rb', line 21

def outputs
  @outputs
end

#paramsObject

Returns the value of attribute params.



21
22
23
# File 'lib/yutani/mod.rb', line 21

def params
  @params
end

#providersObject

Returns the value of attribute providers.



21
22
23
# File 'lib/yutani/mod.rb', line 21

def providers
  @providers
end

#resourcesObject

Returns the value of attribute resources.



21
22
23
# File 'lib/yutani/mod.rb', line 21

def resources
  @resources
end

#variablesObject

Returns the value of attribute variables.



21
22
23
# File 'lib/yutani/mod.rb', line 21

def variables
  @variables
end

Instance Method Details

#child?(mod) ⇒ Boolean

given name of mod, return bool

Returns:

  • (Boolean)


138
139
140
# File 'lib/yutani/mod.rb', line 138

def child?(mod)
  children.map{|m| m.name }.include? mod.name
end

#child_by_name(name) ⇒ Object



142
143
144
# File 'lib/yutani/mod.rb', line 142

def child_by_name(name)
  children.find{|child| child.name == name.to_sym}
end

#childrenObject



125
126
127
# File 'lib/yutani/mod.rb', line 125

def children
  @mods
end

#create_dir_tree(prefix) ⇒ Object



300
301
302
# File 'lib/yutani/mod.rb', line 300

def create_dir_tree(prefix)
  dir_tree(DirectoryTree.new(prefix), '')
end

#debugObject



61
62
63
64
# File 'lib/yutani/mod.rb', line 61

def debug
  resolve_references!(self)
  #pp @mods.unshift(self).map{|m| m.to_h }
end

#descendentsObject



129
130
131
# File 'lib/yutani/mod.rb', line 129

def descendents
  children + children.map{|c| c.descendents}.flatten
end

#dir_pathObject



105
106
107
# File 'lib/yutani/mod.rb', line 105

def dir_path
  tf_name
end

#dir_tree(dt, prefix) ⇒ Object



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/yutani/mod.rb', line 304

def dir_tree(dt, prefix)
  full_dir_path = File.join(prefix, self.dir_path)
  main_tf_path = File.join(full_dir_path, 'main.tf.json')

  dt.add_file(
    main_tf_path,
    0644,
    self.pretty_json
  )

  mods.each do |m|
    m.dir_tree(dt, full_dir_path)
  end

  dt
end

#generate_pathway(mods, path) ⇒ Object

rel_path: relative path to a target mod ret an array of mods tracing that path sorted from target -> source mods: array of module objects, which after being built is returned path: array of path strings: i.e. [.. .. .. a b c]



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/yutani/mod.rb', line 158

def generate_pathway(mods, path)
  curr = path.shift

  case curr
  when /[a-z]+/
    child = child_by_name(curr)
    if child.nil?
      raise InvalidReferencePathException, "no such module #{curr}" 
    else
      child.generate_pathway(mods.unshift(self), path)
    end
  when '..'
    if @parent.nil?
      raise InvalidReferencePathException, "no such module #{curr}" 
    else
      @parent.generate_pathway(mods.unshift(self), path)
    end
  when nil
    return mods.unshift(self)
  else
    raise InvalidReferencePathException, "invalid path component: #{curr}" 
  end
end

#mod(name, **scope, &block) ⇒ Object



41
42
43
44
# File 'lib/yutani/mod.rb', line 41

def mod(name, **scope, &block)

  @mods << Mod.new(name, self, scope, @scope, &block)
end

#parent?(mod) ⇒ Boolean

Returns:

  • (Boolean)


133
134
135
# File 'lib/yutani/mod.rb', line 133

def parent?(mod)
  @parent.name == mod.name
end

#pathObject



146
147
148
# File 'lib/yutani/mod.rb', line 146

def path
  File.join(@parent.path, name.to_s)
end

#pretty_jsonObject



121
122
123
# File 'lib/yutani/mod.rb', line 121

def pretty_json
  JSON.pretty_generate(to_h)
end

#propagate(prev, nxt, var) ⇒ Object

recursive linked-list function, propagating a variable from a target module to a source module seed var with array of [type,name,attr]



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/yutani/mod.rb', line 185

def propagate(prev, nxt, var)
  if prev.empty?
    # we are the source module
    if nxt.empty?
      # src and target in same mod
      "${%s}" % var.join('.')
    else
      if self.child? nxt.first
        # there is no 'composition' mod,
        new_var = [self.name, var].flatten
        nxt.first.params[new_var.join('_')] = "${%s}" % new_var.join('.')
        nxt.first.variables[new_var] = ''

        nxt.shift.propagate(prev.push(self), nxt, var)
      elsif self.parent?(nxt.first)
        # we are propagating upward the variable
        self.outputs[var.join('_')] = "${%s}" % var.join('.')
        nxt.shift.propagate(prev.push(self), nxt, var.join('_'))
      else
        raise "Propagation error!"
      end
    end
  else
    if nxt.empty?
      # we're the source module
      if self.child? prev.last
        # it's been propagated 'up' to us
        "${module.%s.%s}" % [prev.last.name, var]
      elsif self.parent? prev.last
        # it's been propagated 'down' to us
        "${var.%s}" % var
      else
        raise "Propagation error!"
      end
    else
      if self.child? prev.last and self.child? nxt.first
        # we're a 'composition' module; the common ancestor
        # to source and target modules
        new_var = [prev.last.name, var]
        nxt.first.params[new_var.join('_')] = "${module.%s.%s}" % new_var
        nxt.first.variables[new_var.join('_')] = ""

        nxt.shift.propagate(prev.push(self), nxt, new_var.join('_'))
      elsif self.child? prev.last and self.parent? nxt.first
        # we're propagating 'upward' the variable
        # towards the common ancestor
        
        new_var = [prev.last.name, var]
        self.outputs[new_var.join('_')] = "${module.%s.%s}" % new_var

        nxt.shift.propagate(prev.push(self), nxt, new_var.join('_'))
      elsif self.parent? prev.last and self.parent? nxt.first
        # we cannot be a child to two parents in a tree!
        raise "Progation error!"
      elsif self.parent? prev.last and self.child? nxt.first
        nxt.first.params[var] = "${var.%s}" % var
        nxt.first.variables[var] = ""

        nxt.shift.propagate(prev.push(self), nxt, var)
      else
        raise "Propagation error!"
      end
    end
  end
end

#provider(provider_name, **scope, &block) ⇒ Object



115
116
117
118
119
# File 'lib/yutani/mod.rb', line 115

def provider(provider_name, **scope, &block)
  merged_scope = @scope.merge(scope)
  @providers <<
    Provider.new(provider_name, merged_scope, &block)
end

#resolve_references!Object



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/yutani/mod.rb', line 251

def resolve_references!
  @resources.each do |r|
    r.resolve_references! do |ref|

      matching_resources = []

      path_components = ref.relative_path(self).split('/')
      mod_path = generate_pathway([], path_components)
      target_mod = mod_path.shift
 
      # lookup matching resources in mod_path.first
      matches = ref.find_matching_resources_in_module!(target_mod)

      if matches.empty?
        raise ReferenceException, 
          "no matching resources found in mod #{target_mod.name}"
      end

      interpolation_strings = matches.map do |res|
        ra = ResourceAttribute.new(res.resource_type, res.resource_name, ref.attr)
        # clone mod_path, because propagate() will alter it
        target_mod.propagate([], mod_path.clone, [ra.type, ra.name, ra.attr])
      end
      interpolation_strings.length == 1 ? interpolation_strings[0] : 
        interpolation_strings
    end
  end

  children.each do |m|
    m.resolve_references!
  end
end

#resource(resource_type, identifiers, **scope, &block) ⇒ Object



109
110
111
112
113
# File 'lib/yutani/mod.rb', line 109

def resource(resource_type, identifiers, **scope, &block)
  merged_scope = @scope.merge(scope)
  @resources <<
    Resource.new(resource_type, identifiers, merged_scope, &block)
end

#resources_hashObject



53
54
55
56
57
58
59
# File 'lib/yutani/mod.rb', line 53

def resources_hash
  @resources.inject({}) do |r_hash, r|
    r_hash[r.resource_type] ||= {}
    r_hash[r.resource_type][r.resource_name] = r 
    r_hash
  end
end

#source(path) ⇒ Object



46
47
48
49
50
51
# File 'lib/yutani/mod.rb', line 46

def source(path)
  absolute_path = File.expand_path(path, File.dirname(Yutani.entry_path))
  contents = File.read absolute_path

  instance_eval contents, path
end

#tar(filename) ⇒ Object



284
285
286
287
288
289
290
291
# File 'lib/yutani/mod.rb', line 284

def tar(filename)
  # ideally, this needs to be done automatically as part of to_h
  resolve_references!

  File.open(filename, 'w+') do |tarball|
    create_dir_tree('./').to_tar(tarball)
  end
end

#tf_nameObject



99
100
101
102
103
# File 'lib/yutani/mod.rb', line 99

def tf_name
  dirs = @local_scope.values.map{|v| v.to_s.gsub('-', '_') }
  dirs.unshift(name)
  dirs.join('_')
end

#to_fs(prefix = './terraform') ⇒ Object



293
294
295
296
297
298
# File 'lib/yutani/mod.rb', line 293

def to_fs(prefix='./terraform')
  # ideally, this needs to be done automatically as part of to_h
  resolve_references!

  create_dir_tree(prefix).to_fs
end

#to_hObject

this generates the contents of *.tf.main



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/yutani/mod.rb', line 71

def to_h
  h = { 
    module: @mods.inject({}) {|modules,m|
      modules[m.tf_name] = {}
      modules[m.tf_name][:source] = m.dir_path
      modules[m.tf_name].merge! m.params
      modules
    },
    resource: @resources.inject(MyHash.new){|resources,r|
      resources.deep_merge(r.to_h)
    },
    provider: @providers.inject(MyHash.new){|providers,r|
      providers.deep_merge(r.to_h)
    },
    output: @outputs.inject({}){|outputs,(k,v)|
      outputs[k] = { value: v }
      outputs
    },
    variable: @variables.inject({}){|variables,(k,v)|
      variables[k] = {}
      variables
    }
  }

  # terraform doesn't like empty output and variable collections
  h.delete_if {|_,v| v.empty? }
end