Class: CloudFormationTool::CloudFormation

Inherits:
Object
  • Object
show all
Defined in:
lib/cloud_formation_tool/cloud_formation.rb,
lib/cloud_formation_tool/cloud_formation/stack.rb,
lib/cloud_formation_tool/cloud_formation/lambda_code.rb,
lib/cloud_formation_tool/cloud_formation/nested_stack.rb,
lib/cloud_formation_tool/cloud_formation/cloud_front_distribution.rb

Defined Under Namespace

Modules: CloudFrontDistribution, CloudFrontInvalidation Classes: LambdaCode, NestedStack, Stack

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ CloudFormation

Returns a new instance of CloudFormation.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 15

def initialize(path)
  $MAX_USER_DATA_SIZE = 16384 if $MAX_USER_DATA_SIZE.nil?
  log "Loading #{path}"
  @path = path
  @path = "#{@path}/cloud-formation.yaml" if File.directory? @path
  @path = "#{@path}.yaml" if !File.exist? @path and File.exist? "#{@path}.yaml"
  @basedir = File.dirname(@path)
  @compiled = false
  @params = nil
  begin
    text = File.read(@path)
    # remove comments because white space seen between comments can seriously psych Psych
    text.gsub!(/^#.*\n/,'')
    text = fixShorthand(text)
    @data = YAML.load(text, filename: @path, permitted_classes: [Date, Symbol]).to_h
  rescue Psych::SyntaxError => e
    e.message =~ /line (\d+) column (\d+)/
    lines = text.split "\n"
    raise CloudFormationTool::Errors::AppError, "Error parsing #{path} at line #{e.line} column #{e.column}:\n" +
      "#{lines[e.line-1]}\n" +
      "#{(' ' * (e.column - 1 ))}^- #{e.problem} #{e.context}"
  rescue Errno::ENOENT => e
    raise CloudFormationTool::Errors::AppError, "Error reading #{path}: #{e.message}"
  end
end

Instance Attribute Details

#basedirObject (readonly)

Returns the value of attribute basedir.



13
14
15
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 13

def basedir
  @basedir
end

Class Method Details

.parse(path) ⇒ Object



9
10
11
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 9

def self.parse(path)
  CloudFormation.new(path)
end

Instance Method Details

#compile(parameters = nil) ⇒ Object



41
42
43
44
45
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 41

def compile(parameters = nil)
  @params = parameters unless parameters.nil?
  embed_includes
  @data = load_files(@data)
end

#eachObject



227
228
229
230
231
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 227

def each
  compile['Parameters'].each do |name, param|
    yield name, param['Default']
  end
end

#embed_includesObject



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 96

def embed_includes
  (@data.delete(@data.keys.find{|k| k.start_with? 'Include'}) || []).each do |path|
    realpath = "#{@basedir}/#{path}"
    cfile_key = File.dirname(realpath).gsub(%r{/(.)}){|m| $1.upcase }.gsub(/\W+/,'')
    rewrites = Hash.new
    CloudFormation.new(realpath).compile.each do |category, catdata|
      # some categories are meta-data that we can ignore from includes
      next if %w(AWSTemplateFormatVersion Description).include? category
      
      if category == "Parameters"
        rewriteParameters catdata, cfile_key, rewrites
        @data["Parameters"].merge! catdata
        next
      end
      
      case catdata
      when Hash
        # warn against duplicate entities, resources or outputs
        (@data[category] ||= {}).keys.each do |key|
          if catdata.has_key? key
            raise CloudFormationTool::Errors::AppError, "Error compiling #{path} - duplicate '#{category}' item: #{key}"
          end 
        end
        catdata = fixrefs(catdata, rewrites)
        # add included properties
        @data[category].merge! catdata
      when Array
        if @data[category].nil?
          @data[category] = catdata
        elsif @data[category].is_a? Array
          @data[category] += catdata
        else
          raise CloudFormationTool::Errors::AppError, "Error compiling #{path} - conflicting types for '#{category}'"
        end
      else
        if @data[category].nil?
          @data[category] = catdata
        else
          raise CloudFormationTool::Errors::AppError, "Error compiling #{path} - I do not know how to merge non-list non-dictionary '#{category}'!"
        end
      end
      
    end
  end
end

#embed_includes_futureObject



86
87
88
89
90
91
92
93
94
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 86

def embed_includes_future
  (@data.delete(@data.keys.find{|k| k.start_with? 'Include'}) || []).each do |path|
    case path
    when Hash
    when String
      embed_included_path path
    end
  end
end

#fixrefs(data, rmap) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 66

def fixrefs(data, rmap)
  case data
  when Hash
    data.inject({}) do |h,(k,v)|
      h[k] = if k == "Ref"
        rmap[v] || v
      else
        fixrefs(v,rmap)
      end
      h
    end
  when Array
    data.collect do |item|
      fixrefs(item, rmap)
    end
  else
    return data
  end
end

#fixShorthand(text) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 52

def fixShorthand(text)
  text.gsub(/(?:(\s*)([^![:space:]]+))?(\s+)!(\w+)/) do |match|
    case $4
    when *%w(Base64 FindInMap GetAtt GetAZs ImportValue Join Select Sub Split
      And Equals If Not Or)
      ($2.nil? ? "" : "#{$1}#{$2}\n#{$1} ") + "#{$3}\"Fn::#{$4}\":"
    when 'Ref'
      "#{$1}#{$2}\n#{$1} #{$3}#{$4}:"
    else
      match
    end
  end
end

#load_files(data, restype = nil) ⇒ Object



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
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 186

def load_files(data, restype = nil)
  case data
  when Array
    data.collect { |data| load_files(data, restype) }
  when Hash
    # remember the current resource type
    restype = data['Type'] if restype.nil? and data.key?('Type')
    data.inject({}) do |dict, (key, val)|
      dict[key] = case restype
        when 'AWS::AutoScaling::LaunchConfiguration', 'AWS::EC2::LaunchTemplate'
          if (key == "UserData") and (val["File"]) 
            # Support LaunchConfiguration UserData from file
            CloudInit.new("#{@basedir}/#{val["File"]}").to_base64
          elsif (key == "UserData") and (val["FileTemplate"]) 
            # Support LaunchConfiguration UserData from file with substitutions
            { "Fn::Base64" => { "Fn::Sub" => CloudInit.new("#{@basedir}/#{val["FileTemplate"]}").compile } }
          else
            load_files(val, restype)
          end
        when 'AWS::Lambda::Function'
          if key == 'Code'
            LambdaCode.new(val, self, data['Runtime']).to_cloudformation
          else
            load_files(val, restype)
          end
        when 'AWS::CloudFormation::Stack'
          if key == 'Properties' and val.key?('Template')
            NestedStack.new(val, self).to_cloudformation
          else
            load_files(val, restype)
          end
        else
          load_files(val, restype)
      end
      dict
    end
  else
    data
  end
end

#resolveVal(value) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 163

def resolveVal(value)
  case value
  when Hash
    if value.key? 'Ref'
      if @params.nil?
        # no parameters, we are probably in a sub template, just return the ref and hope
        # a parent template has what it takes to resolve the ref
        value
      else # parameters are set for this template - we can try to resolve
        res = @params[value['Ref']] || ((@data['Parameters']||{})[value['Ref']] || {})['Default']
        if res.nil?
          raise CloudFormationTool::Errors::AppError, "Reference #{value['Ref']} can't be resolved"
        end
        res
      end
    else
      raise CloudFormationTool::Errors::AppError, "Value #{value} is not a valid value or reference"
    end
  else
    value
  end
end

#rewriteParameters(data, key, rewrites) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 142

def rewriteParameters data, key, rewrites
  (@data["Parameters"]||={}).each do |name, param|
    unless data.has_key? name
      @data["Parameters"][name] = param
      next
    end
    
    next if param['Default'] == data[name]['Default']
    
    if data[name].has_key?('Override') and data[name]['Override'] == false
      data.delete(name)
      next
    end
    
    newname = "#{key}z#{name}"
    log "Rewriting conflicting parameter #{name} (='#{data[name]['Default']}') to #{newname}"
    data[newname] = data.delete name
    rewrites[name] = newname
  end
end

#to_yaml(parameters = {}) ⇒ Object



47
48
49
50
# File 'lib/cloud_formation_tool/cloud_formation.rb', line 47

def to_yaml(parameters = {})
  @params = parameters
  compile.to_yaml
end