lono 1.1.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.gitmodules +3 -0
  3. data/CHANGELOG.md +8 -0
  4. data/README.md +150 -39
  5. data/bin/lono +2 -2
  6. data/circle.yml +4 -0
  7. data/lib/lono.rb +16 -7
  8. data/lib/lono/cfn.rb +64 -0
  9. data/lib/lono/cfn/aws_services.rb +37 -0
  10. data/lib/lono/cfn/base.rb +144 -0
  11. data/lib/lono/cfn/create.rb +34 -0
  12. data/lib/lono/cfn/delete.rb +26 -0
  13. data/lib/lono/cfn/diff.rb +43 -0
  14. data/lib/lono/cfn/help.rb +93 -0
  15. data/lib/lono/cfn/preview.rb +133 -0
  16. data/lib/lono/cfn/update.rb +62 -0
  17. data/lib/lono/cfn/util.rb +21 -0
  18. data/lib/lono/cli.rb +19 -10
  19. data/lib/lono/command.rb +25 -0
  20. data/lib/lono/help.rb +59 -0
  21. data/lib/lono/new.rb +3 -2
  22. data/lib/lono/param.rb +20 -0
  23. data/lib/lono/param/generator.rb +90 -0
  24. data/lib/lono/param/help.rb +15 -0
  25. data/lib/lono/project_checker.rb +44 -0
  26. data/lib/lono/template.rb +22 -248
  27. data/lib/lono/template/bashify.rb +39 -0
  28. data/lib/lono/template/dsl.rb +139 -0
  29. data/lib/lono/template/help.rb +25 -0
  30. data/lib/lono/template/template.rb +251 -0
  31. data/lib/lono/version.rb +1 -1
  32. data/lib/{starter_project_yaml → starter_projects/json_project}/Gemfile +0 -1
  33. data/lib/{starter_project_json → starter_projects/json_project}/Guardfile +0 -0
  34. data/lib/{starter_project_json → starter_projects/json_project}/config/lono.rb +0 -0
  35. data/lib/{starter_project_json → starter_projects/json_project}/config/lono/api.rb +0 -0
  36. data/lib/starter_projects/json_project/params/api-web-prod.txt +20 -0
  37. data/lib/{starter_project_json → starter_projects/json_project}/templates/db.json.erb +0 -0
  38. data/lib/{starter_project_json → starter_projects/json_project}/templates/partial/host_record.json.erb +0 -0
  39. data/lib/{starter_project_json → starter_projects/json_project}/templates/partial/server.json.erb +0 -0
  40. data/lib/{starter_project_json → starter_projects/json_project}/templates/user_data/app.sh.erb +0 -0
  41. data/lib/{starter_project_json → starter_projects/json_project}/templates/user_data/db.sh.erb +0 -0
  42. data/lib/{starter_project_json → starter_projects/json_project}/templates/user_data/db2.sh.erb +0 -0
  43. data/lib/{starter_project_json → starter_projects/json_project}/templates/user_data/ruby_script.rb.erb +0 -0
  44. data/lib/{starter_project_json → starter_projects/json_project}/templates/web.json.erb +0 -0
  45. data/lib/{starter_project_json → starter_projects/yaml_project}/Gemfile +0 -1
  46. data/lib/{starter_project_yaml → starter_projects/yaml_project}/Guardfile +0 -0
  47. data/lib/{starter_project_yaml → starter_projects/yaml_project}/config/lono.rb +0 -0
  48. data/lib/{starter_project_yaml → starter_projects/yaml_project}/config/lono/api.rb +0 -0
  49. data/lib/starter_projects/yaml_project/params/api-web-prod.txt +20 -0
  50. data/lib/{starter_project_yaml → starter_projects/yaml_project}/templates/db.yml.erb +0 -0
  51. data/lib/{starter_project_yaml → starter_projects/yaml_project}/templates/partial/host_record.yml.erb +0 -0
  52. data/lib/{starter_project_yaml → starter_projects/yaml_project}/templates/partial/server.yml.erb +0 -0
  53. data/lib/{starter_project_yaml → starter_projects/yaml_project}/templates/partial/user_data/bootstrap.sh.erb +0 -0
  54. data/lib/{starter_project_yaml → starter_projects/yaml_project}/templates/web.yml.erb +0 -0
  55. data/lono.gemspec +15 -10
  56. data/spec/fixtures/my_project/config/lono.rb +1 -0
  57. data/spec/fixtures/my_project/params/my-stack.txt +3 -0
  58. data/spec/fixtures/my_project/templates/.gitkeep +0 -0
  59. data/spec/fixtures/my_project/templates/my-stack.yml.erb +0 -0
  60. data/spec/lib/lono/cfn_spec.rb +35 -0
  61. data/spec/lib/lono/new_spec.rb +3 -3
  62. data/spec/lib/lono/param_spec.rb +15 -0
  63. data/spec/lib/lono/{dsl_spec.rb → template/dsl_spec.rb} +9 -9
  64. data/spec/lib/lono/template/template_spec.rb +104 -0
  65. data/spec/lib/lono/template_spec.rb +22 -37
  66. data/spec/lib/lono_spec.rb +6 -83
  67. data/vendor/plissken/Gemfile +14 -0
  68. data/vendor/plissken/LICENSE.txt +20 -0
  69. data/vendor/plissken/README.md +46 -0
  70. data/vendor/plissken/Rakefile +56 -0
  71. data/vendor/plissken/VERSION +1 -0
  72. data/vendor/plissken/lib/plissken.rb +1 -0
  73. data/vendor/plissken/lib/plissken/ext/hash/to_snake_keys.rb +45 -0
  74. data/vendor/plissken/plissken.gemspec +61 -0
  75. data/vendor/plissken/spec/lib/to_snake_keys_spec.rb +177 -0
  76. data/vendor/plissken/spec/spec_helper.rb +90 -0
  77. data/vendor/plissken/test/helper.rb +20 -0
  78. data/vendor/plissken/test/plissken/ext/hash/to_snake_keys_test.rb +184 -0
  79. data/vendor/plissken/test/test_plissken.rb +2 -0
  80. metadata +115 -39
  81. data/lib/lono/bashify.rb +0 -41
  82. data/lib/lono/cli/help.rb +0 -37
  83. data/lib/lono/dsl.rb +0 -132
@@ -0,0 +1,25 @@
1
+ require 'thor'
2
+
3
+ module Lono
4
+ class Command < Thor
5
+ class << self
6
+ def dispatch(m, args, options, config)
7
+ # Allow calling for help via:
8
+ # lono generate help
9
+ # lono generate -h
10
+ # lono generate --help
11
+ # lono generate -D
12
+ #
13
+ # as well thor's nomral setting as
14
+ #
15
+ # lono help generate
16
+ help_flags = Thor::HELP_MAPPINGS + ["help"]
17
+ if args.length > 1 && !(args & help_flags).empty?
18
+ args -= help_flags
19
+ args.insert(-2, "help")
20
+ end
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,59 @@
1
+ module Lono::Help
2
+ def new_long_desc
3
+ <<-EOL
4
+ Examples:
5
+
6
+ $ lono new project
7
+
8
+ $ lono new lono
9
+ EOL
10
+ end
11
+
12
+ def generate
13
+ <<-EOL
14
+ Examples:
15
+
16
+ $ lono generate
17
+
18
+ $ lono g -c # shortcut
19
+
20
+ Builds both CloudFormation template and parameter files based on lono project and writes them to the output folder on the filesystem.
21
+ EOL
22
+
23
+ end
24
+
25
+ def template
26
+ <<-EOL
27
+ Examples:
28
+
29
+ $ lono template generate --help
30
+
31
+ $ lono template bashify --help
32
+ EOL
33
+
34
+ end
35
+
36
+ def cfn
37
+ <<-EOL
38
+ Examples:
39
+
40
+ $ lono cfn create my-stack
41
+
42
+ $ lono cfn preview my-stack
43
+
44
+ $ lono cfn update my-stack
45
+
46
+ $ lono cfn delete my-stack
47
+ EOL
48
+ end
49
+
50
+ def param
51
+ <<-EOL
52
+ Examples:
53
+
54
+ $ lono param generate
55
+ EOL
56
+ end
57
+
58
+ extend self
59
+ end
@@ -9,11 +9,12 @@ module Lono
9
9
 
10
10
  def run
11
11
  puts "Setting up lono project" unless options[:quiet]
12
- source_root = File.expand_path("../../starter_project_#{@format}", __FILE__)
12
+ source_root = File.expand_path("../../starter_projects/#{@format}_project", __FILE__)
13
13
  paths = Dir.glob("#{source_root}/**/*").
14
14
  select {|p| File.file?(p) }
15
15
  paths.each do |src|
16
- regexp = Regexp.new(".*starter_project_#{@format}/")
16
+ # starter_projects/yaml_project/ ->
17
+ regexp = Regexp.new(".*starter_projects/#{@format}_project/")
17
18
  dest = src.gsub(regexp,'')
18
19
  dest = "#{@project_root}/#{dest}"
19
20
 
@@ -0,0 +1,20 @@
1
+ require "thor"
2
+
3
+ module Lono
4
+ class Param < Command
5
+ autoload :Help, 'lono/param/help'
6
+ autoload :Generator, 'lono/param/generator'
7
+
8
+ class_option :verbose, type: :boolean
9
+ class_option :noop, type: :boolean
10
+ class_option :mute, type: :boolean
11
+ class_option :project_root, desc: "project root to use", default: '.'
12
+
13
+ desc "generate NAME", "generate parameter json file for NAME"
14
+ long_desc Help.generate
15
+ option :path, desc: "Name of the source that maps to the params txt file. name -> params/NAME.txt. Use this to override the params/NAME.txt convention"
16
+ def generate
17
+ Generator.generate_all(options)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,90 @@
1
+ class Lono::Param::Generator
2
+ def self.generate_all(options)
3
+ puts "Generating params files"
4
+ project_root = options[:project_root] || '.'
5
+ Dir.glob("#{project_root}/params/**/*.txt").each do |path|
6
+ next if File.directory?(path)
7
+ name = path.sub(/.*params\//, '').sub('.txt', '')
8
+ param = Lono::Param::Generator.new(name, options)
9
+ param.generate
10
+ end
11
+ end
12
+
13
+ def initialize(name, options)
14
+ @name = name
15
+ @options = options
16
+ @project_root = options[:project_root] || '.'
17
+ @source_path = options[:path] || "#{@project_root}/params/#{@name}.txt"
18
+ end
19
+
20
+ def generate
21
+ # useful option for lono cfn
22
+ return if @options[:allow_no_file] && !File.exist?(@source_path)
23
+
24
+ if File.exist?(@source_path)
25
+ contents = IO.read(@source_path)
26
+ data = convert_to_cfn_format(contents)
27
+ json = JSON.pretty_generate(data)
28
+ write_output(json)
29
+ puts "Params file generated for #{@name} at #{output_path}" unless @options[:mute]
30
+ else
31
+ puts "#{@source_path} could not be found? Are you sure it exist?"
32
+ exit 1
33
+ end
34
+ end
35
+
36
+ # useful for when calling CloudFormation via the aws-sdk gem
37
+ def params
38
+ # useful option for lono cfn
39
+ return {} if @options[:allow_no_file] && !File.exist?(@source_path)
40
+
41
+ contents = IO.read(@source_path)
42
+ convert_to_cfn_format(contents, :underscore)
43
+ end
44
+
45
+ def parse_contents(contents)
46
+ lines = contents.split("\n")
47
+ # remove comment at the end of the line
48
+ lines.map! { |l| l.sub(/#.*/,'').strip }
49
+ # filter out commented lines
50
+ lines = lines.reject { |l| l =~ /(^|\s)#/i }
51
+ # filter out empty lines
52
+ lines = lines.reject { |l| l.strip.empty? }
53
+ lines
54
+ end
55
+
56
+ def convert_to_cfn_format(contents, casing=:camel)
57
+ lines = parse_contents(contents)
58
+ params = []
59
+ lines.each do |line|
60
+ key,value = line.strip.split("=").map {|x| x.strip}
61
+ param = if value == "use_previous_value"
62
+ {
63
+ ParameterKey: key,
64
+ UsePreviousValue: true
65
+ }
66
+ elsif value
67
+ {
68
+ ParameterKey: key,
69
+ ParameterValue: value
70
+ }
71
+ end
72
+ if param
73
+ param = param.to_snake_keys if casing == :underscore
74
+ params << param
75
+ end
76
+ end
77
+ params
78
+ end
79
+
80
+ def output_path
81
+ "#{@project_root}/output/params/#{@name}.json".sub(/\.\//,'')
82
+ end
83
+
84
+ def write_output(json)
85
+ dir = File.dirname(output_path)
86
+ FileUtils.mkdir_p(dir) unless File.exist?(dir)
87
+ IO.write(output_path, json)
88
+ end
89
+
90
+ end
@@ -0,0 +1,15 @@
1
+ class Lono::Param::Help
2
+ class << self
3
+ def generate
4
+ <<-EOL
5
+ Example:
6
+
7
+ To generate a CloudFormation json parameter files in the params folder to the output/params folder.
8
+
9
+ $ lono-params generate
10
+
11
+ If you have params/my-stack.txt. It will generate a CloudFormation json file in output/params/my-stack.json.
12
+ EOL
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ module Lono
2
+ class ProjectChecker
3
+ # Checks to see command is running in a lono project.
4
+ # If not, provide a friendly message and exit.
5
+ def self.check(project_root)
6
+ new(project_root).check
7
+ end
8
+
9
+ def initialize(project_root)
10
+ @project_root = project_root
11
+ end
12
+
13
+ def check
14
+ config_folder_exist
15
+ templates_folder_exist
16
+ empty_folders
17
+ end
18
+
19
+ def config_folder_exist
20
+ unless File.exist?("#{@project_root}/config")
21
+ puts "The config folder does not exist in this project. Are you sure this is a lono project?"
22
+ exit 1
23
+ end
24
+ end
25
+
26
+ def templates_folder_exist
27
+ unless File.exist?("#{@project_root}/templates")
28
+ puts "The templates folder does not exist in this project. Are you sure this is a lono project?"
29
+ exit 1
30
+ end
31
+ end
32
+
33
+ def empty_folders
34
+ if Dir["#{@project_root}/config/**/*.rb"].empty?
35
+ puts "The config folder does not contain any lono template definitions."
36
+ exit 1
37
+ end
38
+ if Dir["#{@project_root}/templates/**/*"].empty?
39
+ puts "The templates folder does not contain any lono template definitions."
40
+ exit 1
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,253 +1,27 @@
1
- require 'erb'
2
- require 'json'
3
- require 'base64'
1
+ require "thor"
2
+ require_relative "command"
4
3
 
5
4
  module Lono
6
- class Template
7
- include ERB::Util
8
-
9
- attr_reader :name
10
- def initialize(name, block, options={})
11
- @name = name
12
- @block = block
13
- @options = options
14
- end
15
-
16
- def build
17
- instance_eval(&@block)
18
- template = IO.read(@source)
19
- erb_result(@source, template)
20
- end
21
-
22
- def source(path)
23
- @source = path[0..0] == '/' ? path : "#{@options[:project_root]}/templates/#{path}"
24
- end
25
-
26
- def variables(vars={})
27
- vars.each do |var,value|
28
- instance_variable_set("@#{var}", value)
29
- end
30
- end
31
-
32
- def partial(path,vars={}, options={})
33
- path = "#{@options[:project_root]}/templates/partial/#{path}"
34
- template = IO.read(path)
35
- variables(vars)
36
- result = erb_result(path, template)
37
- result = indent(result, options[:indent]) if options[:indent]
38
- result
39
- end
40
-
41
- # add indentation
42
- def indent(result, indentation_amount)
43
- result.split("\n").map do |line|
44
- " " * indentation_amount + line
45
- end.join("\n")
46
- end
47
-
48
- def erb_result(path, template)
49
- begin
50
- ERB.new(template, nil, "-").result(binding)
51
- rescue Exception => e
52
- puts e
53
-
54
- # how to know where ERB stopped? - https://www.ruby-forum.com/topic/182051
55
- # syntax errors have the (erb):xxx info in e.message
56
- # undefined variables have (erb):xxx info in e.backtrac
57
- error_info = e.message.split("\n").grep(/\(erb\)/)[0]
58
- error_info ||= e.backtrace.grep(/\(erb\)/)[0]
59
- raise unless error_info # unable to find the (erb):xxx: error line
60
- line = error_info.split(':')[1].to_i
61
- puts "Error evaluating ERB template on line #{line.to_s.colorize(:red)} of: #{path.sub(/^\.\//, '')}"
62
-
63
- template_lines = template.split("\n")
64
- context = 5 # lines of context
65
- top, bottom = [line-context-1, 0].max, line+context-1
66
- spacing = template_lines.size.to_s.size
67
- template_lines[top..bottom].each_with_index do |line_content, index|
68
- line_number = top+index+1
69
- if line_number == line
70
- printf("%#{spacing}d %s\n".colorize(:red), line_number, line_content)
71
- else
72
- printf("%#{spacing}d %s\n", line_number, line_content)
73
- end
74
- end
75
- exit 1 unless ENV['TEST']
76
- end
77
- end
78
-
79
- def user_data(path, vars={})
80
- path = "#{@options[:project_root]}/templates/user_data/#{path}"
81
- template = IO.read(path)
82
- variables(vars)
83
- result = erb_result(path, template)
84
- output = []
85
- result.split("\n").each do |line|
86
- output += transform(line)
87
- end
88
- json = output.to_json
89
- json[0] = '' # remove first char: [
90
- json.chop! # remove last char: ]
91
- end
92
-
93
- def ref(name)
94
- %Q|{"Ref"=>"#{name}"}|
95
- end
96
-
97
- def find_in_map(*args)
98
- %Q|{"Fn::FindInMap" => [ #{transform_array(args)} ]}|
99
- end
100
-
101
- def base64(value)
102
- %Q|{"Fn::Base64"=>"#{value}"}|
103
- end
104
-
105
- def get_att(*args)
106
- %Q|{"Fn::GetAtt" => [ #{transform_array(args)} ]}|
107
- end
108
-
109
- def get_azs(region="AWS::Region")
110
- %Q|{"Fn::GetAZs"=>"#{region}"}|
111
- end
112
-
113
- def join(delimiter, values)
114
- %Q|{"Fn::Join" => ["#{delimiter}", [ #{transform_array(values)} ]]}|
115
- end
116
-
117
- def select(index, list)
118
- %Q|{"Fn::Select" => ["#{index}", [ #{transform_array(list)} ]]}|
119
- end
120
-
121
- def transform_array(arr)
122
- arr.map! {|x| x =~ /=>/ ? x : x.inspect }
123
- arr.join(',')
124
- end
125
-
126
- # transform each line of bash script to array with cloudformation template objects
127
- def transform(data)
128
- data = evaluate(data)
129
- if data[-1].is_a?(String)
130
- data[0..-2] + ["#{data[-1]}\n"]
131
- else
132
- data + ["\n"]
133
- end
134
- end
135
-
136
- # Input:
137
- # String
138
- # Output:
139
- # Array of parse positions
140
- #
141
- # The positions of tokens taking into account when brackets start and close,
142
- # handles nested brackets.
143
- def bracket_positions(line)
144
- positions,pair,count = [],[],0
145
-
146
- line.split('').each_with_index do |char,i|
147
- pair << i if pair.empty?
148
-
149
- first_pair_char = line[pair[0]]
150
- if first_pair_char == '{' # object logic
151
- if char == '{'
152
- count += 1
153
- end
154
-
155
- if char == '}'
156
- count -= 1
157
- if count == 0
158
- pair << i
159
- positions << pair
160
- pair = []
161
- end
162
- end
163
- else # string logic
164
- lookahead = line[i+1]
165
- if lookahead == '{'
166
- pair << i
167
- positions << pair
168
- pair = []
169
- end
170
- end
171
- end # end of loop
172
-
173
- # for string logic when lookahead does not contain a object token
174
- # need to clear out what's left to match the final pair
175
- if !pair.empty?
176
- pair << line.size - 1
177
- positions << pair
178
- end
179
-
180
- positions
181
- end
182
-
183
- # Input:
184
- # Array - bracket_positions
185
- # Ouput:
186
- # Array - positions that can be use to determine what to parse
187
- def parse_positions(line)
188
- positions = bracket_positions(line)
189
- positions.flatten
190
- end
191
-
192
- # Input
193
- # String line of code to decompose into chunks, some can be transformed into objects
194
- # Output
195
- # Array of strings, some can be transformed into objects
196
- #
197
- # Example:
198
- # line = 'a{b}c{d{d}d}e' # nested brackets
199
- # template.decompose(line).should == ['a','{b}','c','{d{d}d}','e']
200
- def decompose(line)
201
- positions = parse_positions(line)
202
- return [line] if positions.empty?
203
-
204
- result = []
205
- str = ''
206
- until positions.empty?
207
- left = positions.shift
208
- right = positions.shift
209
- token = line[left..right]
210
- # if cfn object, add to the result set but after clearing out
211
- # the temp str that is being built up when the token is just a string
212
- if cfn_object?(token)
213
- unless str.empty? # first token might be a object
214
- result << str
215
- str = ''
216
- end
217
- result << token
218
- else
219
- str << token # keeps building up the string
220
- end
221
- end
222
-
223
- # at the of the loop there's a leftover string, unless the last token
224
- # is an object
225
- result << str unless str.empty?
226
-
227
- result
228
- end
229
-
230
- def cfn_object?(s)
231
- exact = %w[Ref]
232
- pattern = %w[Fn::]
233
- exact_match = !!exact.detect {|word| s.include?(word)}
234
- pattern_match = !!pattern.detect {|p| s =~ Regexp.new(p)}
235
- (exact_match || pattern_match) && s =~ /^{/ && s =~ /=>/
236
- end
237
-
238
- def recompose(decomposition)
239
- decomposition.map { |s| cfn_object?(s) ? eval(s) : s }
240
- end
241
-
242
- def evaluate(line)
243
- recompose(decompose(line))
244
- end
245
-
246
- # For simple just parameters files that can also be generated with lono, the CFN
247
- # Fn::Base64 function is not available and as lono is not being used in the context
248
- # of CloudFormation. So this can be used in it's place.
249
- def encode_base64(text)
250
- Base64.strict_encode64(text).strip
5
+ class Template < Command
6
+ autoload :Help, 'lono/template/help'
7
+ autoload :Bashify, 'lono/template/bashify'
8
+ autoload :DSL, 'lono/template/dsl'
9
+ autoload :Template, 'lono/template/template'
10
+
11
+ desc "generate", "Generate the CloudFormation templates"
12
+ Help.generate
13
+ option :clean, type: :boolean, aliases: "-c", desc: "remove all output files before generating"
14
+ option :project_root, default: ".", aliases: "-r", desc: "project root"
15
+ option :quiet, type: :boolean, aliases: "-q", desc: "silence the output"
16
+ option :pretty, type: :boolean, default: true, desc: "json pretty the output. only applies with json format"
17
+ def generate
18
+ DSL.new(options.clone).run
19
+ end
20
+
21
+ desc "bashify [URL-OR-PATH]", "Convert the UserData section of an existing CloudFormation Template to a starter bash script that is compatiable with lono"
22
+ Help.bashify
23
+ def bashify(path)
24
+ Bashify.new(path: path).run
251
25
  end
252
26
  end
253
27
  end