satorix 0.0.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -1
  3. data/.gitlab-ci.yml +45 -0
  4. data/.rspec +2 -1
  5. data/.rubocop.yml +11 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +2 -0
  8. data/Gemfile.lock +25 -0
  9. data/Procfile +1 -0
  10. data/README.md +93 -1
  11. data/Rakefile +8 -4
  12. data/bin/console +3 -3
  13. data/bin/satorix +8 -0
  14. data/lib/satorix.rb +338 -2
  15. data/lib/satorix/CI/deploy/flynn.rb +129 -0
  16. data/lib/satorix/CI/deploy/flynn/environment_variables.rb +121 -0
  17. data/lib/satorix/CI/deploy/flynn/resources.rb +77 -0
  18. data/lib/satorix/CI/deploy/flynn/routes.rb +266 -0
  19. data/lib/satorix/CI/deploy/flynn/scale.rb +52 -0
  20. data/lib/satorix/CI/shared/buildpack_manager.rb +213 -0
  21. data/lib/satorix/CI/shared/buildpack_manager/buildpack.rb +153 -0
  22. data/lib/satorix/CI/shared/ruby/gem_manager.rb +79 -0
  23. data/lib/satorix/CI/shared/yarn_manager.rb +22 -0
  24. data/lib/satorix/CI/test/python/django_test.rb +35 -0
  25. data/lib/satorix/CI/test/python/safety.rb +27 -0
  26. data/lib/satorix/CI/test/ruby/brakeman.rb +32 -0
  27. data/lib/satorix/CI/test/ruby/bundler_audit.rb +32 -0
  28. data/lib/satorix/CI/test/ruby/cucumber.rb +26 -0
  29. data/lib/satorix/CI/test/ruby/rails_test.rb +26 -0
  30. data/lib/satorix/CI/test/ruby/rspec.rb +26 -0
  31. data/lib/satorix/CI/test/ruby/rubocop.rb +98 -0
  32. data/lib/satorix/CI/test/shared/database.rb +74 -0
  33. data/lib/satorix/shared/console.rb +180 -0
  34. data/lib/satorix/version.rb +1 -1
  35. data/satorix.gemspec +13 -11
  36. data/satorix/CI/deploy/ie_gem_server.rb +76 -0
  37. data/satorix/CI/deploy/rubygems.rb +77 -0
  38. data/satorix/custom.rb +21 -0
  39. metadata +57 -29
  40. data/.travis.yml +0 -5
@@ -0,0 +1,129 @@
1
+ module Satorix
2
+ module CI
3
+ module Deploy
4
+ module Flynn
5
+
6
+ include Satorix
7
+ include Satorix::Shared::Console
8
+
9
+ require_relative 'flynn/environment_variables'
10
+ include EnvironmentVariables
11
+
12
+ require_relative 'flynn/resources'
13
+ include Resources
14
+
15
+ require_relative 'flynn/routes'
16
+ include Routes
17
+
18
+ require_relative 'flynn/scale'
19
+ include Scale
20
+
21
+ extend self
22
+
23
+
24
+ def go
25
+ log_bench('Adding a local reference to the remote Flynn cluster...') { add_cluster }
26
+ log_bench('Creating Flynn project...') { create_project }
27
+ log_bench('Adding Flynn remote to CI local git...') { add_remote }
28
+ log_bench('Adjusting Flynn environment variables...') { adjust_env_vars }
29
+ log_bench('Configuring Flynn inactive slug release count...') { configure_inactive_slug_releases }
30
+ log_bench('Setting resources...') { set_resources }
31
+ log_bench('Deploying to Flynn...') { deploy }
32
+ log_bench('Running Database migrations...') { run_database_migrations } if run_database_migrations?
33
+ log_bench('Scaling application processes...') { adjust_scale }
34
+ log_bench('Setting up routes...') { setup_routes }
35
+ end
36
+
37
+
38
+ def skip_buildpack
39
+ true
40
+ end
41
+
42
+
43
+ def add_cluster
44
+ run_command(['flynn', 'cluster', 'add', '--force', '--default', "--tls-pin=#{ tls_pin }", cluster_name, domain_for_cluster, key], filtered_text: [tls_pin, key])
45
+ end
46
+
47
+
48
+ def add_remote
49
+ run_command(['flynn', '-a', project_name, 'remote', 'add', cluster_name, '-y'])
50
+ end
51
+
52
+
53
+ def cluster_name
54
+ domain_for_cluster
55
+ end
56
+
57
+
58
+ def configure_inactive_slug_releases
59
+ run_command(%w[flynn meta set gc.max_inactive_slug_releases=2])
60
+ end
61
+
62
+
63
+ def create_project
64
+ if project_exists?
65
+ log("Skipping - Flynn project '#{ project_name }' already exists.")
66
+ else
67
+ run_command(['flynn', 'create', "--remote=#{ cluster_name }", '-y', 'project_name'])
68
+ end
69
+ end
70
+
71
+
72
+ def deploy
73
+ run_command(['git', 'push', cluster_name, 'HEAD:refs/heads/master'])
74
+ end
75
+
76
+
77
+ def domain_for_cluster
78
+ ENV["FLYNN_#{ current_branch }_DOMAIN"]
79
+ end
80
+
81
+
82
+ def domain_for_web_host
83
+ "#{ current_branch.downcase }.#{ hosting_namespace }"
84
+ end
85
+
86
+
87
+ def hosting_namespace
88
+ if ENV['SATORIX_HOSTING_NAMESPACE'].to_s !~ only_whitespace
89
+ ENV['SATORIX_HOSTING_NAMESPACE']
90
+ else
91
+ log_error_and_abort("Satorix configuration error: Missing SATORIX_HOSTING_NAMESPACE.\n\nPlease contact support.\n")
92
+ end
93
+
94
+ end
95
+
96
+
97
+ def key
98
+ ENV["FLYNN_#{ current_branch }_KEY"]
99
+ end
100
+
101
+
102
+ def project_exists?
103
+ run_command(%w[flynn apps], quiet: true).split(/\R/).map { |a| a.split.last }.include? project_name
104
+ end
105
+
106
+
107
+ def run_database_migrations?
108
+ [rails_app?, django_app?].any?
109
+ end
110
+
111
+
112
+ def run_database_migrations
113
+ if desired_resource_provider_names.nil? || desired_resource_provider_names.empty?
114
+ log 'Skipping migrations, no database has been defined. Please see the resources section of this log for more information.'
115
+ else
116
+ run_command(%w[flynn run --enable-log bundle exec rake db:migrate]) if rails_app?
117
+ run_command(%w[flynn run --enable-log python public/manage.py migrate]) if django_app?
118
+ end
119
+ end
120
+
121
+
122
+ def tls_pin
123
+ ENV["FLYNN_#{ current_branch }_TLSPIN"]
124
+ end
125
+
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,121 @@
1
+ require 'yaml'
2
+
3
+ module Satorix
4
+ module CI
5
+ module Deploy
6
+ module Flynn
7
+ module EnvironmentVariables
8
+
9
+ def adjust_env_vars
10
+ ensure_required_env_vars_are_defined unless ENV["SATORIX_#{ current_branch }_ENFORCE_ENV_VAR_DEFINITION"] == 'false'
11
+ env_unset
12
+ env_set
13
+ end
14
+
15
+
16
+ def aeev_key_exists?
17
+ !(run_command(%w[flynn env], quiet: true) =~ /#{ exported_aeevs_key }=/mi).nil?
18
+ end
19
+
20
+
21
+ def aeev_prefix
22
+ # AEEV - Application Exportable Environment Variable
23
+ "AEEV_#{ current_branch }_"
24
+ end
25
+
26
+
27
+ def ci_provided_env_vars
28
+ {}.tap do |vars|
29
+ ENV.each do |key, value|
30
+ vars[key.sub(aeev_prefix, '')] = value.sub('***EMPTY_STRING***', '') if key.start_with?(aeev_prefix)
31
+ end
32
+ end
33
+ end
34
+
35
+
36
+ def current_flynn_keys
37
+ aeev_key_exists? ? run_command(['flynn', 'env', 'get', exported_aeevs_key], quiet: true).split : []
38
+ end
39
+
40
+
41
+ def desired_env_vars
42
+ ci_provided_env_vars.tap do |vars|
43
+ vars[exported_aeevs_key] = (vars.keys << exported_aeevs_key).sort.join("\n")
44
+ end
45
+ end
46
+
47
+
48
+ def ensure_required_env_vars_are_defined(configuration_file: 'config/secrets.yml')
49
+ # TODO : handle new Rails secrets methods, like encrypted secrets
50
+ if File.file?(configuration_file)
51
+ secrets = YAML.load_file(configuration_file)
52
+
53
+ all_secrets = {}
54
+ all_secrets.merge!(secrets['shared']) if secrets['shared']
55
+ all_secrets.merge!(secrets['production']) if secrets['production']
56
+
57
+ required = all_secrets.to_yaml.scan(/ENV\[['"](?<var>[A-Z0-9_.]+)['"]\]/).flatten.uniq
58
+ set = ci_provided_env_vars.keys
59
+
60
+ missing = required - set
61
+
62
+ if missing.empty?
63
+ log "All required environment variables from #{ configuration_file } have been defined."
64
+ else
65
+ log_error "Environment variables specified in #{ configuration_file } were not defined for the #{ current_branch.downcase } branch."
66
+ log_error "\nPlease define the following variables in your dashboard:\n#{ missing.join("\n") }\n"
67
+ log_error_and_abort 'Missing required environment variables.'
68
+ end
69
+ else
70
+ log "No #{ configuration_file } exists, skipping environment variable enforcement."
71
+ end
72
+ end
73
+
74
+
75
+ def env_set
76
+ if env_vars_to_set.empty?
77
+ log 'No new environment variables to set.'
78
+ else
79
+ keys_and_values = env_vars_to_set.map { |k, v| "#{ k }=#{ v }" }
80
+ run_command(['flynn', 'env', 'set', keys_and_values].flatten, filtered_text: env_vars_to_set.values)
81
+ end
82
+ end
83
+
84
+
85
+ def env_unset
86
+ if env_vars_to_unset.empty?
87
+ log 'No existing environment variables to unset.'
88
+ else
89
+ run_command(['flynn', 'env', 'unset', env_vars_to_unset])
90
+ end
91
+ end
92
+
93
+
94
+ def env_vars
95
+ @_env_vars ||= begin
96
+ {}.tap do |vars|
97
+ current_flynn_keys.each { |key| vars[key] = run_command(['flynn', 'env', 'get', key], quiet: true).chomp }
98
+ end
99
+ end
100
+ end
101
+
102
+
103
+ def env_vars_to_set
104
+ @_env_vars_to_set ||= desired_env_vars.reject { |k, v| env_vars.key?(k) && env_vars[k] == v }
105
+ end
106
+
107
+
108
+ def env_vars_to_unset
109
+ @_env_vars_to_unset ||= env_vars.keys - desired_env_vars.keys
110
+ end
111
+
112
+
113
+ def exported_aeevs_key
114
+ 'AEEV_KEYS'
115
+ end
116
+
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,77 @@
1
+ module Satorix
2
+ module CI
3
+ module Deploy
4
+ module Flynn
5
+ module Resources
6
+
7
+ def add_resources
8
+ if resources_to_add.empty?
9
+ log 'No resources to add.'
10
+ else
11
+ resources_to_add.each { |resource| run_command(['flynn', 'resource', 'add', resource]) }
12
+ end
13
+ end
14
+
15
+
16
+ def available_resources
17
+ %w(postgres mysql mongodb redis)
18
+ end
19
+
20
+
21
+ def current_resource_provider_names
22
+ resource.split("\n").drop(1).map(&:split).map(&:last)
23
+ end
24
+
25
+
26
+ def desired_resource_provider_names
27
+ names = ENV[resource_provider_key].to_s.split
28
+ disallowed = names - available_resources
29
+ unless disallowed.empty?
30
+ log_error_and_abort("Invalid resource#{ 's' if disallowed.length > 1 }: #{ disallowed.join(' ') }")
31
+ end
32
+ names
33
+ end
34
+
35
+
36
+ def remove_resources
37
+ if resources_to_remove.empty?
38
+ log 'No resources to remove.'
39
+ else
40
+ log "The following previously allocated resources are no longer defined in #{ resource_provider_key }:"
41
+ log resources_to_remove.join(' ')
42
+ log ''
43
+ log 'To remove them, use the flynn resource command: https://flynn.io/docs/cli#resource'
44
+ end
45
+ end
46
+
47
+
48
+ def resource
49
+ run_command(%w[flynn resource], quiet: true).chomp
50
+ end
51
+
52
+
53
+ def resource_provider_key
54
+ "FLYNN_#{ current_branch }_RESOURCES"
55
+ end
56
+
57
+
58
+ def resources_to_add
59
+ desired_resource_provider_names - current_resource_provider_names
60
+ end
61
+
62
+
63
+ def resources_to_remove
64
+ current_resource_provider_names - desired_resource_provider_names
65
+ end
66
+
67
+
68
+ def set_resources
69
+ add_resources
70
+ remove_resources
71
+ end
72
+
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,266 @@
1
+ module Satorix
2
+ module CI
3
+ module Deploy
4
+ module Flynn
5
+ module Routes
6
+
7
+ def find_or_create_route(domain)
8
+ route_ids = route_ids(domain)
9
+ if route_ids.empty?
10
+ log "Adding route for #{ domain }..."
11
+ route_ids << fc_route_add(domain)
12
+ log "Route for #{ domain } added with the ID of #{ route_ids.first }."
13
+ else
14
+ multiple = route_ids.length > 1
15
+ log "Route#{ 's' if multiple } already exist#{ 's' unless multiple } for #{ domain }."
16
+ end
17
+
18
+ route_ids
19
+ end
20
+
21
+
22
+ def configure_routes
23
+ defined_and_internal_routes.each do |ddev_id, domain|
24
+ route_ids = find_or_create_route(domain)
25
+ route_ids.each do |route_id|
26
+ add_tls_to_route(route_id: route_id, ddev_id: ddev_id)
27
+ end
28
+ end
29
+ end
30
+
31
+
32
+ def add_tls_to_route(route_id:, ddev_id:)
33
+ domain = defined_and_internal_routes[ddev_id]
34
+ if use_tls?(ddev_id)
35
+ if use_lets_encrypt?(ddev_id)
36
+ log "Using Let's Encrypt for #{ domain }."
37
+ log_error_and_abort "Let's Encrypt support is not implemented, yet!"
38
+ else
39
+ log "Using #{ user_defined_tls?(ddev_id) ? 'custom' : 'default' } certificate details for #{ domain } (#{ service_for_route_id(route_id) } service)."
40
+ File.open('crt', 'w') { |f| f.write(crt_for_ddev_id(ddev_id)) }
41
+ File.open('key', 'w') { |f| f.write(key_for_ddev_id(ddev_id)) }
42
+ fc_route_update(route_id)
43
+ end
44
+ else
45
+ log "Skipping TLS configuration for #{ domain }."
46
+ log "Environment variables #{ env_var_crt_prefix }#{ ddev_id } and #{ env_var_key_prefix }#{ ddev_id } have not been specified."
47
+ log 'For more information, please refer to https://www.satorix.com/docs/user/projects#certificates'
48
+ end
49
+ log ''
50
+ end
51
+
52
+
53
+ def canonical_domain
54
+ ENV["AEEV_#{ current_branch }_SATORIX_CANONICAL_URI_HOST"]
55
+ end
56
+
57
+
58
+ def canonical_domain_information
59
+ "\nThe above routes will all be redirected to the default URL:\n\n\t#{ canonical_uri }\n\n" if canonical_uri?
60
+ end
61
+
62
+
63
+ def canonical_domain_protocol
64
+ ENV["AEEV_#{ current_branch }_SATORIX_CANONICAL_URI_PROTOCOL"]
65
+ end
66
+
67
+
68
+ def canonical_uri
69
+ "#{ canonical_domain_protocol }://#{ canonical_domain }" if canonical_uri?
70
+ end
71
+
72
+
73
+ def canonical_uri?
74
+ [canonical_domain, canonical_domain_protocol].all? { |x| x.to_s !~ only_whitespace }
75
+ end
76
+
77
+
78
+ def crt_for_ddev_id(ddev_id)
79
+ ENV["#{ env_var_crt_prefix }#{ user_defined_tls?(ddev_id) ? ddev_id : 'DEFAULT' }"]
80
+ end
81
+
82
+
83
+ def custom_crt_for_ddev_id?(ddev_id)
84
+ ENV["#{ env_var_crt_prefix }#{ ddev_id }"].to_s !~ only_whitespace
85
+ end
86
+
87
+
88
+ def custom_key_for_ddev_id?(ddev_id)
89
+ ENV["#{ env_var_key_prefix }#{ ddev_id }"].to_s !~ only_whitespace
90
+ end
91
+
92
+
93
+ def defined_and_internal_routes
94
+ defined_routes.merge flynn_internal_routes
95
+ end
96
+
97
+
98
+ def defined_routes
99
+ {}.tap do |routes|
100
+ invalid_keys = flynn_internal_routes.keys
101
+ ENV.each do |key, value|
102
+ next unless key.start_with?(env_var_domain_prefix)
103
+ renamed_key = key.sub(env_var_domain_prefix, '')
104
+ message = "The user-defined route #{ key } conflicts with an internal route."
105
+ log_error_and_abort message if invalid_keys.include?(renamed_key)
106
+ routes[renamed_key] = value
107
+ end
108
+ end
109
+ end
110
+
111
+
112
+ def display_routing_information
113
+ log 'All application routes...'
114
+ log ''
115
+ log urlified_routes_for_display.map { |r| "\t#{ r }" }
116
+ log canonical_domain_information if canonical_uri?
117
+ end
118
+
119
+
120
+ def env_var_crt_prefix
121
+ "CRT_#{ current_branch }_"
122
+ end
123
+
124
+
125
+ def env_var_domain_prefix
126
+ "DDEV_#{ current_branch }_"
127
+ end
128
+
129
+
130
+ def env_var_key_prefix
131
+ "KEY_#{ current_branch }_"
132
+ end
133
+
134
+
135
+ def fc_route
136
+ run_command(['flynn', '-a', project_name, 'route'], quiet: true).chomp
137
+ end
138
+
139
+
140
+ def fc_route_add(domain)
141
+ run_command(['flynn', 'route', 'add', 'http', domain, '--sticky']).chomp
142
+ end
143
+
144
+
145
+ def fc_route_remove(route_id)
146
+ run_command(['flynn', 'route', 'remove', route_id])
147
+ end
148
+
149
+
150
+ def fc_route_update(route_id)
151
+ run_command(['flynn', 'route', 'update', route_id, '--tls-cert=crt', '--tls-key=key', '--sticky'], quiet: true)
152
+ end
153
+
154
+
155
+ def flynn_internal_routes
156
+ { 'FLYNN_INTERNAL' => "#{ project_name }.#{ domain_for_web_host }" }
157
+ end
158
+
159
+
160
+ def key_for_ddev_id(ddev_id)
161
+ ENV["#{ env_var_key_prefix }#{ user_defined_tls?(ddev_id) ? ddev_id : 'DEFAULT' }"]
162
+ end
163
+
164
+
165
+ def only_whitespace
166
+ /\A\s*\z/m
167
+ end
168
+
169
+
170
+ def remove_undefined_routes
171
+ routes_to_remove.each do |route|
172
+ log_header "Removing undefined route #{ route[routes_legend['ROUTE']] }..."
173
+ fc_route_remove route[routes_legend['ID']]
174
+ end
175
+ end
176
+
177
+
178
+ def route_ids(route)
179
+ [].tap do |route_ids|
180
+ routes_all_of('ROUTE').map { |r| r.sub(/https??:/i, '') }.each_with_index do |r, i|
181
+ route_ids << routes_all_of('ID')[i] if r == route
182
+ end
183
+ end
184
+ end
185
+
186
+
187
+ def routes
188
+ routes_with_legend.drop(1)
189
+ end
190
+
191
+
192
+ def routes_all_of(field)
193
+ routes.map { |a| a[routes_legend[field]] }
194
+ end
195
+
196
+
197
+ def routes_legend
198
+ {}.tap { |h| routes_with_legend.first.each_with_index { |route, index| h[route] = index } }
199
+ end
200
+
201
+
202
+ def routes_to_remove
203
+ routes.reject do |route|
204
+ defined_and_internal_routes.values.include? route[routes_legend['ROUTE']].partition(':').last
205
+ end
206
+ end
207
+
208
+
209
+ def routes_with_legend
210
+ fc_route.split("\n").map(&:split)
211
+ end
212
+
213
+
214
+ def service_for_route_id(route_id)
215
+ index = routes_all_of('ID').index(route_id)
216
+ routes_all_of('SERVICE')[index]
217
+ end
218
+
219
+
220
+ def setup_routes
221
+ configure_routes
222
+ remove_undefined_routes
223
+ display_routing_information
224
+ end
225
+
226
+
227
+ def urlified_routes
228
+ routes_all_of('ROUTE').map { |r| urlify_route r }
229
+ end
230
+
231
+
232
+ def urlified_routes_for_display
233
+ urlified_routes.map { |route| urlify_route_for_display route }.sort
234
+ end
235
+
236
+
237
+ def urlify_route(route)
238
+ route.sub(':', '://')
239
+ end
240
+
241
+
242
+ def urlify_route_for_display(route)
243
+ route =~ /^http:/ ? route : "#{ route.sub 'https', 'http' }, #{ route }"
244
+ end
245
+
246
+
247
+ def use_lets_encrypt?(ddev_id)
248
+ crt_for_ddev_id(ddev_id).to_s.gsub(/\W+/, '').casecmp('letsencrypt').zero? &&
249
+ key_for_ddev_id(ddev_id).to_s.gsub(/\W+/, '').casecmp('letsencrypt').zero?
250
+ end
251
+
252
+
253
+ def use_tls?(ddev_id)
254
+ [crt_for_ddev_id(ddev_id), key_for_ddev_id(ddev_id)].all? { |x| x.to_s !~ only_whitespace }
255
+ end
256
+
257
+
258
+ def user_defined_tls?(ddev_id)
259
+ custom_crt_for_ddev_id?(ddev_id) && custom_key_for_ddev_id?(ddev_id)
260
+ end
261
+
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end