lono 2.1.0 → 3.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +16 -284
  4. data/lib/lono.rb +6 -0
  5. data/lib/lono/cfn.rb +5 -0
  6. data/lib/lono/cfn/base.rb +86 -9
  7. data/lib/lono/cfn/create.rb +27 -2
  8. data/lib/lono/cfn/help.rb +12 -12
  9. data/lib/lono/cfn/preview.rb +12 -11
  10. data/lib/lono/cfn/update.rb +3 -2
  11. data/lib/lono/default/settings.yml +13 -0
  12. data/lib/lono/env.rb +11 -0
  13. data/lib/lono/param.rb +1 -1
  14. data/lib/lono/param/generator.rb +144 -22
  15. data/lib/lono/settings.rb +28 -0
  16. data/lib/lono/template.rb +13 -2
  17. data/lib/lono/template/aws_services.rb +7 -0
  18. data/lib/lono/template/dsl.rb +24 -33
  19. data/lib/lono/template/helpers.rb +144 -0
  20. data/lib/lono/template/template.rb +69 -68
  21. data/lib/lono/template/upload.rb +75 -0
  22. data/lib/lono/version.rb +1 -1
  23. data/lib/starter_projects/json_project/config/{lono.rb → templates/base/blog.rb} +3 -3
  24. data/lib/starter_projects/{yaml_project/config/lono/api.rb → json_project/config/templates/base/stacks.rb} +9 -9
  25. data/lib/starter_projects/json_project/templates/{db.json.erb → db.json} +1 -1
  26. data/lib/starter_projects/json_project/templates/partial/{host_record.json.erb → host_record.json} +0 -0
  27. data/lib/starter_projects/json_project/templates/partial/{server.json.erb → server.json} +2 -2
  28. data/lib/starter_projects/json_project/templates/user_data/{app.sh.erb → app.sh} +0 -0
  29. data/lib/starter_projects/json_project/templates/user_data/{db.sh.erb → db.sh} +0 -0
  30. data/lib/starter_projects/json_project/templates/user_data/{db2.sh.erb → db2.sh} +0 -0
  31. data/lib/starter_projects/json_project/templates/user_data/{ruby_script.rb.erb → ruby_script.rb} +0 -0
  32. data/lib/starter_projects/json_project/templates/{web.json.erb → web.json} +2 -2
  33. data/lib/starter_projects/yaml_project/config/{lono.rb → templates/base/blog.rb} +4 -8
  34. data/lib/starter_projects/{json_project/config/lono/api.rb → yaml_project/config/templates/base/stacks.rb} +11 -13
  35. data/lib/starter_projects/yaml_project/config/templates/prod/stacks.rb +1 -0
  36. data/lib/starter_projects/yaml_project/config/templates/stag/stacks.rb +1 -0
  37. data/lib/starter_projects/yaml_project/config/variables/base/variables.rb +4 -0
  38. data/lib/starter_projects/yaml_project/config/variables/prod/variables.rb +1 -0
  39. data/lib/starter_projects/yaml_project/config/variables/stag/variables.rb +1 -0
  40. data/lib/starter_projects/yaml_project/helpers/my_custom_helper.rb +17 -0
  41. data/lib/starter_projects/yaml_project/params/{api-web-prod.txt → base/api-web-prod.txt} +0 -0
  42. data/lib/starter_projects/yaml_project/params/{example.txt → base/example.txt} +0 -0
  43. data/lib/starter_projects/yaml_project/params/prod/example.txt +1 -0
  44. data/lib/starter_projects/yaml_project/params/stag/example.txt +1 -0
  45. data/lib/starter_projects/yaml_project/templates/{db.yml.erb → db.yml} +1 -1
  46. data/lib/starter_projects/yaml_project/templates/{example.yml.erb → example.yml} +0 -0
  47. data/lib/starter_projects/yaml_project/templates/partial/{host_record.yml.erb → host_record.yml} +0 -0
  48. data/lib/starter_projects/yaml_project/templates/partial/{server.yml.erb → server.yml} +0 -0
  49. data/lib/starter_projects/yaml_project/templates/partial/user_data/{bootstrap.sh.erb → bootstrap.sh} +0 -0
  50. data/lib/starter_projects/yaml_project/templates/{web.yml.erb → web.yml} +2 -2
  51. data/lono.gemspec +1 -0
  52. data/spec/fixtures/params/baseonly/params/base/network.txt +1 -0
  53. data/spec/fixtures/params/envonly/params/prod/network.txt +1 -0
  54. data/spec/fixtures/params/overlay/params/base/network.txt +1 -0
  55. data/spec/fixtures/params/overlay/params/prod/network.txt +1 -0
  56. data/spec/lib/lono/new_spec.rb +1 -1
  57. data/spec/lib/lono/param/generator_spec.rb +34 -0
  58. data/spec/lib/lono/template/dsl_spec.rb +1 -1
  59. data/spec/lib/lono/template_spec.rb +5 -0
  60. metadata +60 -22
@@ -0,0 +1,7 @@
1
+ require "aws-sdk"
2
+
3
+ module Lono::Template::AwsServices
4
+ def s3
5
+ @s3 ||= Aws::S3::Client.new
6
+ end
7
+ end
@@ -2,7 +2,7 @@ class Lono::Template::DSL
2
2
  def initialize(options={})
3
3
  @options = options
4
4
  @project_root = @options[:project_root] || '.'
5
- @path = "#{@project_root}/config/lono.rb"
5
+ @config_path = "#{@project_root}/config"
6
6
  Lono::ProjectChecker.check(@project_root)
7
7
  @templates = []
8
8
  @results = {}
@@ -15,12 +15,23 @@ class Lono::Template::DSL
15
15
  write_output
16
16
  end
17
17
 
18
+ # Instance eval's all the files within each folder under
19
+ # config/lono/base and config/lono/[LONO_ENV]
20
+ # Base gets base first and then the LONO_ENV configs get evaluate second.
21
+ # This means the env specific configs override the base configs.
18
22
  def evaluate_templates
19
- evaluate_template(@path)
20
- load_subfolder
23
+ evaluate_folder("base")
24
+ evaluate_folder(LONO_ENV)
21
25
  @detected_format = detect_format
22
26
  end
23
27
 
28
+ def evaluate_folder(folder)
29
+ paths = Dir.glob("#{@config_path}/templates/#{folder}/**/*")
30
+ paths.select{ |e| File.file?(e) }.each do |path|
31
+ evaluate_template(path)
32
+ end
33
+ end
34
+
24
35
  def evaluate_template(path)
25
36
  begin
26
37
  instance_eval(File.read(path), path)
@@ -55,35 +66,14 @@ class Lono::Template::DSL
55
66
  end
56
67
  end
57
68
 
58
- # Detects the format of the templates. Simply checks the extension of all the
69
+ # Detects the format of the templates. Checks the extension of all the
59
70
  # templates files.
60
71
  # All the templates must be of the same format, either all json or all yaml.
61
72
  def detect_format
62
- # @templates contains Array of Hashes. Example:
63
- # [{name: ""blog-web-prod.json", block: ...},
64
- # {name: ""api-web-prod.json", block: ...}]
65
- formats = @templates.map{ |t| File.extname(t[:name]) }.uniq
66
- if formats.size > 1
67
- puts "ERROR: Detected multiple formats: #{formats.join(", ")}".colorize(:red)
68
- puts "All the source values in the template blocks in the config folder must have the same format extension."
69
- exit 1
70
- else
71
- found_format = formats.first
72
- if found_format
73
- detected_format = found_format.sub(/^\./,'')
74
- detected_format = "yaml" if detected_format == "yml"
75
- else # empty templates, no templates defined yet
76
- detected_format = "yaml" # defaults to yaml
77
- end
78
- end
79
- detected_format
80
- end
81
-
82
- # load any templates defined in project/config/lono/*
83
- def load_subfolder
84
- Dir.glob("#{File.dirname(@path)}/lono/**/*").select{ |e| File.file? e }.each do |path|
85
- evaluate_template(path)
86
- end
73
+ extensions = Dir.glob("#{@project_root}/templates/**/*").map do |path|
74
+ File.extname(path).sub(/^\./,'')
75
+ end.reject(&:empty?).uniq
76
+ extensions.include?('yml') ? 'yml' : 'json' # defaults to yml - falls back to json
87
77
  end
88
78
 
89
79
  def template(name, &block)
@@ -91,8 +81,9 @@ class Lono::Template::DSL
91
81
  end
92
82
 
93
83
  def build_templates
84
+ options = @options.merge(detected_format: @detected_format)
94
85
  @templates.each do |t|
95
- @results[t[:name]] = Lono::Template::Template.new(t[:name], t[:block], @options).build
86
+ @results[t[:name]] = Lono::Template::Template.new(t[:name], t[:block], options).build
96
87
  end
97
88
  end
98
89
 
@@ -102,7 +93,8 @@ class Lono::Template::DSL
102
93
  FileUtils.mkdir(output_path) unless File.exist?(output_path)
103
94
  puts "Generating CloudFormation templates:" unless @options[:quiet]
104
95
  @results.each do |name,text|
105
- path = "#{output_path}/#{name}".sub(/^\.\//,'')
96
+ path = "#{output_path}/#{name}".sub(/^\.\//,'') # strip leading '.'
97
+ path += ".#{@detected_format}"
106
98
  puts " #{path}" unless @options[:quiet]
107
99
  ensure_parent_dir(path)
108
100
  validate(text, path)
@@ -112,7 +104,6 @@ class Lono::Template::DSL
112
104
  end
113
105
  end
114
106
 
115
- # TODO: set @detected_format upon DSL.new
116
107
  def validate(text, path)
117
108
  if @detected_format == "json"
118
109
  validate_json(text, path)
@@ -156,7 +147,7 @@ class Lono::Template::DSL
156
147
  def yaml_format(text)
157
148
  comment =<<~EOS
158
149
  # This file was generated with lono. Do not edit directly, the changes will be lost.
159
- # More info: https://github.com/tongueroo/lono
150
+ # More info: http://lono.cloud
160
151
  EOS
161
152
  "#{comment}#{remove_blank_lines(text)}"
162
153
  end
@@ -0,0 +1,144 @@
1
+ module Lono::Template::Helpers
2
+ def template_s3_path(template_name)
3
+ format = @_detected_format.sub('yaml','yml')
4
+ template_path = "#{template_name}.#{format}"
5
+
6
+ # must have settings.s3_path for this to owrk
7
+ settings = Lono::Settings.new(@project_root)
8
+ if settings.s3_path
9
+ # high jacking Upload for useful s3_https_url method
10
+ upload = Lono::Template::Upload.new(@_options)
11
+ upload.s3_https_url(template_path)
12
+ else
13
+ message = "template_s3_path helper called but s3.path not configured in lono/settings.yml"
14
+ puts "WARN: #{message}".colorize(:yellow)
15
+ message
16
+ end
17
+ end
18
+
19
+ def template_params(param_name)
20
+ param_path = "params/#{LONO_ENV}/#{param_name}.txt"
21
+ generator_options = {
22
+ project_root: @_project_root,
23
+ path: param_path,
24
+ allow_no_file: true
25
+ }.merge(@_options)
26
+ generator = Lono::Param::Generator.new(param_name, generator_options)
27
+ # do not generate because lono cfn calling logic already generated it we only need the values
28
+ generator.params # Returns Array in underscore keys format
29
+ end
30
+
31
+ def user_data(path, vars={})
32
+ path = "#{@_project_root}/templates/user_data/#{path}"
33
+ template = IO.read(path)
34
+ variables(vars)
35
+ result = erb_result(path, template)
36
+ output = []
37
+ result.split("\n").each do |line|
38
+ output += transform(line)
39
+ end
40
+ json = output.to_json
41
+ json[0] = '' # remove first char: [
42
+ json.chop! # remove last char: ]
43
+ end
44
+
45
+ def ref(name)
46
+ %Q|{"Ref"=>"#{name}"}|
47
+ end
48
+
49
+ def find_in_map(*args)
50
+ %Q|{"Fn::FindInMap" => [ #{transform_array(args)} ]}|
51
+ end
52
+
53
+ def base64(value)
54
+ %Q|{"Fn::Base64"=>"#{value}"}|
55
+ end
56
+
57
+ def get_att(*args)
58
+ %Q|{"Fn::GetAtt" => [ #{transform_array(args)} ]}|
59
+ end
60
+
61
+ def get_azs(region="AWS::Region")
62
+ %Q|{"Fn::GetAZs"=>"#{region}"}|
63
+ end
64
+
65
+ def join(delimiter, values)
66
+ %Q|{"Fn::Join" => ["#{delimiter}", [ #{transform_array(values)} ]]}|
67
+ end
68
+
69
+ def select(index, list)
70
+ %Q|{"Fn::Select" => ["#{index}", [ #{transform_array(list)} ]]}|
71
+ end
72
+
73
+ def partial_exist?(path)
74
+ path = partial_path_for(path)
75
+ path = auto_add_format(path)
76
+ path && File.exist?(path)
77
+ end
78
+
79
+ # The partial's path is a relative path given without the extension and
80
+ #
81
+ # Example:
82
+ # Given: file in templates/partial/iam/docker.yml
83
+ # The path should be: iam/docker
84
+ #
85
+ # If the user specifies the extension then use that instead of auto-adding
86
+ # the detected format.
87
+ def partial(path,vars={}, options={})
88
+ path = partial_path_for(path)
89
+ path = auto_add_format(path)
90
+
91
+ template = IO.read(path)
92
+ variables(vars)
93
+ result = erb_result(path, template)
94
+ result = indent(result, options[:indent]) if options[:indent]
95
+ if options[:indent]
96
+ # Add empty line at beginning because empty lines gets stripped during
97
+ # processing anyway. This allows the user to call partial without having
98
+ # to put the partial call at very beginning of the line.
99
+ # This only should happen if user is using indent option.
100
+ ["\n", result].join("\n")
101
+ else
102
+ result
103
+ end
104
+ end
105
+
106
+ # add indentation
107
+ def indent(text, indentation_amount)
108
+ text.split("\n").map do |line|
109
+ " " * indentation_amount + line
110
+ end.join("\n")
111
+ end
112
+
113
+ private
114
+ def partial_path_for(path)
115
+ "#{@_project_root}/templates/partial/#{path}"
116
+ end
117
+
118
+ def auto_add_format(path)
119
+ # Return immediately if user provided explicit extension
120
+ extension = File.extname(path) # current extension
121
+ return path if !extension.empty?
122
+
123
+ # Else let's auto detect
124
+ paths = Dir.glob("#{path}.*")
125
+
126
+ if paths.size == 1 # non-ambiguous match
127
+ return paths.first
128
+ end
129
+
130
+ if paths.size > 1 # ambiguous match
131
+ puts "ERROR: Multiple possible partials found:".colorize(:red)
132
+ paths.each do |path|
133
+ puts " #{path}"
134
+ end
135
+ puts "Please specify an extension in the name to remove the ambiguity.".colorize(:green)
136
+ exit 1
137
+ end
138
+
139
+ # Account for case when user wants to include a file with no extension at all
140
+ return path if File.exist?(path) && !File.directory?(path)
141
+
142
+ path # original path if this point is reached
143
+ end
144
+ end
@@ -3,45 +3,83 @@ require 'json'
3
3
  require 'base64'
4
4
 
5
5
  class Lono::Template::Template
6
+ include Lono::Template::Helpers
6
7
  include ERB::Util
7
8
 
8
- attr_reader :name
9
- def initialize(name, block, options={})
10
- @name = name
11
- @block = block
12
- @options = options
9
+ def initialize(name, block=nil, options={})
10
+ # Taking care to name instance variables with _ in front because we load the
11
+ # variables from config/variables and those instance variables can clobber these
12
+ # instance variables
13
+ @_name = name
14
+ @_options = options
15
+ @_detected_format = options[:detected_format]
16
+ @_block = block
17
+ @_project_root = options[:project_root] || '.'
18
+ @_config_path = "#{@_project_root}/config"
19
+ @_source = default_source(name)
20
+ end
21
+
22
+ def default_source(name)
23
+ "#{@_project_root}/templates/#{name}.#{@_detected_format}" # defaults to name, source method overrides
13
24
  end
14
25
 
15
26
  def build
16
- instance_eval(&@block)
17
- template = IO.read(@source)
18
- erb_result(@source, template)
27
+ load_variables
28
+ load_custom_helpers
29
+ instance_eval(&@_block) if @_block
30
+ template = IO.read(@_source)
31
+ erb_result(@_source, template)
19
32
  end
20
33
 
21
- def source(path)
22
- @source = path[0..0] == '/' ? path : "#{@options[:project_root]}/templates/#{path}"
34
+ def load_variables
35
+ load_variables_folder("base")
36
+ load_variables_folder(LONO_ENV)
23
37
  end
24
38
 
25
- def variables(vars={})
26
- vars.each do |var,value|
27
- instance_variable_set("@#{var}", value)
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))
28
59
  end
29
60
  end
30
61
 
31
- def partial(path,vars={}, options={})
32
- path = "#{@options[:project_root]}/templates/partial/#{path}"
33
- template = IO.read(path)
34
- variables(vars)
35
- result = erb_result(path, template)
36
- result = indent(result, options[:indent]) if options[:indent]
37
- result
62
+ # Load custom helper methods from the user's infra repo
63
+ def load_custom_helpers
64
+ Dir.glob("#{@_project_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
71
+
38
72
  end
39
73
 
40
- # add indentation
41
- def indent(result, indentation_amount)
42
- result.split("\n").map do |line|
43
- " " * indentation_amount + line
44
- end.join("\n")
74
+ def source(path)
75
+ @_source = path[0..0] == '/' ? path : "#{@_project_root}/templates/#{path}"
76
+ @_source += ".#{@_detected_format}"
77
+ end
78
+
79
+ def variables(vars={})
80
+ vars.each do |var,value|
81
+ instance_variable_set("@#{var}", value)
82
+ end
45
83
  end
46
84
 
47
85
  def erb_result(path, template)
@@ -49,6 +87,7 @@ class Lono::Template::Template
49
87
  ERB.new(template, nil, "-").result(binding)
50
88
  rescue Exception => e
51
89
  puts e
90
+ puts e.backtrace if ENV['DEBUG']
52
91
 
53
92
  # how to know where ERB stopped? - https://www.ruby-forum.com/topic/182051
54
93
  # syntax errors have the (erb):xxx info in e.message
@@ -57,7 +96,7 @@ class Lono::Template::Template
57
96
  error_info ||= e.backtrace.grep(/\(erb\)/)[0]
58
97
  raise unless error_info # unable to find the (erb):xxx: error line
59
98
  line = error_info.split(':')[1].to_i
60
- puts "Error evaluating ERB template on line #{line.to_s.colorize(:red)} of: #{path.sub(/^\.\//, '')}"
99
+ puts "Error evaluating ERB template on line #{line.to_s.colorize(:red)} of: #{path.sub(/^\.\//, '').colorize(:green)}"
61
100
 
62
101
  template_lines = template.split("\n")
63
102
  context = 5 # lines of context
@@ -75,48 +114,6 @@ class Lono::Template::Template
75
114
  end
76
115
  end
77
116
 
78
- def user_data(path, vars={})
79
- path = "#{@options[:project_root]}/templates/user_data/#{path}"
80
- template = IO.read(path)
81
- variables(vars)
82
- result = erb_result(path, template)
83
- output = []
84
- result.split("\n").each do |line|
85
- output += transform(line)
86
- end
87
- json = output.to_json
88
- json[0] = '' # remove first char: [
89
- json.chop! # remove last char: ]
90
- end
91
-
92
- def ref(name)
93
- %Q|{"Ref"=>"#{name}"}|
94
- end
95
-
96
- def find_in_map(*args)
97
- %Q|{"Fn::FindInMap" => [ #{transform_array(args)} ]}|
98
- end
99
-
100
- def base64(value)
101
- %Q|{"Fn::Base64"=>"#{value}"}|
102
- end
103
-
104
- def get_att(*args)
105
- %Q|{"Fn::GetAtt" => [ #{transform_array(args)} ]}|
106
- end
107
-
108
- def get_azs(region="AWS::Region")
109
- %Q|{"Fn::GetAZs"=>"#{region}"}|
110
- end
111
-
112
- def join(delimiter, values)
113
- %Q|{"Fn::Join" => ["#{delimiter}", [ #{transform_array(values)} ]]}|
114
- end
115
-
116
- def select(index, list)
117
- %Q|{"Fn::Select" => ["#{index}", [ #{transform_array(list)} ]]}|
118
- end
119
-
120
117
  def transform_array(arr)
121
118
  arr.map! {|x| x =~ /=>/ ? x : x.inspect }
122
119
  arr.join(',')
@@ -248,4 +245,8 @@ class Lono::Template::Template
248
245
  def encode_base64(text)
249
246
  Base64.strict_encode64(text).strip
250
247
  end
248
+
249
+ def name
250
+ @_name
251
+ end
251
252
  end
@@ -0,0 +1,75 @@
1
+ require 'erb'
2
+ require 'json'
3
+ require 'base64'
4
+
5
+ class Lono::Template::Upload
6
+ include Lono::Template::AwsServices
7
+
8
+ def initialize(options={})
9
+ @options = options
10
+ @project_root = options[:project_root] || '.'
11
+ end
12
+
13
+ def run
14
+ ensure_s3_setup!
15
+ paths = Dir.glob("#{@project_root}/output/**/*")
16
+ paths.reject { |p| p =~ %r{output/params} }.
17
+ select { |p| File.file?(p) }.each do |path|
18
+ upload(path)
19
+ end
20
+ say "Templates uploaded to s3."
21
+ end
22
+
23
+ def upload(path)
24
+ pretty_path = path.sub(/^\.\//, '')
25
+ key = "#{s3_path}/#{LONO_ENV}/#{pretty_path.sub(/^output\//,'')}"
26
+ s3_full_path = "s3://#{s3_bucket}/#{key}"
27
+
28
+ resp = s3.put_object(
29
+ body: IO.read(path),
30
+ bucket: s3_bucket,
31
+ key: key,
32
+ storage_class: "REDUCED_REDUNDANCY"
33
+ ) unless @options[:noop]
34
+
35
+ message = "Uploaded: #{pretty_path} to #{s3_full_path}"
36
+ message = "NOOP: #{message}" if @options[:noop]
37
+ say message
38
+ end
39
+
40
+ # https://s3.amazonaws.com/mybucket/cloudformation-templates/prod/parent.yml
41
+ def s3_https_url(template_path)
42
+ ensure_s3_setup!
43
+ "https://s3.amazonaws.com/#{s3_bucket}/#{s3_path}/#{LONO_ENV}/#{template_path}"
44
+ end
45
+
46
+ # Example:
47
+ # s3_bucket('s3://mybucket/templates/storage/path') => mybucket
48
+ def s3_bucket
49
+ @s3_full_path.sub('s3://','').split('/').first
50
+ end
51
+
52
+ # Example:
53
+ # s3_bucket('s3://mybucket/templates/storage/path') => templates/storage/path
54
+ def s3_path
55
+ @s3_full_path.sub('s3://','').split('/')[1..-1].join('/')
56
+ end
57
+
58
+ # nice warning if the s3 path not found
59
+ def ensure_s3_setup!
60
+ return if @options[:noop]
61
+
62
+ settings = Lono::Settings.new(@project_root)
63
+ if settings.s3_path
64
+ @s3_full_path = settings.s3_path
65
+ else
66
+ say "Unable to upload templates to s3 because you have not configured the s3.path option in .lono/settings.yml.".colorize(:red)
67
+ say "Please configure .lono/settings.yml with s3.path. Refer to http://lono.cloud/docs/settings/ for more help.".colorize(:red)
68
+ exit 1
69
+ end
70
+ end
71
+
72
+ def say(message)
73
+ puts message unless @options[:quiet]
74
+ end
75
+ end