heroku-rails-saas 0.1.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.
@@ -0,0 +1,19 @@
1
+ module Heroku
2
+ module Generators
3
+ class ConfigGenerator < ::Rails::Generators::Base
4
+ desc "Generates a Heroku Config file at config/heroku.yml"
5
+
6
+ def self.source_root
7
+ @_heroku_gen_source_root ||= File.expand_path("../../templates", __FILE__)
8
+ end
9
+
10
+ def create_config_file
11
+ template 'heroku.yml', File.join('config', "heroku.yml")
12
+ end
13
+
14
+ def create_rake_file
15
+ template 'heroku.rake', File.join('lib', 'tasks', "heroku.rake")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ # ### Shortcuts: uncomment these for easier to type deployments
2
+ # ### e.g. rake deploy (instead of rake heroku:deploy)
3
+ # ###
4
+ # task :deploy => ["heroku:deploy"]
5
+ # task :console => ["heroku:console"]
6
+ # task :setup => ["heroku:setup"]
7
+ # task :logs => ["heroku:logs"]
8
+ # task :restart => ["heroku:restart"]
9
+
10
+ # Heroku Deploy Callbacks
11
+ namespace :heroku do
12
+
13
+ # runs before all the deploys complete
14
+ task :before_deploy do
15
+
16
+ end
17
+
18
+ # runs before each push to a particular heroku deploy environment
19
+ task :before_each_deploy, [:app_name] do |t,args|
20
+
21
+ end
22
+
23
+ # runs after each push to a particular heroku deploy environment
24
+ task :after_each_deploy, [:app_name] do |t,args|
25
+
26
+ end
27
+
28
+ # runs after all the deploys complete
29
+ task :after_deploy do
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,52 @@
1
+ # Configuration settings for specific apps and thier environments should be located here
2
+ # 'config/heroku/' and named appropiately they will be merged with the rest of the
3
+ # configuration settings
4
+
5
+ # The following is an example config file 'config/heroku/awesomeapp.yml'
6
+ # which map the apps and associated environments to your desired heroku app names
7
+ # for example,
8
+ # awesomeapp:staging
9
+ # would create the RACK_ENV=staging
10
+ # and deploy to http://awesomeapp-staging.heroku.com
11
+
12
+ # apps:
13
+ # production: awesomeapp
14
+ # staging: awesomeapp-staging
15
+ # legacy: awesomeapp-legacy
16
+
17
+ # stacks:
18
+ # bamboo-mri-1.9.2
19
+
20
+ # production:
21
+ # CONFIG_VAR1: "config1-production"
22
+
23
+ # collaborators
24
+ # - "awesomeapp@somedomain.com"
25
+
26
+ # domains:
27
+ # production:
28
+ # - "awesomeapp.com"
29
+ # - "www.awesomeapp.com"
30
+
31
+ # production:
32
+ # - ssl:piggyback
33
+ # - cron:daily
34
+ # - newrelic:bronze
35
+
36
+
37
+ # The following are configuration settings formally under the :all key
38
+ # for all apps and thier environments
39
+ config:
40
+ BUNDLE_WITHOUT: "test:development"
41
+ CONFIG_VAR1: "config1"
42
+ CONFIG_VAR2: "config2"
43
+
44
+ # Be sure to add yourself as a collaborator, otherwise your
45
+ # access to the app will be revoked.
46
+ collaborators:
47
+ - "my-heroku-email@somedomain.com"
48
+ - "another-heroku-email@somedomain.com"
49
+
50
+ addons:
51
+ - scheduler:standard
52
+ # add any other addons here
@@ -0,0 +1,4 @@
1
+ require 'heroku-rails/config'
2
+ require 'heroku-rails/runner'
3
+ require 'heroku-rails/railtie' if defined?(::Rails::Railtie)
4
+ require 'heroku-rails/hash_recursive_merge'
@@ -0,0 +1,146 @@
1
+ require 'erb'
2
+
3
+ module HerokuRails
4
+ class Config
5
+
6
+ SEPERATOR = ":"
7
+
8
+ class << self
9
+ def root
10
+ @heroku_rails_root || ENV["RAILS_ROOT"] || "."
11
+ end
12
+
13
+ def root=(root)
14
+ @heroku_rails_root = root
15
+ end
16
+
17
+ def app_name(app, env)
18
+ "#{app}#{SEPERATOR}#{env}"
19
+ end
20
+
21
+ def extract_environment_from(app_env)
22
+ name, env = app_env.split(SEPERATOR)
23
+ env
24
+ end
25
+ end
26
+
27
+ attr_accessor :settings
28
+
29
+ def initialize(config_files)
30
+ self.settings = aggregate_heroku_configs(config_files)
31
+ end
32
+
33
+ def apps
34
+ self.settings['apps'] || []
35
+ end
36
+
37
+ def app_names
38
+ apps.keys
39
+ end
40
+
41
+ # Returns the app name on heroku froma string format like so: `app:env`
42
+ # Allows for `rake <app:env> [<app:env>] <command>`
43
+ def app_name_on_heroku(string)
44
+ app_name, env = string.split(SEPERATOR)
45
+ apps[app_name][env]
46
+ end
47
+
48
+ # return all enviromnets in this format app:env
49
+ def app_environments(env_filter="")
50
+ apps.each_with_object([]) do |(app, hsh), arr|
51
+ hsh.each { |env, app_name| arr << self.class.app_name(app, env) if (env_filter.nil? || env_filter.empty?) || env == env_filter }
52
+ end
53
+ end
54
+
55
+ # return all environments e.g. staging, production, development
56
+ def all_environments
57
+ environments = apps.each_with_object([]) do |(app, hsh), arr|
58
+ hsh.each { |env, app_name| arr << env }
59
+ end
60
+ environments.uniq
61
+ end
62
+
63
+ # return the stack setting for a particular app environment
64
+ def stack(app_env)
65
+ name, env = app_env.split(SEPERATOR)
66
+ stacks = self.settings['stacks'] || {}
67
+ (stacks[name] && stacks[name][env]) || stacks['all']
68
+ end
69
+
70
+ def cmd(app_env)
71
+ if self.stack(app_env) =~ /cedar/i
72
+ 'heroku run '
73
+ else
74
+ 'heroku '
75
+ end
76
+ end
77
+
78
+ # pull out the config setting hash for a particular app environment
79
+ def config(app_env)
80
+ name, env = app_env.split(SEPERATOR)
81
+ config = self.settings['config'] || {}
82
+ all = config['all'] || {}
83
+
84
+ app_configs = (config[name] && config[name].reject { |k,v| v.class == Hash }) || {}
85
+ # overwrite app configs with the environment specific ones
86
+ merged_environment_configs = app_configs.merge((config[name] && config[name][env]) || {})
87
+
88
+ # overwrite all configs with the environment specific ones
89
+ all.merge(merged_environment_configs)
90
+ end
91
+
92
+ # return a list of domains for a particular app environment
93
+ def domains(app_env)
94
+ name, env = app_env.split(SEPERATOR)
95
+ domains = self.settings['domains'] || {}
96
+ (domains[name] && domains[name][env]) || []
97
+ end
98
+ # return a list of collaborators for a particular app environment
99
+ def collaborators(app_env)
100
+ app_setting_list('collaborators', app_env)
101
+ end
102
+
103
+ # return a list of addons for a particular app environment
104
+ def addons(app_env)
105
+ app_setting_list('addons', app_env)
106
+ end
107
+
108
+ protected
109
+
110
+ def app_setting_list(setting_key, app_env)
111
+ name, env = app_env.split(SEPERATOR)
112
+ setting = self.settings[setting_key] || {}
113
+ all = setting['all'] || []
114
+
115
+ # add in collaborators from app environment to the ones defined in all
116
+ (all + ((setting[name] && setting[name][env]) || [])).uniq
117
+ end
118
+
119
+ private
120
+
121
+ def parse_yml(config_filepath, options)
122
+ if File.exists?(config_filepath)
123
+ config_hash = YAML.load(ERB.new(File.read(config_filepath)).result)
124
+ config_hash = add_all_namespace(config_hash) if options == :default
125
+ config_hash = add_app_namespace(File.basename(config_filepath, ".yml"), config_hash) if options == :apps
126
+ config_hash
127
+ end
128
+ end
129
+
130
+ def add_all_namespace(hsh)
131
+ hsh.each_with_object({}) { |(k,v), h| h[k] = Hash["all" => v] }
132
+ end
133
+
134
+ def add_app_namespace(app_name, hsh)
135
+ hsh["apps"] = hsh.delete("env") if hsh.has_key?("env")
136
+ hsh.each_with_object({}) { |(k,v), h| h[k] = Hash[app_name => v] }
137
+ end
138
+
139
+ def aggregate_heroku_configs(config_files)
140
+ hsh = {}
141
+ config_files[:apps].each_with_object(hsh) { |file, h| h.rmerge!(parse_yml(file, :apps)) }
142
+ # overwrite all configs with the environment specific ones
143
+ hsh.rmerge!(parse_yml(config_files[:default], :default))
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,11 @@
1
+ module HashRecursiveMerge
2
+ def rmerge!(other_hash)
3
+ merge!(other_hash) do |key, old_value, new_value|
4
+ old_value.class == self.class ? old_value.rmerge!(new_value) : new_value
5
+ end
6
+ end
7
+ end
8
+
9
+ class Hash
10
+ include HashRecursiveMerge
11
+ end
@@ -0,0 +1,8 @@
1
+ module HerokuRails
2
+ class Railtie < ::Rails::Railtie
3
+ rake_tasks do
4
+ HerokuRails::Config.root = ::Rails.root
5
+ load 'heroku/rails/tasks.rb'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,278 @@
1
+ require 'heroku/client'
2
+
3
+ module HerokuRails
4
+ class Runner
5
+ def initialize(config)
6
+ @config = config
7
+ @environments = []
8
+ end
9
+
10
+ def authorize
11
+ @heroku ||= Heroku::Client.new(*Heroku::Auth.get_credentials)
12
+ end
13
+
14
+ # add a specific environment to the run list
15
+ def add_environment(env)
16
+ @environments << env
17
+ end
18
+
19
+ # use all environments or filter out production environments
20
+ def all_environments(filter=false)
21
+ @environments = @config.app_environments
22
+ filter ? @environments.reject! { |app| app[production_regex] } : @environments
23
+ end
24
+
25
+ # use all heroku apps filtered by environments
26
+ def environments(env)
27
+ @environments = @config.app_environments(env)
28
+ end
29
+
30
+ # setup apps (create if necessary)
31
+ def setup_apps
32
+ authorize unless @heroku
33
+
34
+ # get a list of all my current apps on Heroku (so we don't create dupes)
35
+ @my_apps = @heroku.list.map{|a| a.first}
36
+
37
+ each_heroku_app do |heroku_env, app_name, repo|
38
+ next if @my_apps.include?(app_name)
39
+
40
+ stack = @config.stack(heroku_env)
41
+ stack_option = " --stack #{stack}" if stack.to_s.size > 0
42
+ creation_command "heroku create #{app_name}#{stack_option} --remote #{app_name}"
43
+ end
44
+ end
45
+
46
+ # setup the stacks for each app (migrating if necessary)
47
+ def setup_stacks
48
+ authorize unless @heroku
49
+ each_heroku_app do |heroku_env, app_name, repo|
50
+ # get the intended stack setting
51
+ stack = @config.stack(heroku_env)
52
+
53
+ # get the remote info about the app from heroku
54
+ heroku_app_info = @heroku.info(app_name) || {}
55
+
56
+ # if the stacks don't match, then perform a migration
57
+ if stack != heroku_app_info[:stack]
58
+ puts "Migrating the app: #{app_name} to the stack: #{stack}"
59
+ creation_command "heroku stack:migrate #{stack} --app #{app_name}"
60
+ end
61
+ end
62
+ end
63
+
64
+ # setup the list of collaborators
65
+ def setup_collaborators
66
+ authorize unless @heroku
67
+ each_heroku_app do |heroku_env, app_name, repo|
68
+ # get the remote info about the app from heroku
69
+ heroku_app_info = @heroku.info(app_name) || {}
70
+
71
+ # get the intended list of collaborators to add
72
+ collaborator_emails = @config.collaborators(heroku_env)
73
+
74
+ # add current user to collaborator list (always)
75
+ collaborator_emails << @heroku.user unless collaborator_emails.include?(@heroku.user)
76
+ collaborator_emails << heroku_app_info[:owner] unless collaborator_emails.include?(heroku_app_info[:owner])
77
+
78
+ # get existing collaborators
79
+ existing_emails = heroku_app_info[:collaborators].to_a.map{|c| c[:email]}
80
+
81
+ # get the list of collaborators to delete
82
+ existing_emails.each do |existing_email|
83
+ # check to see if we need to delete this person
84
+ unless collaborator_emails.include?(existing_email)
85
+ # delete that collaborator if they arent on the approved list
86
+ destroy_command "heroku sharing:remove #{existing_email} --app #{app_name}"
87
+ end
88
+ end
89
+
90
+ # get the list of collaborators to add
91
+ collaborator_emails.each do |collaborator_email|
92
+ # check to see if we need to add this person
93
+ unless existing_emails.include?(collaborator_email)
94
+ # add the collaborator if they are not already on the server
95
+ creation_command "heroku sharing:add #{collaborator_email} --app #{app_name}"
96
+ end
97
+ end
98
+
99
+ # display the destructive commands
100
+ output_destroy_commands(app_name)
101
+ end
102
+ end
103
+
104
+ # setup configuration
105
+ def setup_config
106
+ authorize unless @heroku
107
+ each_heroku_app do |app_env, app_name, repo|
108
+ # get the configuration that we are aiming towards
109
+ new_config = @config.config(app_env)
110
+
111
+ # default RACK_ENV and RAILS_ENV to the heroku_env (unless its manually set to something else)
112
+ new_config["RACK_ENV"] = HerokuRails::Config.extract_environment_from(app_env) unless new_config["RACK_ENV"]
113
+ new_config["RAILS_ENV"] = HerokuRails::Config.extract_environment_from(app_env) unless new_config["RAILS_ENV"]
114
+ # get the existing config from heroku's servers
115
+ existing_config = @heroku.config_vars(app_name) || {}
116
+
117
+ # find the config variables to add
118
+ add_config = {}
119
+ new_config.each do |new_key, new_val|
120
+ add_config[new_key] = new_val unless existing_config[new_key] == new_val
121
+ end
122
+
123
+ # persist the changes onto heroku
124
+ unless add_config.empty?
125
+ # add the config
126
+ set_config = ""
127
+ add_config.each do |key, val|
128
+ set_config << "#{key}='#{val}' "
129
+ end
130
+ creation_command "heroku config:add #{set_config} --app #{app_name}"
131
+ system_with_echo("#{@config.cmd(app_env)} rails runner 'Rails.cache.clear' --app #{app_name}")
132
+ end
133
+ end
134
+ end
135
+
136
+ # setup the addons for heroku
137
+ def setup_addons
138
+ authorize unless @heroku
139
+ each_heroku_app do |heroku_env, app_name, repo|
140
+ # get the addons that we are aiming towards
141
+ addons = @config.addons(heroku_env)
142
+
143
+ # get the addons that are already on the servers
144
+ existing_addons = (@heroku.installed_addons(app_name) || []).map{|a| a["name"]}
145
+
146
+ # all apps need the shared database
147
+ addons << "shared-database:5mb" unless addons.any? {|x| x[/heroku-postgresql|shared-database|heroku-shared-postgresql|amazon_rds/]}
148
+
149
+ # remove the addons that need to be removed
150
+ existing_addons.each do |existing_addon|
151
+ # check to see if we need to delete this addon
152
+ unless addons.include?(existing_addon)
153
+ # delete this addon if they arent on the approved list
154
+ destroy_command "heroku addons:remove #{existing_addon} --app #{app_name} --confirm #{app_name}"
155
+ end
156
+ end
157
+
158
+ # add the addons that dont exist already
159
+ addons.each do |addon|
160
+ # check to see if we need to add this addon
161
+ unless existing_addons.include?(addon)
162
+ # add this addon if they are not already added
163
+ creation_command "heroku addons:add #{addon} --app #{app_name}"
164
+ end
165
+ end
166
+
167
+ # display the destructive commands
168
+ output_destroy_commands(app_name)
169
+ end
170
+ end
171
+
172
+ # setup the domains for heroku
173
+ def setup_domains
174
+ authorize unless @heroku
175
+ each_heroku_app do |heroku_env, app_name, repo|
176
+ # get the domains that we are aiming towards
177
+ domains = @config.domains(heroku_env)
178
+
179
+ # get the domains that are already on the servers
180
+ existing_domains = (@heroku.list_domains(app_name) || []).map{|a| a[:domain]}
181
+
182
+ # remove the domains that need to be removed
183
+ existing_domains.each do |existing_domain|
184
+ # check to see if we need to delete this domain
185
+ unless domains.include?(existing_domain)
186
+ # delete this domain if they arent on the approved list
187
+ destroy_command "heroku domains:remove #{existing_domain} --app #{app_name}"
188
+ end
189
+ end
190
+
191
+ # add the domains that dont exist already
192
+ domains.each do |domain|
193
+ # check to see if we need to add this domain
194
+ unless existing_domains.include?(domain)
195
+ # add this domain if they are not already added
196
+ creation_command "heroku domains:add #{domain} --app #{app_name}"
197
+ end
198
+ end
199
+
200
+ # display the destructive commands
201
+ output_destroy_commands(app_name)
202
+ end
203
+ end
204
+
205
+ # cycles through each configured heroku app
206
+ # yields the environment name, the app name, and the repo url
207
+ def each_heroku_app
208
+
209
+ if @config.apps.size == 0
210
+ puts "\nNo heroku apps are configured. Run:
211
+ rails generate heroku:config\n\n"
212
+ puts "this will generate a default config/heroku.yml that you should edit"
213
+ puts "and then try running this command again"
214
+
215
+ exit(1)
216
+ end
217
+
218
+ if (@environments.nil? || @environments.empty?) && @config.apps.size == 1
219
+ @environments = [all_environments(true).try(:first)].compact
220
+ end
221
+
222
+ if @environments.present?
223
+ @environments.each do |env|
224
+ app_name = @config.app_name_on_heroku(env)
225
+ yield(env, app_name, "git@heroku.com:#{app_name}.git")
226
+ end
227
+ else
228
+ puts "\nYou must first specify at least one Heroku app:
229
+ rake <app>:<environment> [<app>:<environment>] <command>
230
+ rake awesomeapp:production restart
231
+ rake demo:staging deploy"
232
+
233
+ puts "\n\nYou can use also command all Heroku apps(except production environments) for this project:
234
+ rake all heroku:setup\n"
235
+
236
+ exit(1)
237
+ end
238
+ end
239
+
240
+ def system_with_echo(*args)
241
+ puts args.join(' ')
242
+ command(*args)
243
+ end
244
+
245
+ def creation_command(*args)
246
+ system_with_echo(*args)
247
+ end
248
+
249
+ def destroy_command(*args)
250
+ # puts args.join(' ')
251
+ @destroy_commands ||= []
252
+ @destroy_commands << args.join(' ')
253
+ end
254
+
255
+ def output_destroy_commands(app)
256
+ if @destroy_commands.try(:any?)
257
+ puts "The #{app} had a few things removed from the heroku.yml."
258
+ puts "If they are no longer neccessary, then run the following commands:\n\n"
259
+ @destroy_commands.each do |destroy_command|
260
+ puts destroy_command
261
+ end
262
+ puts "\n\nthese commands may cause data loss so make sure you know that these are necessary"
263
+ end
264
+ # clear destroy commands
265
+ @destroy_commands = []
266
+ end
267
+
268
+ def command(*args)
269
+ raise "*** command \"#{args.join ' '}\" failed" unless system(*args)
270
+ end
271
+
272
+ private
273
+
274
+ def production_regex
275
+ Regexp.new("#{@config.class::SEPERATOR}(production|prod|live)")
276
+ end
277
+ end
278
+ end