Class: Aws::Cfn::Dsl::Template

Inherits:
TemplateDSL
  • Object
show all
Defined in:
lib/aws/cfn/dsl/template.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path = nil, &block) ⇒ Template

Returns a new instance of Template.



17
18
19
20
21
22
# File 'lib/aws/cfn/dsl/template.rb', line 17

def initialize(path=nil,&block)
  @path = path || File.dirname(caller[2].split(%r'\s+').shift.split(':')[0])
  super() do
    # We do nothing with the template for now
  end
end

Instance Attribute Details

#dictObject (readonly)

Returns the value of attribute dict.



11
12
13
# File 'lib/aws/cfn/dsl/template.rb', line 11

def dict
  @dict
end

Instance Method Details

#exec!(argv = ARGV) ⇒ Object



94
95
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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
250
251
252
253
254
255
# File 'lib/aws/cfn/dsl/template.rb', line 94

def exec!(argv=ARGV)
  @opts = Slop.parse(help: true) do
    banner "usage: #{$PROGRAM_NAME} <expand|diff|validate|create|update|delete>"
    on :o, :output=,        'The template file to save this DSL expansion to', as: String
  end

  action = argv[0] || 'expand'
  unless %w(expand diff validate create update delete).include? action
    $stderr.puts "usage: #{$PROGRAM_NAME} <expand|diff|validate|create|update|delete>"
    exit(2)
  end
  unless (argv & %w(--template-file --template-url)).empty?
    $stderr.puts "#{File.basename($PROGRAM_NAME)}:  The --template-file and --template-url command-line options are not allowed. (You are running the template itself right now ... !)"
    exit(2)
  end

  # Find parameters where extension attribute :Immutable is true then remove it from the
  # cfn template since we can't pass it to CloudFormation.
  immutable_parameters = excise_parameter_attribute!(:Immutable)

  # Tag CloudFormation stacks based on :Tags defined in the template
  cfn_tags = excise_tags!
  # The command line string looks like: --tag "Key=key; Value=value" --tag "Key2=key2; Value2=value"
  cfn_tags_options = cfn_tags.sort.map { |tag| ["--tag", "Key=%s; Value=%s" % tag.split('=')] }.flatten

  # example: <template.rb> cfn-create-stack my-stack-name --parameters "Env=prod" --region eu-west-1
  # Execute the AWS CLI cfn-cmd command to validate/create/update a CloudFormation stack.
  if action == 'diff' or (action == 'expand' and not nopretty)
    template_string = JSON.pretty_generate(self)
  else
    template_string = JSON.generate(self)
  end

  if action == 'expand'
    # Write the pretty-printed JSON template to stdout and exit.  [--nopretty] option writes output with minimal whitespace
    # example: <template.rb> expand --parameters "Env=prod" --region eu-west-1 --nopretty
    if @opts[:output]
      dest = @opts[:output]
      if File.directory? dest
        file = File.basename $PROGRAM_NAME
        file.gsub!(%r'\.rb', '.json')
        dest = File.join dest, file
      end
      IO.write(dest, template_string)
    else
      puts template_string
    end
    exit(true)
  end

  temp_file = File.absolute_path("#{$PROGRAM_NAME}.expanded.json")
  File.write(temp_file, template_string)

  cmdline = ['cfn-cmd'] + argv + ['--template-file', temp_file] + cfn_tags_options

  case action
    when 'diff'
      # example: <template.rb> diff my-stack-name --parameters "Env=prod" --region eu-west-1
      # Diff the current template for an existing stack with the expansion of this template.

      # The --parameters and --tag options were used to expand the template but we don't need them anymore.  Discard.
      _, cfn_options = extract_options(argv[1..-1], %w(), %w(--parameters --tag))

      # Separate the remaining command-line options into options for 'cfn-cmd' and options for 'diff'.
      cfn_options, diff_options = extract_options(cfn_options, %w(),
                                                  %w(--stack-name --region --parameters --connection-timeout -I --access-key-id -S --secret-key -K --ec2-private-key-file-path -U --url))

      # If the first argument is a stack name then shift it from diff_options over to cfn_options.
      if diff_options[0] && !(/^-/ =~ diff_options[0])
        cfn_options.unshift(diff_options.shift)
      end

      # Run CloudFormation commands to describe the existing stack
      cfn_options_string           = cfn_options.map { |arg| "'#{arg}'" }.join(' ')
      old_template_raw             = exec_capture_stdout("cfn-cmd cfn-get-template #{cfn_options_string}")
      # ec2 template output is not valid json: TEMPLATE  "<json>\n"\n
      old_template_object          = JSON.parse(old_template_raw[11..-3])
      old_template_string          = JSON.pretty_generate(old_template_object)
      old_stack_attributes         = exec_describe_stack(cfn_options_string)
      old_tags_string              = old_stack_attributes["TAGS"]
      old_parameters_string        = old_stack_attributes["PARAMETERS"]

      # Sort the tag strings alphabetically to make them easily comparable
      old_tags_string = (old_tags_string || '').split(';').sort.map { |tag| %Q(TAG "#{tag}"\n) }.join
      tags_string     = cfn_tags.sort.map { |tag| "TAG \"#{tag}\"\n" }.join

      # Sort the parameter strings alphabetically to make them easily comparable
      old_parameters_string = (old_parameters_string || '').split(';').sort.map { |param| %Q(PARAMETER "#{param}"\n) }.join
      parameters_string     = parameters.sort.map { |key, value| "PARAMETER \"#{key}=#{value}\"\n" }.join

      # Diff the expanded template with the template from CloudFormation.
      old_temp_file = File.absolute_path("#{$PROGRAM_NAME}.current.json")
      new_temp_file = File.absolute_path("#{$PROGRAM_NAME}.expanded.json")
      File.write(old_temp_file, old_tags_string + old_parameters_string + old_template_string)
      File.write(new_temp_file, tags_string + parameters_string + template_string)

      # Compare templates
      system(*["diff"] + diff_options + [old_temp_file, new_temp_file])

      File.delete(old_temp_file)
      File.delete(new_temp_file)

      exit(true)

    when 'cfn-validate-template'
      # The cfn-validate-template command doesn't support --parameters so remove it if it was provided for template expansion.
      _, cmdline = extract_options(cmdline, %w(), %w(--parameters --tag))

    when 'cfn-update-stack'
      # Pick out the subset of cfn-update-stack options that apply to cfn-describe-stacks.
      cfn_options, other_options = extract_options(argv[1..-1], %w(),
                                                   %w(--stack-name --region --connection-timeout -I --access-key-id -S --secret-key -K --ec2-private-key-file-path -U --url))

      # If the first argument is a stack name then shift it over to cfn_options.
      if other_options[0] && !(/^-/ =~ other_options[0])
        cfn_options.unshift(other_options.shift)
      end

      # Run CloudFormation command to describe the existing stack
      cfn_options_string = cfn_options.map { |arg| "'#{arg}'" }.join(' ')
      old_stack_attributes = exec_describe_stack(cfn_options_string)

      # If updating a stack and some parameters are marked as immutable, fail if the new parameters don't match the old ones.
      if not immutable_parameters.empty?
        old_parameters_string = old_stack_attributes["PARAMETERS"]
        old_parameters = Hash[(old_parameters_string || '').split(';').map { |pair| pair.split('=', 2) }]
        new_parameters = parameters

        immutable_parameters.sort.each do |param|
          if old_parameters[param].to_s != new_parameters[param].to_s
            $stderr.puts "Error: cfn-update-stack may not update immutable parameter " +
                             "'#{param}=#{old_parameters[param]}' to '#{param}=#{new_parameters[param]}'."
            exit(false)
          end
        end
      end

      # Tags are immutable in CloudFormation.  The cfn-update-stack command doesn't support --tag options, so remove
      # the argument (if it exists) and validate against the existing stack to ensure tags haven't changed.
      # Compare the sorted arrays for an exact match
      old_cfn_tags = old_stack_attributes['TAGS'].split(';').sort rescue [] # Use empty Array if .split fails
      if cfn_tags != old_cfn_tags
        $stderr.puts "CloudFormation stack tags do not match and cannot be updated. You must either use the same tags or create a new stack." +
                         "\n" + (old_cfn_tags - cfn_tags).map {|tag| "< #{tag}" }.join("\n") +
                         "\n" + "---" +
                         "\n" + (cfn_tags - old_cfn_tags).map {|tag| "> #{tag}"}.join("\n")
        exit(false)
      end
      _, cmdline = extract_options(cmdline, %w(), %w(--tag))
  end

  # Execute command cmdline
  unless system(*cmdline)
    $stderr.puts "\nExecution of 'cfn-cmd' failed.  To facilitate debugging, the generated JSON template " +
                     "file was not deleted.  You may delete the file manually if it isn't needed: #{temp_file}"
    exit(false)
  end

  File.delete(temp_file)

  exit(true)
end

#file(b) ⇒ Object



24
25
26
27
# File 'lib/aws/cfn/dsl/template.rb', line 24

def file(b)
  block = File.read File.join(@path,b)
  eval block
end

#hash_refs(line, scope) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/aws/cfn/dsl/template.rb', line 73

def hash_refs(line,scope)
  match = line.match %r/^(.*?)(\{\s*:\S+\s*=>.*?\}|\{\s*\S+:\s*.*?\})(.*)$/
  if match
    h = nil
    eval "h = #{match[2]}", binding
    k = h.keys[0]
    v = h.delete(k)
    v = if v.is_a?Array
          v.map{|e| e.to_s }
        else
          v.to_s
        end

    h[k.to_s] = v
    scope[:logger].debug h
    [match[1], h, hash_refs(match[3],scope) ]
  else
    "#{line}\n"
  end
end

#mapping(name, options = nil) ⇒ Object



29
30
31
32
33
34
35
# File 'lib/aws/cfn/dsl/template.rb', line 29

def mapping(name, options=nil)
  if options.nil?
    file "Mappings/#{name}.rb"
  else
    super(name,options)
  end
end

#output(name, options = nil) ⇒ Object

def resource_file(p)

file "Resources/#{p}.rb"

end



65
66
67
68
69
70
71
# File 'lib/aws/cfn/dsl/template.rb', line 65

def output(name, options=nil)
  if options.nil?
    file "Outputs/#{name}.rb"
  else
    super(name,options)
  end
end

#parameter(name, options = nil) ⇒ Object

def mapping_file(p)

file "Mappings/#{p}.rb"

end



41
42
43
44
45
46
47
# File 'lib/aws/cfn/dsl/template.rb', line 41

def parameter(name, options=nil)
  if options.nil?
    file "Parameters/#{name}.rb"
  else
    super(name,options)
  end
end

#resource(name, options = nil) ⇒ Object

def parameter_file(p)

file "Parameters/#{p}.rb"

end



53
54
55
56
57
58
59
# File 'lib/aws/cfn/dsl/template.rb', line 53

def resource(name, options=nil)
  if options.nil?
    file "Resources/#{name}.rb"
  else
    super(name,options)
  end
end