lono 3.5.0 → 4.0.0

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.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +15 -4
  3. data/.rspec +1 -0
  4. data/CHANGELOG.md +15 -1
  5. data/Gemfile +3 -3
  6. data/Guardfile +17 -8
  7. data/{LICENSE → LICENSE.txt} +1 -1
  8. data/README.md +20 -12
  9. data/Rakefile +1 -2
  10. data/{bin → exe}/lono +1 -0
  11. data/lib/lono.rb +12 -9
  12. data/lib/lono/cfn.rb +7 -9
  13. data/lib/lono/cfn/{aws_services.rb → aws_service.rb} +1 -1
  14. data/lib/lono/cfn/base.rb +41 -38
  15. data/lib/lono/cfn/create.rb +6 -2
  16. data/lib/lono/cfn/delete.rb +2 -2
  17. data/lib/lono/cfn/diff.rb +1 -1
  18. data/lib/lono/cfn/preview.rb +26 -15
  19. data/lib/lono/cfn/update.rb +11 -9
  20. data/lib/lono/cfn/util.rb +3 -3
  21. data/lib/lono/clean.rb +1 -1
  22. data/lib/lono/cli.rb +71 -39
  23. data/lib/lono/command.rb +42 -18
  24. data/lib/lono/completer.rb +162 -0
  25. data/lib/lono/completer/script.rb +6 -0
  26. data/lib/lono/completer/script.sh +10 -0
  27. data/lib/lono/completion.rb +15 -0
  28. data/lib/lono/core.rb +23 -9
  29. data/lib/lono/core/config.rb +20 -0
  30. data/lib/lono/default/settings.yml +33 -13
  31. data/lib/lono/help.rb +6 -79
  32. data/lib/lono/help/cfn.md +6 -0
  33. data/lib/lono/help/cfn/create.md +22 -0
  34. data/lib/lono/help/cfn/delete.md +5 -0
  35. data/lib/lono/help/cfn/diff.md +5 -0
  36. data/lib/lono/help/cfn/download.md +5 -0
  37. data/lib/lono/help/cfn/preview.md +11 -0
  38. data/lib/lono/help/cfn/update.md +21 -0
  39. data/lib/lono/help/completion.md +22 -0
  40. data/lib/lono/help/completion_script.md +3 -0
  41. data/lib/lono/help/generate.md +7 -0
  42. data/lib/lono/help/hello.md +5 -0
  43. data/lib/lono/help/import.md +7 -0
  44. data/lib/lono/help/inspect.md +4 -0
  45. data/lib/lono/help/inspect/depends.md +3 -0
  46. data/lib/lono/help/inspect/summary.md +3 -0
  47. data/lib/lono/help/new.md +8 -0
  48. data/lib/lono/help/param.md +3 -0
  49. data/lib/lono/{param/help.rb → help/param/generate.md} +1 -9
  50. data/lib/lono/help/script/build.md +5 -0
  51. data/lib/lono/help/script/upload.md +8 -0
  52. data/lib/lono/help/template.md +4 -0
  53. data/lib/lono/help/template/bashify.md +4 -0
  54. data/lib/lono/help/template/generate.md +7 -0
  55. data/lib/lono/help/user_data.md +3 -0
  56. data/lib/lono/importer.rb +43 -20
  57. data/lib/lono/inspector.rb +2 -19
  58. data/lib/lono/inspector/base.rb +2 -2
  59. data/lib/lono/inspector/{depends.rb → graph.rb} +3 -3
  60. data/lib/lono/inspector/summary.rb +1 -1
  61. data/lib/lono/new.rb +79 -26
  62. data/lib/lono/new/helper.rb +16 -0
  63. data/lib/lono/new/message.rb +35 -0
  64. data/lib/lono/param.rb +1 -2
  65. data/lib/lono/param/generator.rb +34 -86
  66. data/lib/lono/project_checker.rb +35 -40
  67. data/lib/lono/script.rb +19 -0
  68. data/lib/lono/script/base.rb +9 -0
  69. data/lib/lono/script/build.rb +73 -0
  70. data/lib/lono/script/upload.rb +81 -0
  71. data/lib/lono/sequence.rb +33 -0
  72. data/lib/lono/setting.rb +83 -0
  73. data/lib/lono/template.rb +8 -9
  74. data/lib/lono/template/{aws_services.rb → aws_service.rb} +1 -1
  75. data/lib/lono/template/context.rb +73 -0
  76. data/lib/lono/template/dsl.rb +63 -64
  77. data/lib/lono/template/helper.rb +201 -0
  78. data/lib/lono/template/template.rb +29 -221
  79. data/lib/lono/template/upload.rb +41 -33
  80. data/lib/lono/upgrade4.rb +175 -0
  81. data/lib/lono/user_data.rb +31 -0
  82. data/lib/lono/version.rb +1 -1
  83. data/lib/starter_projects/autoscaling/.gitignore +1 -0
  84. data/lib/starter_projects/{json_project → autoscaling}/Gemfile +0 -0
  85. data/lib/starter_projects/{yaml_project → autoscaling}/Guardfile +0 -0
  86. data/lib/starter_projects/autoscaling/README.md +118 -0
  87. data/lib/starter_projects/autoscaling/app/definitions/base.rb +2 -0
  88. data/lib/starter_projects/autoscaling/app/templates/autoscaling.yml +682 -0
  89. data/lib/starter_projects/autoscaling/config/params/base/autoscaling.txt +6 -0
  90. data/lib/starter_projects/autoscaling/config/settings.yml +33 -0
  91. data/lib/starter_projects/ec2/.gitignore +1 -0
  92. data/lib/starter_projects/{yaml_project → ec2}/Gemfile +0 -0
  93. data/lib/starter_projects/{json_project → ec2}/Guardfile +1 -1
  94. data/lib/starter_projects/ec2/README.md +86 -0
  95. data/lib/starter_projects/ec2/app/definitions/base.rb +2 -0
  96. data/lib/starter_projects/ec2/app/definitions/development.rb +1 -0
  97. data/lib/starter_projects/ec2/app/definitions/production.rb +1 -0
  98. data/lib/starter_projects/{yaml_project → ec2/app}/helpers/my_custom_helper.rb +0 -0
  99. data/lib/starter_projects/{json_project/templates/user_data/app.sh → ec2/app/partials/user_data/bootstrap.sh} +1 -2
  100. data/lib/starter_projects/{yaml_project → ec2/app}/templates/example.yml +0 -0
  101. data/lib/starter_projects/{json_project/params/base/api-web.txt → ec2/config/params/base/example.txt} +0 -0
  102. data/lib/starter_projects/ec2/config/params/development/example.txt +3 -0
  103. data/lib/starter_projects/ec2/config/params/production/example.txt +2 -0
  104. data/lib/starter_projects/ec2/config/settings.yml +33 -0
  105. data/lib/starter_projects/ec2/config/variables/base.rb +3 -0
  106. data/lib/starter_projects/ec2/config/variables/development.rb +2 -0
  107. data/lib/starter_projects/ec2/config/variables/production.rb +2 -0
  108. data/lib/starter_projects/ec2/welcome.txt +8 -0
  109. data/lib/starter_projects/skeleton/.gitignore +1 -0
  110. data/lib/starter_projects/skeleton/Gemfile +3 -0
  111. data/lib/starter_projects/skeleton/Guardfile +12 -0
  112. data/lib/starter_projects/skeleton/README.md +53 -0
  113. data/{spec/fixtures/my_project/templates/.gitkeep → lib/starter_projects/skeleton/app/definitions/base.rb} +0 -0
  114. data/lib/starter_projects/skeleton/config/settings.yml +33 -0
  115. data/lib/starter_projects/skeleton/welcome.txt +7 -0
  116. data/lono.gemspec +12 -10
  117. data/spec/fixtures/lono_project/.gitignore +1 -0
  118. data/spec/fixtures/lono_project/Gemfile +3 -0
  119. data/spec/fixtures/lono_project/Guardfile +12 -0
  120. data/spec/fixtures/lono_project/app/definitions/base.rb +10 -0
  121. data/spec/fixtures/lono_project/app/definitions/base/more.rb +7 -0
  122. data/spec/fixtures/lono_project/app/definitions/development.rb +1 -0
  123. data/spec/fixtures/lono_project/app/definitions/production.rb +1 -0
  124. data/spec/fixtures/lono_project/app/helpers/custom_helper.rb +5 -0
  125. data/spec/fixtures/lono_project/app/partials/security_group.yml +10 -0
  126. data/{lib/starter_projects/yaml_project/templates/partial → spec/fixtures/lono_project/app/partials}/user_data/bootstrap.sh +8 -2
  127. data/spec/fixtures/lono_project/app/templates/example.yml +50 -0
  128. data/{lib/starter_projects/yaml_project/params/base/api-web-prod.txt → spec/fixtures/lono_project/config/params/base/example.txt} +1 -0
  129. data/spec/fixtures/lono_project/config/params/development/example.txt +1 -0
  130. data/spec/fixtures/lono_project/config/params/production/example.txt +1 -0
  131. data/spec/fixtures/lono_project/config/settings.yml +31 -0
  132. data/spec/fixtures/lono_project/config/variables/base.rb +3 -0
  133. data/spec/fixtures/lono_project/config/variables/development.rb +1 -0
  134. data/spec/fixtures/lono_project/config/variables/production.rb +1 -0
  135. data/spec/fixtures/params/envonly/params/{prod → development}/network.txt +0 -0
  136. data/spec/fixtures/params/overlay/params/{prod → development}/network.txt +0 -0
  137. data/spec/fixtures/raw_templates/aws-waf-security-automations.template +2 -2
  138. data/spec/lib/lono/cfn_spec.rb +6 -9
  139. data/spec/lib/lono/cli_spec.rb +44 -0
  140. data/spec/lib/lono/completion_spec.rb +17 -0
  141. data/spec/lib/lono/inspect_spec.rb +6 -15
  142. data/spec/lib/lono/param/generator_spec.rb +45 -26
  143. data/spec/lib/lono/param_spec.rb +1 -3
  144. data/spec/lib/lono/setting_spec.rb +47 -0
  145. data/spec/lib/lono/template/dsl_spec.rb +33 -157
  146. data/spec/lib/lono/template_spec.rb +4 -16
  147. data/spec/spec_helper.rb +45 -14
  148. metadata +168 -82
  149. data/.coveralls.yml +0 -1
  150. data/lib/lono/cfn/help.rb +0 -103
  151. data/lib/lono/current_region.rb +0 -42
  152. data/lib/lono/inspector/help.rb +0 -21
  153. data/lib/lono/settings.rb +0 -45
  154. data/lib/lono/template/help.rb +0 -25
  155. data/lib/lono/template/helpers.rb +0 -136
  156. data/lib/starter_projects/json_project/.gitignore +0 -1
  157. data/lib/starter_projects/json_project/config/templates/base/blog.rb +0 -20
  158. data/lib/starter_projects/json_project/config/templates/base/stacks.rb +0 -58
  159. data/lib/starter_projects/json_project/templates/db.json +0 -212
  160. data/lib/starter_projects/json_project/templates/partial/host_record.json +0 -28
  161. data/lib/starter_projects/json_project/templates/partial/server.json +0 -45
  162. data/lib/starter_projects/json_project/templates/user_data/db.sh +0 -39
  163. data/lib/starter_projects/json_project/templates/user_data/db2.sh +0 -2
  164. data/lib/starter_projects/json_project/templates/user_data/ruby_script.rb +0 -5
  165. data/lib/starter_projects/json_project/templates/web.json +0 -386
  166. data/lib/starter_projects/yaml_project/.gitignore +0 -1
  167. data/lib/starter_projects/yaml_project/config/templates/base/blog.rb +0 -20
  168. data/lib/starter_projects/yaml_project/config/templates/base/stacks.rb +0 -56
  169. data/lib/starter_projects/yaml_project/config/templates/prod/stacks.rb +0 -1
  170. data/lib/starter_projects/yaml_project/config/templates/stag/stacks.rb +0 -1
  171. data/lib/starter_projects/yaml_project/config/variables/base/variables.rb +0 -4
  172. data/lib/starter_projects/yaml_project/config/variables/prod/variables.rb +0 -1
  173. data/lib/starter_projects/yaml_project/config/variables/stag/variables.rb +0 -1
  174. data/lib/starter_projects/yaml_project/params/base/example.txt +0 -2
  175. data/lib/starter_projects/yaml_project/params/prod/example.txt +0 -1
  176. data/lib/starter_projects/yaml_project/params/stag/example.txt +0 -1
  177. data/lib/starter_projects/yaml_project/templates/db.yml +0 -148
  178. data/lib/starter_projects/yaml_project/templates/partial/host_record.yml +0 -14
  179. data/lib/starter_projects/yaml_project/templates/partial/server.yml +0 -59
  180. data/lib/starter_projects/yaml_project/templates/web.yml +0 -206
  181. data/spec/fixtures/my_project/config/templates/base/stacks.rb +0 -3
  182. data/spec/fixtures/my_project/params/my-stack.txt +0 -3
  183. data/spec/fixtures/my_project/templates/my-stack.yml +0 -0
  184. data/spec/lib/lono/new_spec.rb +0 -59
  185. data/spec/lib/lono/template/template_spec.rb +0 -104
  186. data/spec/lib/lono_spec.rb +0 -27
@@ -1,11 +1,10 @@
1
1
  class Lono::Template::DSL
2
2
  def initialize(options={})
3
3
  @options = options
4
- @config_path = "#{Lono.root}/config"
5
4
  Lono::ProjectChecker.check
5
+ Lono::ProjectChecker.empty_templates
6
6
  @templates = []
7
7
  @results = {}
8
- @detected_format = nil
9
8
  end
10
9
 
11
10
  def run(options={})
@@ -14,24 +13,29 @@ class Lono::Template::DSL
14
13
  write_output
15
14
  end
16
15
 
17
- # Instance eval's all the files within each folder under
18
- # config/lono/base and config/lono/[Lono.env]
19
- # Base gets base first and then the Lono.env configs get evaluate second.
20
- # This means the env specific configs override the base configs.
16
+ # Instance eval's the template declarations in app/definitions in this order:
17
+ #
18
+ # app/definitions/base.rb
19
+ # app/definitions/base - all files in folder
20
+ # app/definitions/[Lono.env].rb
21
+ # app/definitions/[Lono.env] - all files in folder
22
+ #
23
+ # So Lono.env specific template declarations override base template declarations.
21
24
  def evaluate_templates
25
+ evaluate_template("base")
22
26
  evaluate_folder("base")
27
+ evaluate_template(Lono.env)
23
28
  evaluate_folder(Lono.env)
24
- @detected_format = detect_format
25
29
  end
26
30
 
27
- def evaluate_folder(folder)
28
- paths = Dir.glob("#{@config_path}/templates/#{folder}/**/*")
29
- paths.select{ |e| File.file?(e) }.each do |path|
30
- evaluate_template(path)
31
- end
31
+ def evaluate_template(name)
32
+ path = "#{Lono.config.definitions_path}/#{name}.rb"
33
+ evaluate_template_path(path)
32
34
  end
33
35
 
34
- def evaluate_template(path)
36
+ def evaluate_template_path(path)
37
+ return unless File.exist?(path)
38
+
35
39
  begin
36
40
  instance_eval(File.read(path), path)
37
41
  rescue Exception => e
@@ -41,6 +45,13 @@ class Lono::Template::DSL
41
45
  end
42
46
  end
43
47
 
48
+ def evaluate_folder(folder)
49
+ paths = Dir.glob("#{Lono.config.definitions_path}/#{folder}/**/*")
50
+ paths.select{ |e| File.file?(e) }.each do |path|
51
+ evaluate_template_path(path)
52
+ end
53
+ end
54
+
44
55
  # Prints out a user friendly task_definition error message
45
56
  def template_evaluation_error(e)
46
57
  error_info = e.backtrace.first
@@ -65,85 +76,73 @@ class Lono::Template::DSL
65
76
  end
66
77
  end
67
78
 
68
- # Detects the format of the templates. Checks the extension of all the
69
- # templates files.
70
- # All the templates must be of the same format, either all json or all yaml.
71
- def detect_format
72
- extensions = Dir.glob("#{Lono.root}/templates/**/*").map do |path|
73
- File.extname(path).sub(/^\./,'')
74
- end.reject(&:empty?).uniq
75
- extensions.include?('yml') ? 'yml' : 'json' # defaults to yml - falls back to json
76
- end
77
-
78
79
  def template(name, &block)
79
80
  @templates << {name: name, block: block}
80
81
  end
81
82
 
82
83
  def build_templates
83
- options = @options.merge(detected_format: @detected_format)
84
84
  @templates.each do |t|
85
- @results[t[:name]] = Lono::Template::Template.new(t[:name], t[:block], options).build
85
+ @results[t[:name]] = Lono::Template::Template.new(t[:name], t[:block], @options).build
86
86
  end
87
87
  end
88
88
 
89
89
  def write_output
90
- output_path = "#{Lono.root}/output"
90
+ output_path = "#{Lono.config.output_path}/templates"
91
91
  FileUtils.rm_rf(output_path) if @options[:clean]
92
- FileUtils.mkdir(output_path) unless File.exist?(output_path)
92
+ FileUtils.mkdir_p(output_path)
93
93
  puts "Generating CloudFormation templates:" unless @options[:quiet]
94
94
  @results.each do |name,text|
95
95
  path = "#{output_path}/#{name}".sub(/^\.\//,'') # strip leading '.'
96
- path += ".#{@detected_format}"
96
+ path += ".yml"
97
97
  puts " #{path}" unless @options[:quiet]
98
98
  ensure_parent_dir(path)
99
- validate(text, path)
100
- File.open(path, 'w') do |f|
101
- f.write(output_format(text))
102
- end
103
- end
104
- end
105
-
106
- def validate(text, path)
107
- if @detected_format == "json"
108
- validate_json(text, path)
109
- else
110
- validate_yaml(text, path)
99
+ text = commented(text)
100
+ IO.write(path, text) # write file first so validate method is simpler
101
+ validate(path)
111
102
  end
112
103
  end
113
104
 
114
- def validate_yaml(yaml, path)
105
+ def validate(path)
106
+ text = IO.read(path)
115
107
  begin
116
- YAML.load(yaml)
108
+ YAML.load(text)
117
109
  rescue Psych::SyntaxError => e
118
- puts "Invalid yaml. Output written to #{path} for debugging".colorize(:red)
119
- puts "ERROR: #{e.message}".colorize(:red)
120
- File.open(path, 'w') {|f| f.write(yaml) }
121
- exit 1
110
+ handle_yaml_syntax_error(e, path)
122
111
  end
123
112
  end
124
113
 
125
- def validate_json(json, path)
126
- begin
127
- JSON.parse(json)
128
- rescue JSON::ParserError => e
129
- puts "Invalid json. Output written to #{path} for debugging".colorize(:red)
130
- puts "ERROR: #{e.message}".colorize(:red)
131
- File.open(path, 'w') {|f| f.write(json) }
132
- exit 1
133
- end
134
- end
114
+ def handle_yaml_syntax_error(e, path)
115
+ io = StringIO.new
116
+ io.puts "Invalid yaml. Output written to debugging: #{path}".colorize(:red)
117
+ io.puts "ERROR: #{e.message}".colorize(:red)
135
118
 
136
- def output_format(text)
137
- @options[:pretty] ? prettify(text) : text
138
- end
119
+ # Grab line info. Example error:
120
+ # ERROR: (<unknown>): could not find expected ':' while scanning a simple key at line 2 column 1
121
+ md = e.message.match(/at line (\d+) column (\d+)/)
122
+ line = md[1].to_i
139
123
 
140
- # Input text is either yaml or json.
141
- # Do not prettify yaml format because it removes the !Ref like CloudFormation notation
142
- def prettify(text)
143
- @detected_format == "json" ? JSON.pretty_generate(JSON.parse(text)) : yaml_format(text)
124
+ lines = IO.read(path).split("\n")
125
+ context = 5 # lines of context
126
+ top, bottom = [line-context-1, 0].max, line+context-1
127
+ spacing = lines.size.to_s.size
128
+ lines[top..bottom].each_with_index do |line_content, index|
129
+ line_number = top+index+1
130
+ if line_number == line
131
+ io.printf("%#{spacing}d %s\n".colorize(:red), line_number, line_content)
132
+ else
133
+ io.printf("%#{spacing}d %s\n", line_number, line_content)
134
+ end
135
+ end
136
+
137
+ if ENV['TEST']
138
+ io.string
139
+ else
140
+ puts io.string
141
+ exit 1
142
+ end
144
143
  end
145
144
 
146
- def yaml_format(text)
145
+ def commented(text)
147
146
  comment =<<~EOS
148
147
  # This file was generated with lono. Do not edit directly, the changes will be lost.
149
148
  # More info: http://lono.cloud
@@ -0,0 +1,201 @@
1
+ # This is included into Lono::Template::Context.
2
+ # It has access to the original thor CLI options via @options.
3
+ #
4
+ # @options gets passed into:
5
+ #
6
+ # Lono::Template::Context.new(@options)
7
+ module Lono::Template::Helper
8
+ # Bash code that is meant to included in user-data
9
+ def extract_scripts(options={})
10
+ check_s3_folder_settings!
11
+
12
+ settings = setting.data["extract_scripts"] || {}
13
+ options = settings.merge(options)
14
+ # defaults also here in case they are removed from settings
15
+ to = options[:to] || "/opt"
16
+ user = options[:as] || "ec2-user"
17
+
18
+ if Dir.glob("#{Lono.config.scripts_path}/*").empty?
19
+ puts "WARN: you are using the extract_scripts helper method but you do not have any app/scripts.".colorize(:yellow)
20
+ calling_line = caller[0].split(':')[0..1].join(':')
21
+ puts "Called from: #{calling_line}"
22
+ return ""
23
+ end
24
+
25
+ <<-BASH_CODE
26
+ # Generated from the lono extract_scripts helper.
27
+ # Downloads scripts from s3, extract them, and setup.
28
+ mkdir -p #{to}
29
+ aws s3 cp #{scripts_s3_path} #{to}/
30
+ cd #{to}
31
+ tar zxf #{to}/#{scripts_name}
32
+ chmod -R a+x #{to}/scripts
33
+ chown -R #{user}:#{user} #{to}/scripts
34
+ BASH_CODE
35
+ end
36
+
37
+ def check_s3_folder_settings!
38
+ return if setting.s3_folder
39
+
40
+ puts "Helper method called that requires the s3_folder to be set at:"
41
+ lines = caller.reject { |l| l =~ %r{lib/lono} } # hide internal lono trace
42
+ puts " #{lines[0]}"
43
+
44
+ puts "Please configure your settings.yml with an s3_folder.".colorize(:red)
45
+ puts "Detected AWS_PROFILE #{ENV['AWS_PROFILE'].inspect}"
46
+ exit 1
47
+ end
48
+
49
+ def scripts_name
50
+ File.basename(scripts_s3_path)
51
+ end
52
+
53
+ def scripts_s3_path
54
+ upload = Lono::Script::Upload.new
55
+ upload.s3_dest
56
+ end
57
+
58
+ def template_s3_path(template_name)
59
+ check_s3_folder_settings!
60
+ # high jacking Upload for useful s3_https_url method
61
+ template_path = "#{template_name}.yml"
62
+ upload = Lono::Template::Upload.new(@options)
63
+ upload.s3_https_url(template_path)
64
+ end
65
+
66
+ def template_params(param_name)
67
+ generator_options = {
68
+ allow_no_file: true
69
+ }.merge(@options)
70
+ generator = Lono::Param::Generator.new(param_name, generator_options)
71
+ # do not generate because lono cfn calling logic already generated it we only need the values
72
+ generator.params # Returns Array in underscore keys format
73
+ end
74
+
75
+ # Adjust the partial path so that it will use app/user_data
76
+ def user_data(path,vars={}, options={})
77
+ options.merge!(user_data: true)
78
+ partial(path,vars, options)
79
+ end
80
+
81
+ # The partial's path is a relative path.
82
+ #
83
+ # Example:
84
+ # Given file in app/partials/iam/docker.yml
85
+ #
86
+ # <%= partial("iam/docker", {}, indent: 10) %>
87
+ # <%= partial("iam/docker.yml", {}, indent: 10) %>
88
+ #
89
+ # If the user specifies the extension then use that instead of auto-adding
90
+ # the detected format.
91
+ def partial(path,vars={}, options={})
92
+ path = options[:user_data] ?
93
+ user_data_path_for(path) :
94
+ partial_path_for(path)
95
+ path = auto_add_format(path)
96
+
97
+ instance_variables!(vars)
98
+ result = render_path(path)
99
+
100
+ result = indent(result, options[:indent]) if options[:indent]
101
+ result + "\n"
102
+ end
103
+
104
+ # add indentation
105
+ def indent(text, indentation_amount)
106
+ text.split("\n").map do |line|
107
+ " " * indentation_amount + line
108
+ end.join("\n")
109
+ end
110
+
111
+ def partial_exist?(path)
112
+ path = partial_path_for(path)
113
+ path = auto_add_format(path)
114
+ path && File.exist?(path)
115
+ end
116
+
117
+ def current_region
118
+ region = Aws.config[:region]
119
+ region ||= ENV['AWS_REGION']
120
+ return region if region
121
+
122
+ default_region = 'us-east-1' # fallback if default not found in ~/.aws/config
123
+ if ENV['AWS_PROFILE']
124
+ path = "#{ENV['HOME']}/.aws/config"
125
+ if File.exist?(path)
126
+ lines = IO.readlines(path)
127
+ capture_default, capture_current = false, false
128
+ lines.each do | line|
129
+ if line.include?('[default]')
130
+ capture_default = true # next line
131
+ next
132
+ end
133
+ if capture_default && line.match(/region = /)
134
+ # over default from above
135
+ default_region = line.split(' = ').last.strip
136
+ capture_default = false
137
+ end
138
+
139
+ md = line.match(/\[profile (.*)\]/)
140
+ if md && md[1] == ENV['AWS_PROFILE']
141
+ capture_current = true
142
+ next
143
+ end
144
+ if capture_current && line.match(/region = /)
145
+ region = line.split(' = ').last.strip
146
+ capture_current = false
147
+ end
148
+ end
149
+ end
150
+
151
+ region ||= default_region
152
+ return region if region
153
+ end
154
+
155
+ 'us-east-1' # default
156
+ end
157
+
158
+ private
159
+ def render_path(path)
160
+ RenderMePretty.result(path, context: self)
161
+ end
162
+
163
+ def user_data_path_for(path)
164
+ "#{Lono.config.user_data_path}/#{path}"
165
+ end
166
+
167
+ def partial_path_for(path)
168
+ "#{Lono.config.partials_path}/#{path}"
169
+ end
170
+
171
+ def auto_add_format(path)
172
+ # Return immediately if user provided explicit extension
173
+ extension = File.extname(path) # current extension
174
+ return path if !extension.empty?
175
+
176
+ # Else let's auto detect
177
+ paths = Dir.glob("#{path}.*")
178
+
179
+ if paths.size == 1 # non-ambiguous match
180
+ return paths.first
181
+ end
182
+
183
+ if paths.size > 1 # ambiguous match
184
+ puts "ERROR: Multiple possible partials found:".colorize(:red)
185
+ paths.each do |path|
186
+ puts " #{path}"
187
+ end
188
+ puts "Please specify an extension in the name to remove the ambiguity.".colorize(:green)
189
+ exit 1
190
+ end
191
+
192
+ # Account for case when user wants to include a file with no extension at all
193
+ return path if File.exist?(path) && !File.directory?(path)
194
+
195
+ path # original path if this point is reached
196
+ end
197
+
198
+ def setting
199
+ @setting ||= Lono::Setting.new
200
+ end
201
+ end
@@ -3,249 +3,57 @@ require 'json'
3
3
  require 'base64'
4
4
 
5
5
  class Lono::Template::Template
6
- include Lono::Template::Helpers
7
- include Lono::CurrentRegion
8
6
  include ERB::Util
9
7
 
8
+ # Main template DSL methods are: source and variables
9
+ #
10
+ # template "example-2" do
11
+ # source "example"
12
+ # variables(test: 1)
13
+ # end
14
+ #
15
+ attr_reader :name
10
16
  def initialize(name, block=nil, options={})
11
17
  # Taking care to name instance variables with _ in front because we load the
12
18
  # variables from config/variables and those instance variables can clobber these
13
19
  # instance variables
14
- @_name = name
15
- @_options = options
16
- @_detected_format = options[:detected_format]
17
- @_block = block
18
- @_config_path = "#{Lono.root}/config"
19
- @_source = default_source(name)
20
- end
21
-
22
- def default_source(name)
23
- "#{Lono.root}/templates/#{name}.#{@_detected_format}" # defaults to name, source method overrides
24
- end
25
-
26
- def build
27
- load_variables
28
- load_custom_helpers
29
- instance_eval(&@_block) if @_block
30
- template = IO.read(@_source)
31
- erb_result(@_source, template)
32
- end
33
-
34
- def load_variables
35
- load_variables_folder("base")
36
- load_variables_folder(Lono.env)
37
- end
38
-
39
- # Load the variables defined in config/variables/* to make available in the
40
- # template blocks in config/templates/*.
41
- #
42
- # Example:
43
- #
44
- # `config/variables/base/variables.rb`:
45
- # @foo = 123
46
- #
47
- # `config/templates/base/resources.rb`:
48
- # template "mytemplate.yml" do
49
- # source "mytemplate.yml.erb"
50
- # variables(foo: @foo)
51
- # end
52
- #
53
- # NOTE: Only able to make instance variables avaialble with instance_eval
54
- # Wasnt able to make local variables available.
55
- def load_variables_folder(folder)
56
- paths = Dir.glob("#{@_config_path}/variables/#{folder}/**/*")
57
- paths.select{ |e| File.file? e }.each do |path|
58
- instance_eval(IO.read(path))
59
- end
60
- end
61
-
62
- # Load custom helper methods from the user's infra repo
63
- def load_custom_helpers
64
- Dir.glob("#{Lono.root}/helpers/**/*_helper.rb").each do |path|
65
- filename = path.sub(%r{.*/},'').sub('.rb','')
66
- module_name = filename.classify
67
-
68
- require path
69
- self.class.send :include, module_name.constantize
70
- end
20
+ @name = name
21
+ @block = block
22
+ @options = options
23
+ @source_path = default_source_path(name)
71
24
  end
72
25
 
26
+ # Returns path, example: ./app/templates/example.yml
73
27
  def source(path)
74
- @_source = path[0..0] == '/' ? path : "#{Lono.root}/templates/#{path}"
75
- @_source += ".#{@_detected_format}"
28
+ @source_path = path[0..0] == '/' ? path : "#{Lono.config.templates_path}/#{path}"
29
+ @source_path += ".yml"
76
30
  end
77
31
 
78
32
  def variables(vars={})
79
33
  vars.each do |var,value|
80
- instance_variable_set("@#{var}", value)
34
+ context.instance_variable_set("@#{var}", value)
81
35
  end
82
36
  end
83
37
 
84
- def erb_result(path, template)
85
- begin
86
- ERB.new(template, nil, "-").result(binding)
87
- rescue Exception => e
88
- puts e
89
- puts e.backtrace if ENV['DEBUG']
90
-
91
- # how to know where ERB stopped? - https://www.ruby-forum.com/topic/182051
92
- # syntax errors have the (erb):xxx info in e.message
93
- # undefined variables have (erb):xxx info in e.backtrac
94
- error_info = e.message.split("\n").grep(/\(erb\)/)[0]
95
- error_info ||= e.backtrace.grep(/\(erb\)/)[0]
96
- raise unless error_info # unable to find the (erb):xxx: error line
97
- line = error_info.split(':')[1].to_i
98
- puts "Error evaluating ERB template on line #{line.to_s.colorize(:red)} of: #{path.sub(/^\.\//, '').colorize(:green)}"
99
-
100
- template_lines = template.split("\n")
101
- context = 5 # lines of context
102
- top, bottom = [line-context-1, 0].max, line+context-1
103
- spacing = template_lines.size.to_s.size
104
- template_lines[top..bottom].each_with_index do |line_content, index|
105
- line_number = top+index+1
106
- if line_number == line
107
- printf("%#{spacing}d %s\n".colorize(:red), line_number, line_content)
108
- else
109
- printf("%#{spacing}d %s\n", line_number, line_content)
110
- end
111
- end
112
- exit 1 unless ENV['TEST']
113
- end
38
+ # internal methods
39
+ def default_source_path(name)
40
+ "#{Lono.config.templates_path}/#{name}.yml" # defaults to name, source method overrides
114
41
  end
115
42
 
116
- def transform_array(arr)
117
- arr.map! {|x| x =~ /=>/ ? x : x.inspect }
118
- arr.join(',')
119
- end
43
+ def build
44
+ instance_eval(&@block) if @block
120
45
 
121
- # transform each line of bash script to array with cloudformation template objects
122
- def transform(data)
123
- data = evaluate(data)
124
- if data[-1].is_a?(String)
125
- data[0..-2] + ["#{data[-1]}\n"]
46
+ if File.exist?(@source_path)
47
+ RenderMePretty.result(@source_path, context: context)
126
48
  else
127
- data + ["\n"]
49
+ puts "ERROR: #{@source_path} does not exist, but it was used as a template source.".colorize(:red)
50
+ exit 1
128
51
  end
129
52
  end
130
53
 
131
- # Input:
132
- # String
133
- # Output:
134
- # Array of parse positions
135
- #
136
- # The positions of tokens taking into account when brackets start and close,
137
- # handles nested brackets.
138
- def bracket_positions(line)
139
- positions,pair,count = [],[],0
140
-
141
- line.split('').each_with_index do |char,i|
142
- pair << i if pair.empty?
143
-
144
- first_pair_char = line[pair[0]]
145
- if first_pair_char == '{' # object logic
146
- if char == '{'
147
- count += 1
148
- end
149
-
150
- if char == '}'
151
- count -= 1
152
- if count == 0
153
- pair << i
154
- positions << pair
155
- pair = []
156
- end
157
- end
158
- else # string logic
159
- lookahead = line[i+1]
160
- if lookahead == '{'
161
- pair << i
162
- positions << pair
163
- pair = []
164
- end
165
- end
166
- end # end of loop
167
-
168
- # for string logic when lookahead does not contain a object token
169
- # need to clear out what's left to match the final pair
170
- if !pair.empty?
171
- pair << line.size - 1
172
- positions << pair
173
- end
174
-
175
- positions
176
- end
177
-
178
- # Input:
179
- # Array - bracket_positions
180
- # Ouput:
181
- # Array - positions that can be use to determine what to parse
182
- def parse_positions(line)
183
- positions = bracket_positions(line)
184
- positions.flatten
185
- end
186
-
187
- # Input
188
- # String line of code to decompose into chunks, some can be transformed into objects
189
- # Output
190
- # Array of strings, some can be transformed into objects
191
- #
192
- # Example:
193
- # line = 'a{b}c{d{d}d}e' # nested brackets
194
- # template.decompose(line).should == ['a','{b}','c','{d{d}d}','e']
195
- def decompose(line)
196
- positions = parse_positions(line)
197
- return [line] if positions.empty?
198
-
199
- result = []
200
- str = ''
201
- until positions.empty?
202
- left = positions.shift
203
- right = positions.shift
204
- token = line[left..right]
205
- # if cfn object, add to the result set but after clearing out
206
- # the temp str that is being built up when the token is just a string
207
- if cfn_object?(token)
208
- unless str.empty? # first token might be a object
209
- result << str
210
- str = ''
211
- end
212
- result << token
213
- else
214
- str << token # keeps building up the string
215
- end
216
- end
217
-
218
- # at the of the loop there's a leftover string, unless the last token
219
- # is an object
220
- result << str unless str.empty?
221
-
222
- result
223
- end
224
-
225
- def cfn_object?(s)
226
- exact = %w[Ref]
227
- pattern = %w[Fn::]
228
- exact_match = !!exact.detect {|word| s.include?(word)}
229
- pattern_match = !!pattern.detect {|p| s =~ Regexp.new(p)}
230
- (exact_match || pattern_match) && s =~ /^{/ && s =~ /=>/
231
- end
232
-
233
- def recompose(decomposition)
234
- decomposition.map { |s| cfn_object?(s) ? eval(s) : s }
235
- end
236
-
237
- def evaluate(line)
238
- recompose(decompose(line))
239
- end
240
-
241
- # For simple just parameters files that can also be generated with lono, the CFN
242
- # Fn::Base64 function is not available and as lono is not being used in the context
243
- # of CloudFormation. So this can be used in it's place.
244
- def encode_base64(text)
245
- Base64.strict_encode64(text).strip
246
- end
247
-
248
- def name
249
- @_name
54
+ # Context for ERB rendering.
55
+ # This is where we control what references get passed to the ERB rendering.
56
+ def context
57
+ @context ||= Lono::Template::Context.new(@options)
250
58
  end
251
59
  end