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

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

Instance Method Summary collapse

Constructor Details

#initialize(&block) ⇒ Template

Returns a new instance of Template.



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

def initialize(&block)
  @path = File.dirname(caller[2].split(%r'\s+').shift.split(':')[0])
  super
end

Instance Method Details

#exec!(argv = ARGV) ⇒ Object

def output_file(p)

file "Outputs/#{p}.rb"

end



70
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
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
# File 'lib/aws/cfn/dsl/template.rb', line 70

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



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

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

#mapping(name, options = nil) ⇒ Object



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

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



58
59
60
61
62
63
64
# File 'lib/aws/cfn/dsl/template.rb', line 58

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



34
35
36
37
38
39
40
# File 'lib/aws/cfn/dsl/template.rb', line 34

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



46
47
48
49
50
51
52
# File 'lib/aws/cfn/dsl/template.rb', line 46

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